[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/includes/ -> ajax-actions.php (source)

   1  <?php
   2  /**
   3   * Administration API: Core Ajax handlers
   4   *
   5   * @package WordPress
   6   * @subpackage Administration
   7   * @since 2.1.0
   8   */
   9  
  10  //
  11  // No-privilege Ajax handlers.
  12  //
  13  
  14  /**
  15   * Handles the Heartbeat API in the no-privilege context via AJAX .
  16   *
  17   * Runs when the user is not logged in.
  18   *
  19   * @since 3.6.0
  20   */
  21  function wp_ajax_nopriv_heartbeat() {
  22      $response = array();
  23  
  24      // 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'.
  25      if ( ! empty( $_POST['screen_id'] ) ) {
  26          $screen_id = sanitize_key( $_POST['screen_id'] );
  27      } else {
  28          $screen_id = 'front';
  29      }
  30  
  31      if ( ! empty( $_POST['data'] ) ) {
  32          $data = wp_unslash( (array) $_POST['data'] );
  33  
  34          /**
  35           * Filters Heartbeat Ajax response in no-privilege environments.
  36           *
  37           * @since 3.6.0
  38           *
  39           * @param array  $response  The no-priv Heartbeat response.
  40           * @param array  $data      The $_POST data sent.
  41           * @param string $screen_id The screen ID.
  42           */
  43          $response = apply_filters( 'heartbeat_nopriv_received', $response, $data, $screen_id );
  44      }
  45  
  46      /**
  47       * Filters Heartbeat Ajax response in no-privilege environments when no data is passed.
  48       *
  49       * @since 3.6.0
  50       *
  51       * @param array  $response  The no-priv Heartbeat response.
  52       * @param string $screen_id The screen ID.
  53       */
  54      $response = apply_filters( 'heartbeat_nopriv_send', $response, $screen_id );
  55  
  56      /**
  57       * Fires when Heartbeat ticks in no-privilege environments.
  58       *
  59       * Allows the transport to be easily replaced with long-polling.
  60       *
  61       * @since 3.6.0
  62       *
  63       * @param array  $response  The no-priv Heartbeat response.
  64       * @param string $screen_id The screen ID.
  65       */
  66      do_action( 'heartbeat_nopriv_tick', $response, $screen_id );
  67  
  68      // Send the current time according to the server.
  69      $response['server_time'] = time();
  70  
  71      wp_send_json( $response );
  72  }
  73  
  74  //
  75  // GET-based Ajax handlers.
  76  //
  77  
  78  /**
  79   * Handles fetching a list table via AJAX.
  80   *
  81   * @since 3.1.0
  82   */
  83  function wp_ajax_fetch_list() {
  84      $list_class = $_GET['list_args']['class'];
  85      check_ajax_referer( "fetch-list-$list_class", '_ajax_fetch_list_nonce' );
  86  
  87      $wp_list_table = _get_list_table( $list_class, array( 'screen' => $_GET['list_args']['screen']['id'] ) );
  88      if ( ! $wp_list_table ) {
  89          wp_die( 0 );
  90      }
  91  
  92      if ( ! $wp_list_table->ajax_user_can() ) {
  93          wp_die( -1 );
  94      }
  95  
  96      $wp_list_table->ajax_response();
  97  
  98      wp_die( 0 );
  99  }
 100  
 101  /**
 102   * Handles tag search via AJAX.
 103   *
 104   * @since 3.1.0
 105   */
 106  function wp_ajax_ajax_tag_search() {
 107      if ( ! isset( $_GET['tax'] ) ) {
 108          wp_die( 0 );
 109      }
 110  
 111      $taxonomy        = sanitize_key( $_GET['tax'] );
 112      $taxonomy_object = get_taxonomy( $taxonomy );
 113  
 114      if ( ! $taxonomy_object ) {
 115          wp_die( 0 );
 116      }
 117  
 118      if ( ! current_user_can( $taxonomy_object->cap->assign_terms ) ) {
 119          wp_die( -1 );
 120      }
 121  
 122      $search = wp_unslash( $_GET['q'] );
 123  
 124      $comma = _x( ',', 'tag delimiter' );
 125      if ( ',' !== $comma ) {
 126          $search = str_replace( $comma, ',', $search );
 127      }
 128  
 129      if ( str_contains( $search, ',' ) ) {
 130          $search = explode( ',', $search );
 131          $search = array_last( $search );
 132      }
 133  
 134      $search = trim( $search );
 135  
 136      /**
 137       * Filters the minimum number of characters required to fire a tag search via Ajax.
 138       *
 139       * @since 4.0.0
 140       *
 141       * @param int         $characters      The minimum number of characters required. Default 2.
 142       * @param WP_Taxonomy $taxonomy_object The taxonomy object.
 143       * @param string      $search          The search term.
 144       */
 145      $term_search_min_chars = (int) apply_filters( 'term_search_min_chars', 2, $taxonomy_object, $search );
 146  
 147      /*
 148       * Require $term_search_min_chars chars for matching (default: 2)
 149       * ensure it's a non-negative, non-zero integer.
 150       */
 151      if ( ( 0 === $term_search_min_chars ) || ( strlen( $search ) < $term_search_min_chars ) ) {
 152          wp_die();
 153      }
 154  
 155      $results = get_terms(
 156          array(
 157              'taxonomy'   => $taxonomy,
 158              'name__like' => $search,
 159              'fields'     => 'names',
 160              'hide_empty' => false,
 161              'number'     => isset( $_GET['number'] ) ? (int) $_GET['number'] : 0,
 162          )
 163      );
 164  
 165      /**
 166       * Filters the Ajax term search results.
 167       *
 168       * @since 6.1.0
 169       *
 170       * @param string[]    $results         Array of term names.
 171       * @param WP_Taxonomy $taxonomy_object The taxonomy object.
 172       * @param string      $search          The search term.
 173       */
 174      $results = apply_filters( 'ajax_term_search_results', $results, $taxonomy_object, $search );
 175  
 176      echo implode( "\n", $results );
 177      wp_die();
 178  }
 179  
 180  /**
 181   * Handles compression testing via AJAX.
 182   *
 183   * @since 3.1.0
 184   */
 185  function wp_ajax_wp_compression_test() {
 186      if ( ! current_user_can( 'manage_options' ) ) {
 187          wp_die( -1 );
 188      }
 189  
 190      if ( ini_get( 'zlib.output_compression' ) || 'ob_gzhandler' === ini_get( 'output_handler' ) ) {
 191          // Use `update_option()` on single site to mark the option for autoloading.
 192          if ( is_multisite() ) {
 193              update_site_option( 'can_compress_scripts', 0 );
 194          } else {
 195              update_option( 'can_compress_scripts', 0, true );
 196          }
 197          wp_die( 0 );
 198      }
 199  
 200      if ( isset( $_GET['test'] ) ) {
 201          header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' );
 202          header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
 203          header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
 204          header( 'Content-Type: application/javascript; charset=UTF-8' );
 205          $force_gzip = ( defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP );
 206          $test_str   = '"wpCompressionTest Lorem ipsum dolor sit amet consectetuer mollis sapien urna ut a. Eu nonummy condimentum fringilla tempor pretium platea vel nibh netus Maecenas. Hac molestie amet justo quis pellentesque est ultrices interdum nibh Morbi. Cras mattis pretium Phasellus ante ipsum ipsum ut sociis Suspendisse Lorem. Ante et non molestie. Porta urna Vestibulum egestas id congue nibh eu risus gravida sit. Ac augue auctor Ut et non a elit massa id sodales. Elit eu Nulla at nibh adipiscing mattis lacus mauris at tempus. Netus nibh quis suscipit nec feugiat eget sed lorem et urna. Pellentesque lacus at ut massa consectetuer ligula ut auctor semper Pellentesque. Ut metus massa nibh quam Curabitur molestie nec mauris congue. Volutpat molestie elit justo facilisis neque ac risus Ut nascetur tristique. Vitae sit lorem tellus et quis Phasellus lacus tincidunt nunc Fusce. Pharetra wisi Suspendisse mus sagittis libero lacinia Integer consequat ac Phasellus. Et urna ac cursus tortor aliquam Aliquam amet tellus volutpat Vestibulum. Justo interdum condimentum In augue congue tellus sollicitudin Quisque quis nibh."';
 207  
 208          if ( '1' === $_GET['test'] ) {
 209              echo $test_str;
 210              wp_die();
 211          } elseif ( '2' === $_GET['test'] ) {
 212              if ( ! isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
 213                  wp_die( -1 );
 214              }
 215  
 216              if ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate' ) && function_exists( 'gzdeflate' ) && ! $force_gzip ) {
 217                  header( 'Content-Encoding: deflate' );
 218                  $output = gzdeflate( $test_str, 1 );
 219              } elseif ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) && function_exists( 'gzencode' ) ) {
 220                  header( 'Content-Encoding: gzip' );
 221                  $output = gzencode( $test_str, 1 );
 222              } else {
 223                  wp_die( -1 );
 224              }
 225  
 226              echo $output;
 227              wp_die();
 228          } elseif ( 'no' === $_GET['test'] ) {
 229              check_ajax_referer( 'update_can_compress_scripts' );
 230              // Use `update_option()` on single site to mark the option for autoloading.
 231              if ( is_multisite() ) {
 232                  update_site_option( 'can_compress_scripts', 0 );
 233              } else {
 234                  update_option( 'can_compress_scripts', 0, true );
 235              }
 236          } elseif ( 'yes' === $_GET['test'] ) {
 237              check_ajax_referer( 'update_can_compress_scripts' );
 238              // Use `update_option()` on single site to mark the option for autoloading.
 239              if ( is_multisite() ) {
 240                  update_site_option( 'can_compress_scripts', 1 );
 241              } else {
 242                  update_option( 'can_compress_scripts', 1, true );
 243              }
 244          }
 245      }
 246  
 247      wp_die( 0 );
 248  }
 249  
 250  /**
 251   * Handles image editor previews via AJAX.
 252   *
 253   * @since 3.1.0
 254   */
 255  function wp_ajax_imgedit_preview() {
 256      $post_id = (int) $_GET['postid'];
 257      if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
 258          wp_die( -1 );
 259      }
 260  
 261      check_ajax_referer( "image_editor-$post_id" );
 262  
 263      require_once  ABSPATH . 'wp-admin/includes/image-edit.php';
 264  
 265      if ( ! stream_preview_image( $post_id ) ) {
 266          wp_die( -1 );
 267      }
 268  
 269      wp_die();
 270  }
 271  
 272  /**
 273   * Handles oEmbed caching via AJAX.
 274   *
 275   * @since 3.1.0
 276   *
 277   * @global WP_Embed $wp_embed WordPress Embed object.
 278   */
 279  function wp_ajax_oembed_cache() {
 280      $GLOBALS['wp_embed']->cache_oembed( $_GET['post'] );
 281      wp_die( 0 );
 282  }
 283  
 284  /**
 285   * Handles user autocomplete via AJAX.
 286   *
 287   * @since 3.4.0
 288   * @since 7.1.0 The search term is now sanitized, and a missing, non-string,
 289   *              or empty term results in a `0` response instead of an empty array.
 290   *
 291   * @return never
 292   */
 293  function wp_ajax_autocomplete_user() {
 294      if ( ! is_multisite() || ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
 295          wp_die( -1 );
 296      }
 297  
 298      /** This filter is documented in wp-admin/user-new.php */
 299      if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) {
 300          wp_die( -1 );
 301      }
 302  
 303      $return = array();
 304  
 305      // Obtain the search term, and short-circuit missing/invalid search term.
 306      if ( ! isset( $_REQUEST['term'] ) || ! is_string( $_REQUEST['term'] ) ) {
 307          wp_die( 0 );
 308      }
 309      /*
 310       * Asterisks are trimmed since wildcards are appended below. Without this, a
 311       * term consisting only of asterisks would result in an empty search that
 312       * matches all users.
 313       */
 314      $term = trim( sanitize_text_field( wp_unslash( $_REQUEST['term'] ) ), '*' );
 315      if ( '' === $term ) {
 316          wp_die( 0 );
 317      }
 318  
 319      /*
 320       * Check the type of request.
 321       * Current allowed values are `add` and `search`.
 322       */
 323      if ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] ) {
 324          $type = $_REQUEST['autocomplete_type'];
 325      } else {
 326          $type = 'add';
 327      }
 328  
 329      /*
 330       * Check the desired field for value.
 331       * Current allowed values are `user_email` and `user_login`.
 332       */
 333      if ( isset( $_REQUEST['autocomplete_field'] ) && 'user_email' === $_REQUEST['autocomplete_field'] ) {
 334          $field = $_REQUEST['autocomplete_field'];
 335      } else {
 336          $field = 'user_login';
 337      }
 338  
 339      // Exclude current users of this blog.
 340      if ( isset( $_REQUEST['site_id'] ) ) {
 341          $id = absint( $_REQUEST['site_id'] );
 342      } else {
 343          $id = get_current_blog_id();
 344      }
 345  
 346      $include_blog_users = ( 'search' === $type ? get_users(
 347          array(
 348              'blog_id' => $id,
 349              'fields'  => 'ID',
 350          )
 351      ) : array() );
 352  
 353      $exclude_blog_users = ( 'add' === $type ? get_users(
 354          array(
 355              'blog_id' => $id,
 356              'fields'  => 'ID',
 357          )
 358      ) : array() );
 359  
 360      $users = get_users(
 361          array(
 362              'blog_id'        => false,
 363              'search'         => '*' . $term . '*',
 364              'include'        => $include_blog_users,
 365              'exclude'        => $exclude_blog_users,
 366              'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ),
 367          )
 368      );
 369  
 370      foreach ( $users as $user ) {
 371          $return[] = array(
 372              /* translators: 1: User login, 2: User email address. */
 373              'label' => sprintf( _x( '%1$s (%2$s)', 'user autocomplete result' ), $user->user_login, $user->user_email ),
 374              'value' => $user->$field,
 375          );
 376      }
 377  
 378      wp_die( wp_json_encode( $return ) );
 379  }
 380  
 381  /**
 382   * Handles Ajax requests for community events
 383   *
 384   * @since 4.8.0
 385   */
 386  function wp_ajax_get_community_events() {
 387      require_once  ABSPATH . 'wp-admin/includes/class-wp-community-events.php';
 388  
 389      check_ajax_referer( 'community_events' );
 390  
 391      $search         = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : '';
 392      $timezone       = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : '';
 393      $user_id        = get_current_user_id();
 394      $saved_location = get_user_option( 'community-events-location', $user_id );
 395      $events_client  = new WP_Community_Events( $user_id, $saved_location );
 396      $events         = $events_client->get_events( $search, $timezone );
 397      $ip_changed     = false;
 398  
 399      if ( is_wp_error( $events ) ) {
 400          wp_send_json_error(
 401              array(
 402                  'error' => $events->get_error_message(),
 403              )
 404          );
 405      } else {
 406          if ( empty( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) ) {
 407              $ip_changed = true;
 408          } elseif ( isset( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) && $saved_location['ip'] !== $events['location']['ip'] ) {
 409              $ip_changed = true;
 410          }
 411  
 412          /*
 413           * The location should only be updated when it changes. The API doesn't always return
 414           * a full location; sometimes it's missing the description or country. The location
 415           * that was saved during the initial request is known to be good and complete, though.
 416           * It should be left intact until the user explicitly changes it (either by manually
 417           * searching for a new location, or by changing their IP address).
 418           *
 419           * If the location was updated with an incomplete response from the API, then it could
 420           * break assumptions that the UI makes (e.g., that there will always be a description
 421           * that corresponds to a latitude/longitude location).
 422           *
 423           * The location is stored network-wide, so that the user doesn't have to set it on each site.
 424           */
 425          if ( $ip_changed || $search ) {
 426              update_user_meta( $user_id, 'community-events-location', $events['location'] );
 427          }
 428  
 429          wp_send_json_success( $events );
 430      }
 431  }
 432  
 433  /**
 434   * Handles dashboard widgets via AJAX.
 435   *
 436   * @since 3.4.0
 437   */
 438  function wp_ajax_dashboard_widgets() {
 439      require_once  ABSPATH . 'wp-admin/includes/dashboard.php';
 440  
 441      $pagenow = $_GET['pagenow'];
 442      if ( 'dashboard-user' === $pagenow || 'dashboard-network' === $pagenow || 'dashboard' === $pagenow ) {
 443          set_current_screen( $pagenow );
 444      }
 445  
 446      switch ( $_GET['widget'] ) {
 447          case 'dashboard_primary':
 448              wp_dashboard_primary();
 449              break;
 450      }
 451      wp_die();
 452  }
 453  
 454  /**
 455   * Handles Customizer preview logged-in status via AJAX.
 456   *
 457   * @since 3.4.0
 458   */
 459  function wp_ajax_logged_in() {
 460      wp_die( 1 );
 461  }
 462  
 463  //
 464  // Ajax helpers.
 465  //
 466  
 467  /**
 468   * Sends back current comment total and new page links if they need to be updated.
 469   *
 470   * Contrary to normal success Ajax response ("1"), die with time() on success.
 471   *
 472   * @since 2.7.0
 473   * @access private
 474   *
 475   * @param int $comment_id Comment ID.
 476   * @param int $delta      Optional. Change in the number of total comments. Default -1.
 477   */
 478  function _wp_ajax_delete_comment_response( $comment_id, $delta = -1 ) {
 479      $total    = isset( $_POST['_total'] ) ? (int) $_POST['_total'] : 0;
 480      $per_page = isset( $_POST['_per_page'] ) ? (int) $_POST['_per_page'] : 0;
 481      $page     = isset( $_POST['_page'] ) ? (int) $_POST['_page'] : 0;
 482      $url      = isset( $_POST['_url'] ) ? sanitize_url( $_POST['_url'] ) : '';
 483  
 484      // JS didn't send us everything we need to know. Just die with success message.
 485      if ( ! $total || ! $per_page || ! $page || ! $url ) {
 486          $time           = time();
 487          $comment        = get_comment( $comment_id );
 488          $comment_status = '';
 489          $comment_link   = '';
 490  
 491          if ( $comment ) {
 492              $comment_status = $comment->comment_approved;
 493          }
 494  
 495          if ( 1 === (int) $comment_status ) {
 496              $comment_link = get_comment_link( $comment );
 497          }
 498  
 499          $counts = wp_count_comments();
 500  
 501          $response = new WP_Ajax_Response(
 502              array(
 503                  'what'         => 'comment',
 504                  // Here for completeness - not used.
 505                  'id'           => $comment_id,
 506                  'supplemental' => array(
 507                      'status'               => $comment_status,
 508                      'postId'               => $comment ? $comment->comment_post_ID : '',
 509                      'time'                 => $time,
 510                      'in_moderation'        => $counts->moderated,
 511                      'i18n_comments_text'   => sprintf(
 512                          /* translators: %s: Number of comments. */
 513                          _n( '%s Comment', '%s Comments', $counts->approved ),
 514                          number_format_i18n( $counts->approved )
 515                      ),
 516                      'i18n_moderation_text' => sprintf(
 517                          /* translators: %s: Number of comments. */
 518                          _n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
 519                          number_format_i18n( $counts->moderated )
 520                      ),
 521                      'comment_link'         => $comment_link,
 522                  ),
 523              )
 524          );
 525          $response->send();
 526      }
 527  
 528      $total += $delta;
 529      if ( $total < 0 ) {
 530          $total = 0;
 531      }
 532  
 533      // Only do the expensive stuff on a page-break, and about 1 other time per page.
 534      if ( 0 === $total % $per_page || 1 === mt_rand( 1, $per_page ) ) {
 535          $post_id = 0;
 536          // What type of comment count are we looking for?
 537          $status = 'all';
 538          $parsed = parse_url( $url );
 539  
 540          if ( isset( $parsed['query'] ) ) {
 541              parse_str( $parsed['query'], $query_vars );
 542  
 543              if ( ! empty( $query_vars['comment_status'] ) ) {
 544                  $status = $query_vars['comment_status'];
 545              }
 546  
 547              if ( ! empty( $query_vars['p'] ) ) {
 548                  $post_id = (int) $query_vars['p'];
 549              }
 550  
 551              if ( ! empty( $query_vars['comment_type'] ) ) {
 552                  $type = $query_vars['comment_type'];
 553              }
 554          }
 555  
 556          if ( empty( $type ) ) {
 557              // Only use the comment count if not filtering by a comment_type.
 558              $comment_count = wp_count_comments( $post_id );
 559  
 560              // We're looking for a known type of comment count.
 561              if ( isset( $comment_count->$status ) ) {
 562                  $total = $comment_count->$status;
 563              }
 564          }
 565          // Else use the decremented value from above.
 566      }
 567  
 568      // The time since the last comment count.
 569      $time    = time();
 570      $comment = get_comment( $comment_id );
 571      $counts  = wp_count_comments();
 572  
 573      $response = new WP_Ajax_Response(
 574          array(
 575              'what'         => 'comment',
 576              'id'           => $comment_id,
 577              'supplemental' => array(
 578                  'status'               => $comment ? $comment->comment_approved : '',
 579                  'postId'               => $comment ? $comment->comment_post_ID : '',
 580                  /* translators: %s: Number of comments. */
 581                  'total_items_i18n'     => sprintf( _n( '%s item', '%s items', $total ), number_format_i18n( $total ) ),
 582                  'total_pages'          => (int) ceil( $total / $per_page ),
 583                  'total_pages_i18n'     => number_format_i18n( (int) ceil( $total / $per_page ) ),
 584                  'total'                => $total,
 585                  'time'                 => $time,
 586                  'in_moderation'        => $counts->moderated,
 587                  'i18n_moderation_text' => sprintf(
 588                      /* translators: %s: Number of comments. */
 589                      _n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
 590                      number_format_i18n( $counts->moderated )
 591                  ),
 592              ),
 593          )
 594      );
 595      $response->send();
 596  }
 597  
 598  //
 599  // POST-based Ajax handlers.
 600  //
 601  
 602  /**
 603   * Handles adding a hierarchical term via AJAX.
 604   *
 605   * @since 3.1.0
 606   * @access private
 607   */
 608  function _wp_ajax_add_hierarchical_term() {
 609      $action   = $_POST['action'];
 610      $taxonomy = get_taxonomy( substr( $action, 4 ) );
 611      check_ajax_referer( $action, '_ajax_nonce-add-' . $taxonomy->name );
 612  
 613      if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
 614          wp_die( -1 );
 615      }
 616  
 617      $names  = explode( ',', $_POST[ 'new' . $taxonomy->name ] );
 618      $parent = isset( $_POST[ 'new' . $taxonomy->name . '_parent' ] ) ? (int) $_POST[ 'new' . $taxonomy->name . '_parent' ] : 0;
 619  
 620      if ( 0 > $parent ) {
 621          $parent = 0;
 622      }
 623  
 624      if ( 'category' === $taxonomy->name ) {
 625          $post_category = isset( $_POST['post_category'] ) ? (array) $_POST['post_category'] : array();
 626      } else {
 627          $post_category = ( isset( $_POST['tax_input'] ) && isset( $_POST['tax_input'][ $taxonomy->name ] ) ) ? (array) $_POST['tax_input'][ $taxonomy->name ] : array();
 628      }
 629  
 630      $checked_categories = array_map( 'absint', (array) $post_category );
 631      $popular_ids        = wp_popular_terms_checklist( $taxonomy->name, 0, 10, false );
 632  
 633      foreach ( $names as $category_name ) {
 634          $category_name     = trim( $category_name );
 635          $category_nicename = sanitize_title( $category_name );
 636  
 637          if ( '' === $category_nicename ) {
 638              continue;
 639          }
 640  
 641          $category_id = wp_insert_term( $category_name, $taxonomy->name, array( 'parent' => $parent ) );
 642  
 643          if ( ! $category_id || is_wp_error( $category_id ) ) {
 644              continue;
 645          } else {
 646              $category_id = $category_id['term_id'];
 647          }
 648  
 649          $checked_categories[] = $category_id;
 650  
 651          if ( $parent ) { // Do these all at once in a second.
 652              continue;
 653          }
 654  
 655          ob_start();
 656  
 657          wp_terms_checklist(
 658              0,
 659              array(
 660                  'taxonomy'             => $taxonomy->name,
 661                  'descendants_and_self' => $category_id,
 662                  'selected_cats'        => $checked_categories,
 663                  'popular_cats'         => $popular_ids,
 664              )
 665          );
 666  
 667          $data = ob_get_clean();
 668  
 669          $add = array(
 670              'what'     => $taxonomy->name,
 671              'id'       => $category_id,
 672              'data'     => str_replace( array( "\n", "\t" ), '', $data ),
 673              'position' => -1,
 674          );
 675      }
 676  
 677      if ( $parent ) { // Foncy - replace the parent and all its children.
 678          $parent  = get_term( $parent, $taxonomy->name );
 679          $term_id = $parent->term_id;
 680  
 681          while ( $parent->parent ) { // Get the top parent.
 682              $parent = get_term( $parent->parent, $taxonomy->name );
 683              if ( is_wp_error( $parent ) ) {
 684                  break;
 685              }
 686              $term_id = $parent->term_id;
 687          }
 688  
 689          ob_start();
 690  
 691          wp_terms_checklist(
 692              0,
 693              array(
 694                  'taxonomy'             => $taxonomy->name,
 695                  'descendants_and_self' => $term_id,
 696                  'selected_cats'        => $checked_categories,
 697                  'popular_cats'         => $popular_ids,
 698              )
 699          );
 700  
 701          $data = ob_get_clean();
 702  
 703          $add = array(
 704              'what'     => $taxonomy->name,
 705              'id'       => $term_id,
 706              'data'     => str_replace( array( "\n", "\t" ), '', $data ),
 707              'position' => -1,
 708          );
 709      }
 710  
 711      $parent_dropdown_args = array(
 712          'taxonomy'         => $taxonomy->name,
 713          'hide_empty'       => 0,
 714          'name'             => 'new' . $taxonomy->name . '_parent',
 715          'orderby'          => 'name',
 716          'hierarchical'     => 1,
 717          'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;',
 718      );
 719  
 720      /** This filter is documented in wp-admin/includes/meta-boxes.php */
 721      $parent_dropdown_args = apply_filters( 'post_edit_category_parent_dropdown_args', $parent_dropdown_args );
 722  
 723      ob_start();
 724  
 725      wp_dropdown_categories( $parent_dropdown_args );
 726  
 727      $supplemental = ob_get_clean();
 728  
 729      $add['supplemental'] = array( 'newcat_parent' => $supplemental );
 730  
 731      $response = new WP_Ajax_Response( $add );
 732      $response->send();
 733  }
 734  
 735  /**
 736   * Handles deleting a comment via AJAX.
 737   *
 738   * @since 3.1.0
 739   */
 740  function wp_ajax_delete_comment() {
 741      $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
 742  
 743      $comment = get_comment( $id );
 744  
 745      if ( ! $comment ) {
 746          wp_die( time() );
 747      }
 748  
 749      if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) {
 750          wp_die( -1 );
 751      }
 752  
 753      check_ajax_referer( "delete-comment_$id" );
 754      $status = wp_get_comment_status( $comment );
 755      $delta  = -1;
 756  
 757      if ( isset( $_POST['trash'] ) && '1' === $_POST['trash'] ) {
 758          if ( 'trash' === $status ) {
 759              wp_die( time() );
 760          }
 761  
 762          $result = wp_trash_comment( $comment );
 763      } elseif ( isset( $_POST['untrash'] ) && '1' === $_POST['untrash'] ) {
 764          if ( 'trash' !== $status ) {
 765              wp_die( time() );
 766          }
 767  
 768          $result = wp_untrash_comment( $comment );
 769  
 770          // Undo trash, not in Trash.
 771          if ( ! isset( $_POST['comment_status'] ) || 'trash' !== $_POST['comment_status'] ) {
 772              $delta = 1;
 773          }
 774      } elseif ( isset( $_POST['spam'] ) && '1' === $_POST['spam'] ) {
 775          if ( 'spam' === $status ) {
 776              wp_die( time() );
 777          }
 778  
 779          $result = wp_spam_comment( $comment );
 780      } elseif ( isset( $_POST['unspam'] ) && '1' === $_POST['unspam'] ) {
 781          if ( 'spam' !== $status ) {
 782              wp_die( time() );
 783          }
 784  
 785          $result = wp_unspam_comment( $comment );
 786  
 787          // Undo spam, not in spam.
 788          if ( ! isset( $_POST['comment_status'] ) || 'spam' !== $_POST['comment_status'] ) {
 789              $delta = 1;
 790          }
 791      } elseif ( isset( $_POST['delete'] ) && '1' === $_POST['delete'] ) {
 792          $result = wp_delete_comment( $comment );
 793      } else {
 794          wp_die( -1 );
 795      }
 796  
 797      if ( $result ) {
 798          // Decide if we need to send back '1' or a more complicated response including page links and comment counts.
 799          _wp_ajax_delete_comment_response( $comment->comment_ID, $delta );
 800      }
 801  
 802      wp_die( 0 );
 803  }
 804  
 805  /**
 806   * Handles deleting a tag via AJAX.
 807   *
 808   * @since 3.1.0
 809   */
 810  function wp_ajax_delete_tag() {
 811      $tag_id = (int) $_POST['tag_ID'];
 812      check_ajax_referer( "delete-tag_$tag_id" );
 813  
 814      if ( ! current_user_can( 'delete_term', $tag_id ) ) {
 815          wp_die( -1 );
 816      }
 817  
 818      $taxonomy = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag';
 819      $tag      = get_term( $tag_id, $taxonomy );
 820  
 821      if ( ! $tag || is_wp_error( $tag ) ) {
 822          wp_die( 1 );
 823      }
 824  
 825      if ( wp_delete_term( $tag_id, $taxonomy ) ) {
 826          wp_die( 1 );
 827      } else {
 828          wp_die( 0 );
 829      }
 830  }
 831  
 832  /**
 833   * Handles deleting a link via AJAX.
 834   *
 835   * @since 3.1.0
 836   */
 837  function wp_ajax_delete_link() {
 838      $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
 839  
 840      check_ajax_referer( "delete-bookmark_$id" );
 841  
 842      if ( ! current_user_can( 'manage_links' ) ) {
 843          wp_die( -1 );
 844      }
 845  
 846      $link = get_bookmark( $id );
 847      if ( ! $link || is_wp_error( $link ) ) {
 848          wp_die( 1 );
 849      }
 850  
 851      if ( wp_delete_link( $id ) ) {
 852          wp_die( 1 );
 853      } else {
 854          wp_die( 0 );
 855      }
 856  }
 857  
 858  /**
 859   * Handles deleting meta via AJAX.
 860   *
 861   * @since 3.1.0
 862   */
 863  function wp_ajax_delete_meta() {
 864      $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
 865  
 866      check_ajax_referer( "delete-meta_$id" );
 867      $meta = get_metadata_by_mid( 'post', $id );
 868  
 869      if ( ! $meta ) {
 870          wp_die( 1 );
 871      }
 872  
 873      if ( is_protected_meta( $meta->meta_key, 'post' ) || ! current_user_can( 'delete_post_meta', $meta->post_id, $meta->meta_key ) ) {
 874          wp_die( -1 );
 875      }
 876  
 877      if ( delete_meta( $meta->meta_id ) ) {
 878          wp_die( 1 );
 879      }
 880  
 881      wp_die( 0 );
 882  }
 883  
 884  /**
 885   * Handles deleting a post via AJAX.
 886   *
 887   * @since 3.1.0
 888   *
 889   * @param string $action Action to perform.
 890   */
 891  function wp_ajax_delete_post( $action ) {
 892      if ( empty( $action ) ) {
 893          $action = 'delete-post';
 894      }
 895  
 896      $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
 897      check_ajax_referer( "{$action}_$id" );
 898  
 899      if ( ! current_user_can( 'delete_post', $id ) ) {
 900          wp_die( -1 );
 901      }
 902  
 903      if ( ! get_post( $id ) ) {
 904          wp_die( 1 );
 905      }
 906  
 907      if ( wp_delete_post( $id ) ) {
 908          wp_die( 1 );
 909      } else {
 910          wp_die( 0 );
 911      }
 912  }
 913  
 914  /**
 915   * Handles sending a post to the Trash via AJAX.
 916   *
 917   * @since 3.1.0
 918   *
 919   * @param string $action Action to perform.
 920   */
 921  function wp_ajax_trash_post( $action ) {
 922      if ( empty( $action ) ) {
 923          $action = 'trash-post';
 924      }
 925  
 926      $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
 927      check_ajax_referer( "{$action}_$id" );
 928  
 929      if ( ! current_user_can( 'delete_post', $id ) ) {
 930          wp_die( -1 );
 931      }
 932  
 933      if ( ! get_post( $id ) ) {
 934          wp_die( 1 );
 935      }
 936  
 937      if ( 'trash-post' === $action ) {
 938          $done = wp_trash_post( $id );
 939      } else {
 940          $done = wp_untrash_post( $id );
 941      }
 942  
 943      if ( $done ) {
 944          wp_die( 1 );
 945      }
 946  
 947      wp_die( 0 );
 948  }
 949  
 950  /**
 951   * Handles restoring a post from the Trash via AJAX.
 952   *
 953   * @since 3.1.0
 954   *
 955   * @param string $action Action to perform.
 956   */
 957  function wp_ajax_untrash_post( $action ) {
 958      if ( empty( $action ) ) {
 959          $action = 'untrash-post';
 960      }
 961  
 962      wp_ajax_trash_post( $action );
 963  }
 964  
 965  /**
 966   * Handles deleting a page via AJAX.
 967   *
 968   * @since 3.1.0
 969   *
 970   * @param string $action Action to perform.
 971   */
 972  function wp_ajax_delete_page( $action ) {
 973      if ( empty( $action ) ) {
 974          $action = 'delete-page';
 975      }
 976  
 977      $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
 978      check_ajax_referer( "{$action}_$id" );
 979  
 980      if ( ! current_user_can( 'delete_page', $id ) ) {
 981          wp_die( -1 );
 982      }
 983  
 984      if ( ! get_post( $id ) ) {
 985          wp_die( 1 );
 986      }
 987  
 988      if ( wp_delete_post( $id ) ) {
 989          wp_die( 1 );
 990      } else {
 991          wp_die( 0 );
 992      }
 993  }
 994  
 995  /**
 996   * Handles dimming a comment via AJAX.
 997   *
 998   * @since 3.1.0
 999   */
1000  function wp_ajax_dim_comment() {
1001      $id      = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
1002      $comment = get_comment( $id );
1003  
1004      if ( ! $comment ) {
1005          $response = new WP_Ajax_Response(
1006              array(
1007                  'what' => 'comment',
1008                  'id'   => new WP_Error(
1009                      'invalid_comment',
1010                      /* translators: %d: Comment ID. */
1011                      sprintf( __( 'Comment %d does not exist' ), $id )
1012                  ),
1013              )
1014          );
1015          $response->send();
1016      }
1017  
1018      if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && ! current_user_can( 'moderate_comments' ) ) {
1019          wp_die( -1 );
1020      }
1021  
1022      $current = wp_get_comment_status( $comment );
1023  
1024      if ( isset( $_POST['new'] ) && $_POST['new'] === $current ) {
1025          wp_die( time() );
1026      }
1027  
1028      check_ajax_referer( "approve-comment_$id" );
1029  
1030      if ( in_array( $current, array( 'unapproved', 'spam' ), true ) ) {
1031          $result = wp_set_comment_status( $comment, 'approve', true );
1032      } else {
1033          $result = wp_set_comment_status( $comment, 'hold', true );
1034      }
1035  
1036      if ( is_wp_error( $result ) ) {
1037          $response = new WP_Ajax_Response(
1038              array(
1039                  'what' => 'comment',
1040                  'id'   => $result,
1041              )
1042          );
1043          $response->send();
1044      }
1045  
1046      // Decide if we need to send back '1' or a more complicated response including page links and comment counts.
1047      _wp_ajax_delete_comment_response( $comment->comment_ID );
1048      wp_die( 0 );
1049  }
1050  
1051  /**
1052   * Handles adding a link category via AJAX.
1053   *
1054   * @since 3.1.0
1055   *
1056   * @param string $action Action to perform.
1057   */
1058  function wp_ajax_add_link_category( $action ) {
1059      if ( empty( $action ) ) {
1060          $action = 'add-link-category';
1061      }
1062  
1063      check_ajax_referer( $action );
1064  
1065      $taxonomy_object = get_taxonomy( 'link_category' );
1066  
1067      if ( ! current_user_can( $taxonomy_object->cap->manage_terms ) ) {
1068          wp_die( -1 );
1069      }
1070  
1071      $names    = explode( ',', wp_unslash( $_POST['newcat'] ) );
1072      $response = new WP_Ajax_Response();
1073  
1074      foreach ( $names as $category_name ) {
1075          $category_name = trim( $category_name );
1076          $slug          = sanitize_title( $category_name );
1077  
1078          if ( '' === $slug ) {
1079              continue;
1080          }
1081  
1082          $category_id = wp_insert_term( $category_name, 'link_category' );
1083  
1084          if ( ! $category_id || is_wp_error( $category_id ) ) {
1085              continue;
1086          } else {
1087              $category_id = $category_id['term_id'];
1088          }
1089  
1090          $category_name = esc_html( $category_name );
1091  
1092          $response->add(
1093              array(
1094                  'what'     => 'link-category',
1095                  'id'       => $category_id,
1096                  'data'     => "<li id='link-category-$category_id'><label for='in-link-category-$category_id' class='selectit'><input value='" . esc_attr( $category_id ) . "' type='checkbox' checked='checked' name='link_category[]' id='in-link-category-$category_id'/> $category_name</label></li>",
1097                  'position' => -1,
1098              )
1099          );
1100      }
1101  
1102      $response->send();
1103  }
1104  
1105  /**
1106   * Handles adding a tag via AJAX.
1107   *
1108   * @since 3.1.0
1109   */
1110  function wp_ajax_add_tag() {
1111      check_ajax_referer( 'add-tag', '_wpnonce_add-tag' );
1112  
1113      $taxonomy        = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag';
1114      $taxonomy_object = get_taxonomy( $taxonomy );
1115  
1116      if ( ! current_user_can( $taxonomy_object->cap->edit_terms ) ) {
1117          wp_die( -1 );
1118      }
1119  
1120      $response = new WP_Ajax_Response();
1121  
1122      $tag = wp_insert_term( $_POST['tag-name'], $taxonomy, $_POST );
1123  
1124      if ( $tag && ! is_wp_error( $tag ) ) {
1125          $tag = get_term( $tag['term_id'], $taxonomy );
1126      }
1127  
1128      if ( ! $tag || is_wp_error( $tag ) ) {
1129          $message    = __( 'An error has occurred. Please reload the page and try again.' );
1130          $error_code = 'error';
1131  
1132          if ( is_wp_error( $tag ) && $tag->get_error_message() ) {
1133              $message = $tag->get_error_message();
1134          }
1135  
1136          if ( is_wp_error( $tag ) && $tag->get_error_code() ) {
1137              $error_code = $tag->get_error_code();
1138          }
1139  
1140          $response->add(
1141              array(
1142                  'what' => 'taxonomy',
1143                  'data' => new WP_Error( $error_code, $message ),
1144              )
1145          );
1146          $response->send();
1147      }
1148  
1149      $wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => $_POST['screen'] ) );
1150  
1151      $level      = 0;
1152      $no_parents = '';
1153  
1154      if ( is_taxonomy_hierarchical( $taxonomy ) ) {
1155          $level = count( get_ancestors( $tag->term_id, $taxonomy, 'taxonomy' ) );
1156          ob_start();
1157          $wp_list_table->single_row( $tag, $level );
1158          $no_parents = ob_get_clean();
1159      }
1160  
1161      ob_start();
1162      $wp_list_table->single_row( $tag );
1163      $parents = ob_get_clean();
1164  
1165      require  ABSPATH . 'wp-admin/includes/edit-tag-messages.php';
1166  
1167      $message = '';
1168      if ( isset( $messages[ $taxonomy_object->name ][1] ) ) {
1169          $message = $messages[ $taxonomy_object->name ][1];
1170      } elseif ( isset( $messages['_item'][1] ) ) {
1171          $message = $messages['_item'][1];
1172      }
1173  
1174      $response->add(
1175          array(
1176              'what'         => 'taxonomy',
1177              'data'         => $message,
1178              'supplemental' => array(
1179                  'parents'   => $parents,
1180                  'noparents' => $no_parents,
1181                  'notice'    => $message,
1182              ),
1183          )
1184      );
1185  
1186      $response->add(
1187          array(
1188              'what'         => 'term',
1189              'position'     => $level,
1190              'supplemental' => (array) $tag,
1191          )
1192      );
1193  
1194      $response->send();
1195  }
1196  
1197  /**
1198   * Handles getting a tagcloud via AJAX.
1199   *
1200   * @since 3.1.0
1201   */
1202  function wp_ajax_get_tagcloud() {
1203      if ( ! isset( $_POST['tax'] ) ) {
1204          wp_die( 0 );
1205      }
1206  
1207      $taxonomy        = sanitize_key( $_POST['tax'] );
1208      $taxonomy_object = get_taxonomy( $taxonomy );
1209  
1210      if ( ! $taxonomy_object ) {
1211          wp_die( 0 );
1212      }
1213  
1214      if ( ! current_user_can( $taxonomy_object->cap->assign_terms ) ) {
1215          wp_die( -1 );
1216      }
1217  
1218      $tags = get_terms(
1219          array(
1220              'taxonomy' => $taxonomy,
1221              'number'   => 45,
1222              'orderby'  => 'count',
1223              'order'    => 'DESC',
1224          )
1225      );
1226  
1227      if ( empty( $tags ) ) {
1228          wp_die( $taxonomy_object->labels->not_found );
1229      }
1230  
1231      if ( is_wp_error( $tags ) ) {
1232          wp_die( $tags->get_error_message() );
1233      }
1234  
1235      foreach ( $tags as $key => $tag ) {
1236          $tags[ $key ]->link = '#';
1237          $tags[ $key ]->id   = $tag->term_id;
1238      }
1239  
1240      // We need raw tag names here, so don't filter the output.
1241      $return = wp_generate_tag_cloud(
1242          $tags,
1243          array(
1244              'filter' => 0,
1245              'format' => 'list',
1246          )
1247      );
1248  
1249      if ( empty( $return ) ) {
1250          wp_die( 0 );
1251      }
1252  
1253      echo $return;
1254      wp_die();
1255  }
1256  
1257  /**
1258   * Handles getting comments via AJAX.
1259   *
1260   * @since 3.1.0
1261   *
1262   * @global int $post_id Post ID.
1263   *
1264   * @param string $action Action to perform.
1265   */
1266  function wp_ajax_get_comments( $action ) {
1267      global $post_id;
1268  
1269      if ( empty( $action ) ) {
1270          $action = 'get-comments';
1271      }
1272  
1273      check_ajax_referer( $action );
1274  
1275      if ( empty( $post_id ) && ! empty( $_REQUEST['p'] ) ) {
1276          $id = absint( $_REQUEST['p'] );
1277          if ( ! empty( $id ) ) {
1278              $post_id = $id;
1279          }
1280      }
1281  
1282      if ( empty( $post_id ) ) {
1283          wp_die( -1 );
1284      }
1285  
1286      $wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1287  
1288      if ( ! current_user_can( 'edit_post', $post_id ) ) {
1289          wp_die( -1 );
1290      }
1291  
1292      $wp_list_table->prepare_items();
1293  
1294      if ( ! $wp_list_table->has_items() ) {
1295          wp_die( 1 );
1296      }
1297  
1298      $response = new WP_Ajax_Response();
1299  
1300      ob_start();
1301      foreach ( $wp_list_table->items as $comment ) {
1302          if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && 0 === $comment->comment_approved ) {
1303              continue;
1304          }
1305          get_comment( $comment );
1306          $wp_list_table->single_row( $comment );
1307      }
1308      $comment_list_item = ob_get_clean();
1309  
1310      $response->add(
1311          array(
1312              'what' => 'comments',
1313              'data' => $comment_list_item,
1314          )
1315      );
1316  
1317      $response->send();
1318  }
1319  
1320  /**
1321   * Handles replying to a comment via AJAX.
1322   *
1323   * @since 3.1.0
1324   *
1325   * @param string $action Action to perform.
1326   */
1327  function wp_ajax_replyto_comment( $action ) {
1328      if ( empty( $action ) ) {
1329          $action = 'replyto-comment';
1330      }
1331  
1332      check_ajax_referer( $action, '_ajax_nonce-replyto-comment' );
1333  
1334      $comment_post_id = (int) $_POST['comment_post_ID'];
1335      $post            = get_post( $comment_post_id );
1336  
1337      if ( ! $post ) {
1338          wp_die( -1 );
1339      }
1340  
1341      if ( ! current_user_can( 'edit_post', $comment_post_id ) ) {
1342          wp_die( -1 );
1343      }
1344  
1345      if ( empty( $post->post_status ) ) {
1346          wp_die( 1 );
1347      } elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) {
1348          wp_die( __( 'You cannot reply to a comment on a draft post.' ) );
1349      }
1350  
1351      $user = wp_get_current_user();
1352  
1353      if ( $user->exists() ) {
1354          $comment_author       = wp_slash( $user->display_name );
1355          $comment_author_email = wp_slash( $user->user_email );
1356          $comment_author_url   = wp_slash( $user->user_url );
1357          $user_id              = $user->ID;
1358  
1359          if ( current_user_can( 'unfiltered_html' ) ) {
1360              if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) {
1361                  $_POST['_wp_unfiltered_html_comment'] = '';
1362              }
1363  
1364              if ( wp_create_nonce( 'unfiltered-html-comment' ) !== $_POST['_wp_unfiltered_html_comment'] ) {
1365                  kses_remove_filters(); // Start with a clean slate.
1366                  kses_init_filters();   // Set up the filters.
1367                  remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
1368                  add_filter( 'pre_comment_content', 'wp_filter_kses' );
1369              }
1370          }
1371      } else {
1372          wp_die( __( 'Sorry, you must be logged in to reply to a comment.' ) );
1373      }
1374  
1375      $comment_content = trim( $_POST['content'] );
1376  
1377      if ( '' === $comment_content ) {
1378          wp_die( __( 'Please type your comment text.' ) );
1379      }
1380  
1381      $comment_type = isset( $_POST['comment_type'] ) ? trim( $_POST['comment_type'] ) : 'comment';
1382  
1383      $comment_parent = 0;
1384  
1385      if ( isset( $_POST['comment_ID'] ) ) {
1386          $comment_parent = absint( $_POST['comment_ID'] );
1387      }
1388  
1389      $comment_auto_approved = false;
1390  
1391      $commentdata = array(
1392          'comment_post_ID' => $comment_post_id,
1393      );
1394  
1395      $commentdata += compact(
1396          'comment_author',
1397          'comment_author_email',
1398          'comment_author_url',
1399          'comment_content',
1400          'comment_type',
1401          'comment_parent',
1402          'user_id'
1403      );
1404  
1405      // Automatically approve parent comment.
1406      if ( ! empty( $_POST['approve_parent'] ) ) {
1407          $parent = get_comment( $comment_parent );
1408  
1409          if ( $parent && '0' === $parent->comment_approved && (int) $parent->comment_post_ID === $comment_post_id ) {
1410              if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) {
1411                  wp_die( -1 );
1412              }
1413  
1414              if ( wp_set_comment_status( $parent, 'approve' ) ) {
1415                  $comment_auto_approved = true;
1416              }
1417          }
1418      }
1419  
1420      $comment_id = wp_new_comment( $commentdata );
1421  
1422      if ( is_wp_error( $comment_id ) ) {
1423          wp_die( $comment_id->get_error_message() );
1424      }
1425  
1426      $comment = get_comment( $comment_id );
1427  
1428      if ( ! $comment ) {
1429          wp_die( 1 );
1430      }
1431  
1432      $position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';
1433  
1434      ob_start();
1435      if ( isset( $_REQUEST['mode'] ) && 'dashboard' === $_REQUEST['mode'] ) {
1436          require_once  ABSPATH . 'wp-admin/includes/dashboard.php';
1437          _wp_dashboard_recent_comments_row( $comment );
1438      } else {
1439          if ( isset( $_REQUEST['mode'] ) && 'single' === $_REQUEST['mode'] ) {
1440              $wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1441          } else {
1442              $wp_list_table = _get_list_table( 'WP_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1443          }
1444          $wp_list_table->single_row( $comment );
1445      }
1446      $comment_list_item = ob_get_clean();
1447  
1448      $response_data = array(
1449          'what'     => 'comment',
1450          'id'       => $comment->comment_ID,
1451          'data'     => $comment_list_item,
1452          'position' => $position,
1453      );
1454  
1455      $counts = wp_count_comments();
1456  
1457      $response_data['supplemental'] = array(
1458          'in_moderation'        => $counts->moderated,
1459          'i18n_comments_text'   => sprintf(
1460              /* translators: %s: Number of comments. */
1461              _n( '%s Comment', '%s Comments', $counts->approved ),
1462              number_format_i18n( $counts->approved )
1463          ),
1464          'i18n_moderation_text' => sprintf(
1465              /* translators: %s: Number of comments. */
1466              _n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
1467              number_format_i18n( $counts->moderated )
1468          ),
1469      );
1470  
1471      if ( $comment_auto_approved ) {
1472          $response_data['supplemental']['parent_approved'] = $parent->comment_ID;
1473          $response_data['supplemental']['parent_post_id']  = $parent->comment_post_ID;
1474      }
1475  
1476      $response = new WP_Ajax_Response();
1477      $response->add( $response_data );
1478      $response->send();
1479  }
1480  
1481  /**
1482   * Handles editing a comment via AJAX.
1483   *
1484   * @since 3.1.0
1485   */
1486  function wp_ajax_edit_comment() {
1487      check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );
1488  
1489      $comment_id = (int) $_POST['comment_ID'];
1490  
1491      if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
1492          wp_die( -1 );
1493      }
1494  
1495      if ( '' === $_POST['content'] ) {
1496          wp_die( __( 'Please type your comment text.' ) );
1497      }
1498  
1499      if ( isset( $_POST['status'] ) ) {
1500          $_POST['comment_status'] = $_POST['status'];
1501      }
1502  
1503      $updated = edit_comment();
1504      if ( is_wp_error( $updated ) ) {
1505          wp_die( $updated->get_error_message() );
1506      }
1507  
1508      $position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';
1509      /*
1510       * Checkbox is used to differentiate between the Edit Comments screen (1)
1511       * and the Comments section on the Edit Post screen (0).
1512       */
1513      $checkbox      = ( isset( $_POST['checkbox'] ) && '1' === $_POST['checkbox'] ) ? 1 : 0;
1514      $wp_list_table = _get_list_table( $checkbox ? 'WP_Comments_List_Table' : 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1515  
1516      $comment = get_comment( $comment_id );
1517  
1518      if ( empty( $comment->comment_ID ) ) {
1519          wp_die( -1 );
1520      }
1521  
1522      ob_start();
1523      $wp_list_table->single_row( $comment );
1524      $comment_list_item = ob_get_clean();
1525  
1526      $response = new WP_Ajax_Response();
1527  
1528      $response->add(
1529          array(
1530              'what'     => 'edit_comment',
1531              'id'       => $comment->comment_ID,
1532              'data'     => $comment_list_item,
1533              'position' => $position,
1534          )
1535      );
1536  
1537      $response->send();
1538  }
1539  
1540  /**
1541   * Handles adding a menu item via AJAX.
1542   *
1543   * @since 3.1.0
1544   */
1545  function wp_ajax_add_menu_item() {
1546      check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' );
1547  
1548      if ( ! current_user_can( 'edit_theme_options' ) ) {
1549          wp_die( -1 );
1550      }
1551  
1552      require_once  ABSPATH . 'wp-admin/includes/nav-menu.php';
1553  
1554      /*
1555       * For performance reasons, we omit some object properties from the checklist.
1556       * The following is a hacky way to restore them when adding non-custom items.
1557       */
1558      $menu_items_data = array();
1559  
1560      foreach ( (array) $_POST['menu-item'] as $menu_item_data ) {
1561          if (
1562              ! empty( $menu_item_data['menu-item-type'] ) &&
1563              'custom' !== $menu_item_data['menu-item-type'] &&
1564              ! empty( $menu_item_data['menu-item-object-id'] )
1565          ) {
1566              switch ( $menu_item_data['menu-item-type'] ) {
1567                  case 'post_type':
1568                      $_object = get_post( $menu_item_data['menu-item-object-id'] );
1569                      break;
1570  
1571                  case 'post_type_archive':
1572                      $_object = get_post_type_object( $menu_item_data['menu-item-object'] );
1573                      break;
1574  
1575                  case 'taxonomy':
1576                      $_object = get_term( $menu_item_data['menu-item-object-id'], $menu_item_data['menu-item-object'] );
1577                      break;
1578              }
1579  
1580              $_menu_items = array_map( 'wp_setup_nav_menu_item', array( $_object ) );
1581              $_menu_item  = reset( $_menu_items );
1582  
1583              // Restore the missing menu item properties.
1584              $menu_item_data['menu-item-description'] = $_menu_item->description;
1585          }
1586  
1587          $menu_items_data[] = $menu_item_data;
1588      }
1589  
1590      $item_ids = wp_save_nav_menu_items( 0, $menu_items_data );
1591      if ( is_wp_error( $item_ids ) ) {
1592          wp_die( 0 );
1593      }
1594  
1595      $menu_items = array();
1596  
1597      foreach ( (array) $item_ids as $menu_item_id ) {
1598          $menu_object = get_post( $menu_item_id );
1599  
1600          if ( ! empty( $menu_object->ID ) ) {
1601              $menu_object        = wp_setup_nav_menu_item( $menu_object );
1602              $menu_object->title = empty( $menu_object->title ) ? __( 'Menu Item' ) : $menu_object->title;
1603              $menu_object->label = $menu_object->title; // Don't show "(pending)" in ajax-added items.
1604              $menu_items[]       = $menu_object;
1605          }
1606      }
1607  
1608      /** This filter is documented in wp-admin/includes/nav-menu.php */
1609      $walker_class_name = apply_filters( 'wp_edit_nav_menu_walker', 'Walker_Nav_Menu_Edit', $_POST['menu'] );
1610  
1611      if ( ! class_exists( $walker_class_name ) ) {
1612          wp_die( 0 );
1613      }
1614  
1615      if ( ! empty( $menu_items ) ) {
1616          $args = array(
1617              'after'       => '',
1618              'before'      => '',
1619              'link_after'  => '',
1620              'link_before' => '',
1621              'walker'      => new $walker_class_name(),
1622          );
1623  
1624          echo walk_nav_menu_tree( $menu_items, 0, (object) $args );
1625      }
1626  
1627      wp_die();
1628  }
1629  
1630  /**
1631   * Handles adding meta via AJAX.
1632   *
1633   * @since 3.1.0
1634   */
1635  function wp_ajax_add_meta() {
1636      check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' );
1637      $count   = 0;
1638      $post_id = (int) $_POST['post_id'];
1639      $post    = get_post( $post_id );
1640  
1641      if ( isset( $_POST['metakeyselect'] ) || isset( $_POST['metakeyinput'] ) ) {
1642          if ( ! $post || ! current_user_can( 'edit_post', $post_id ) ) {
1643              wp_die( -1 );
1644          }
1645  
1646          if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) {
1647              wp_die( 1 );
1648          }
1649  
1650          // If the post is an autodraft, save the post as a draft and then attempt to save the meta.
1651          if ( 'auto-draft' === $post->post_status ) {
1652              $post_data                = array();
1653              $post_data['action']      = 'draft'; // Warning fix.
1654              $post_data['post_ID']     = $post_id;
1655              $post_data['post_type']   = $post->post_type;
1656              $post_data['post_status'] = 'draft';
1657              $now                      = time();
1658  
1659              $post_data['post_title'] = sprintf(
1660                  /* translators: 1: Post creation date, 2: Post creation time. */
1661                  __( 'Draft created on %1$s at %2$s' ),
1662                  gmdate( __( 'F j, Y' ), $now ),
1663                  gmdate( __( 'g:i a' ), $now )
1664              );
1665  
1666              $post_id = edit_post( $post_data );
1667  
1668              if ( $post_id ) {
1669                  if ( is_wp_error( $post_id ) ) {
1670                      $response = new WP_Ajax_Response(
1671                          array(
1672                              'what' => 'meta',
1673                              'data' => $post_id,
1674                          )
1675                      );
1676                      $response->send();
1677                  }
1678  
1679                  $meta_id = add_meta( $post_id );
1680  
1681                  if ( ! $meta_id ) {
1682                      wp_die( __( 'Please provide a custom field value.' ) );
1683                  }
1684              } else {
1685                  wp_die( 0 );
1686              }
1687          } else {
1688              $meta_id = add_meta( $post_id );
1689  
1690              if ( ! $meta_id ) {
1691                  wp_die( __( 'Please provide a custom field value.' ) );
1692              }
1693          }
1694  
1695          $meta    = get_metadata_by_mid( 'post', $meta_id );
1696          $post_id = (int) $meta->post_id;
1697          $meta    = get_object_vars( $meta );
1698  
1699          $response = new WP_Ajax_Response(
1700              array(
1701                  'what'         => 'meta',
1702                  'id'           => $meta_id,
1703                  'data'         => _list_meta_row( $meta, $count ),
1704                  'position'     => 1,
1705                  'supplemental' => array( 'postid' => $post_id ),
1706              )
1707          );
1708      } else { // Update?
1709          $meta_id = (int) key( $_POST['meta'] );
1710          $key     = wp_unslash( $_POST['meta'][ $meta_id ]['key'] );
1711          $value   = wp_unslash( $_POST['meta'][ $meta_id ]['value'] );
1712  
1713          if ( '' === trim( $key ) ) {
1714              wp_die( __( 'Please provide a custom field name.' ) );
1715          }
1716  
1717          $meta = get_metadata_by_mid( 'post', $meta_id );
1718  
1719          if ( ! $meta ) {
1720              wp_die( 0 ); // If meta doesn't exist.
1721          }
1722  
1723          if (
1724              is_protected_meta( $meta->meta_key, 'post' ) || is_protected_meta( $key, 'post' ) ||
1725              ! current_user_can( 'edit_post_meta', $meta->post_id, $meta->meta_key ) ||
1726              ! current_user_can( 'edit_post_meta', $meta->post_id, $key )
1727          ) {
1728              wp_die( -1 );
1729          }
1730  
1731          if ( $meta->meta_value !== $value || $meta->meta_key !== $key ) {
1732              $update_result = update_metadata_by_mid( 'post', $meta_id, $value, $key );
1733  
1734              if ( ! $update_result ) {
1735                  wp_die( 0 ); // We know meta exists; we also know it's unchanged (or DB error, in which case there are bigger problems).
1736              }
1737          }
1738  
1739          $response = new WP_Ajax_Response(
1740              array(
1741                  'what'         => 'meta',
1742                  'id'           => $meta_id,
1743                  'old_id'       => $meta_id,
1744                  'data'         => _list_meta_row(
1745                      array(
1746                          'meta_key'   => $key,
1747                          'meta_value' => $value,
1748                          'meta_id'    => $meta_id,
1749                      ),
1750                      $c
1751                  ),
1752                  'position'     => 0,
1753                  'supplemental' => array( 'postid' => $meta->post_id ),
1754              )
1755          );
1756      }
1757  
1758      $response->send();
1759  }
1760  
1761  /**
1762   * Handles adding a user via AJAX.
1763   *
1764   * @since 3.1.0
1765   *
1766   * @param string $action Action to perform.
1767   */
1768  function wp_ajax_add_user( $action ) {
1769      if ( empty( $action ) ) {
1770          $action = 'add-user';
1771      }
1772  
1773      check_ajax_referer( $action );
1774  
1775      if ( ! current_user_can( 'create_users' ) ) {
1776          wp_die( -1 );
1777      }
1778  
1779      $user_id = edit_user();
1780  
1781      if ( ! $user_id ) {
1782          wp_die( 0 );
1783      } elseif ( is_wp_error( $user_id ) ) {
1784          $response = new WP_Ajax_Response(
1785              array(
1786                  'what' => 'user',
1787                  'id'   => $user_id,
1788              )
1789          );
1790          $response->send();
1791      }
1792  
1793      $user_object   = get_userdata( $user_id );
1794      $wp_list_table = _get_list_table( 'WP_Users_List_Table' );
1795  
1796      $role = current( $user_object->roles );
1797  
1798      $response = new WP_Ajax_Response(
1799          array(
1800              'what'         => 'user',
1801              'id'           => $user_id,
1802              'data'         => $wp_list_table->single_row( $user_object, '', $role ),
1803              'supplemental' => array(
1804                  'show-link' => sprintf(
1805                      /* translators: %s: The new user. */
1806                      __( 'User %s added' ),
1807                      '<a href="#user-' . $user_id . '">' . $user_object->user_login . '</a>'
1808                  ),
1809                  'role'      => $role,
1810              ),
1811          )
1812      );
1813      $response->send();
1814  }
1815  
1816  /**
1817   * Handles closed post boxes via AJAX.
1818   *
1819   * @since 3.1.0
1820   */
1821  function wp_ajax_closed_postboxes() {
1822      check_ajax_referer( 'closedpostboxes', 'closedpostboxesnonce' );
1823      $closed = isset( $_POST['closed'] ) ? explode( ',', $_POST['closed'] ) : array();
1824      $closed = array_filter( $closed );
1825  
1826      $hidden = isset( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array();
1827      $hidden = array_filter( $hidden );
1828  
1829      $page = $_POST['page'] ?? '';
1830  
1831      if ( sanitize_key( $page ) !== $page ) {
1832          wp_die( 0 );
1833      }
1834  
1835      $user = wp_get_current_user();
1836      if ( ! $user ) {
1837          wp_die( -1 );
1838      }
1839  
1840      if ( is_array( $closed ) ) {
1841          update_user_meta( $user->ID, "closedpostboxes_$page", $closed );
1842      }
1843  
1844      if ( is_array( $hidden ) ) {
1845          // Postboxes that are always shown.
1846          $hidden = array_diff( $hidden, array( 'submitdiv', 'linksubmitdiv', 'manage-menu', 'create-menu' ) );
1847          update_user_meta( $user->ID, "metaboxhidden_$page", $hidden );
1848      }
1849  
1850      wp_die( 1 );
1851  }
1852  
1853  /**
1854   * Handles hidden columns via AJAX.
1855   *
1856   * @since 3.1.0
1857   */
1858  function wp_ajax_hidden_columns() {
1859      check_ajax_referer( 'screen-options-nonce', 'screenoptionnonce' );
1860      $page = $_POST['page'] ?? '';
1861  
1862      if ( sanitize_key( $page ) !== $page ) {
1863          wp_die( 0 );
1864      }
1865  
1866      $user = wp_get_current_user();
1867      if ( ! $user ) {
1868          wp_die( -1 );
1869      }
1870  
1871      $hidden = ! empty( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array();
1872      update_user_meta( $user->ID, "manage{$page}columnshidden", $hidden );
1873  
1874      wp_die( 1 );
1875  }
1876  
1877  /**
1878   * Handles updating whether to display the welcome panel via AJAX.
1879   *
1880   * @since 3.1.0
1881   */
1882  function wp_ajax_update_welcome_panel() {
1883      check_ajax_referer( 'welcome-panel-nonce', 'welcomepanelnonce' );
1884  
1885      if ( ! current_user_can( 'edit_theme_options' ) ) {
1886          wp_die( -1 );
1887      }
1888  
1889      update_user_meta( get_current_user_id(), 'show_welcome_panel', empty( $_POST['visible'] ) ? 0 : 1 );
1890  
1891      wp_die( 1 );
1892  }
1893  
1894  /**
1895   * Handles for retrieving menu meta boxes via AJAX.
1896   *
1897   * @since 3.1.0
1898   */
1899  function wp_ajax_menu_get_metabox() {
1900      if ( ! current_user_can( 'edit_theme_options' ) ) {
1901          wp_die( -1 );
1902      }
1903  
1904      require_once  ABSPATH . 'wp-admin/includes/nav-menu.php';
1905  
1906      if ( isset( $_POST['item-type'] ) && 'post_type' === $_POST['item-type'] ) {
1907          $type     = 'posttype';
1908          $callback = 'wp_nav_menu_item_post_type_meta_box';
1909          $items    = (array) get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
1910      } elseif ( isset( $_POST['item-type'] ) && 'taxonomy' === $_POST['item-type'] ) {
1911          $type     = 'taxonomy';
1912          $callback = 'wp_nav_menu_item_taxonomy_meta_box';
1913          $items    = (array) get_taxonomies( array( 'show_ui' => true ), 'object' );
1914      }
1915  
1916      if ( ! empty( $_POST['item-object'] ) && isset( $items[ $_POST['item-object'] ] ) ) {
1917          $menus_meta_box_object = $items[ $_POST['item-object'] ];
1918  
1919          /** This filter is documented in wp-admin/includes/nav-menu.php */
1920          $item = apply_filters( 'nav_menu_meta_box_object', $menus_meta_box_object );
1921  
1922          $box_args = array(
1923              'id'       => 'add-' . $item->name,
1924              'title'    => $item->labels->name,
1925              'callback' => $callback,
1926              'args'     => $item,
1927          );
1928  
1929          ob_start();
1930          $callback( null, $box_args );
1931  
1932          $markup = ob_get_clean();
1933  
1934          echo wp_json_encode(
1935              array(
1936                  'replace-id' => $type . '-' . $item->name,
1937                  'markup'     => $markup,
1938              )
1939          );
1940      }
1941  
1942      wp_die();
1943  }
1944  
1945  /**
1946   * Handles internal linking via AJAX.
1947   *
1948   * @since 3.1.0
1949   */
1950  function wp_ajax_wp_link_ajax() {
1951      check_ajax_referer( 'internal-linking', '_ajax_linking_nonce' );
1952  
1953      $args = array();
1954  
1955      if ( isset( $_POST['search'] ) ) {
1956          $args['s'] = wp_unslash( $_POST['search'] );
1957      }
1958  
1959      if ( isset( $_POST['term'] ) ) {
1960          $args['s'] = wp_unslash( $_POST['term'] );
1961      }
1962  
1963      $args['pagenum'] = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
1964  
1965      if ( ! class_exists( '_WP_Editors', false ) ) {
1966          require  ABSPATH . WPINC . '/class-wp-editor.php';
1967      }
1968  
1969      $results = _WP_Editors::wp_link_query( $args );
1970  
1971      if ( ! isset( $results ) ) {
1972          wp_die( 0 );
1973      }
1974  
1975      echo wp_json_encode( $results );
1976      echo "\n";
1977  
1978      wp_die();
1979  }
1980  
1981  /**
1982   * Handles saving menu locations via AJAX.
1983   *
1984   * @since 3.1.0
1985   */
1986  function wp_ajax_menu_locations_save() {
1987      if ( ! current_user_can( 'edit_theme_options' ) ) {
1988          wp_die( -1 );
1989      }
1990  
1991      check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' );
1992  
1993      if ( ! isset( $_POST['menu-locations'] ) ) {
1994          wp_die( 0 );
1995      }
1996  
1997      set_theme_mod( 'nav_menu_locations', array_map( 'absint', $_POST['menu-locations'] ) );
1998      wp_die( 1 );
1999  }
2000  
2001  /**
2002   * Handles saving the meta box order via AJAX.
2003   *
2004   * @since 3.1.0
2005   */
2006  function wp_ajax_meta_box_order() {
2007      check_ajax_referer( 'meta-box-order' );
2008      $order        = isset( $_POST['order'] ) ? (array) $_POST['order'] : false;
2009      $page_columns = $_POST['page_columns'] ?? 'auto';
2010  
2011      if ( 'auto' !== $page_columns ) {
2012          $page_columns = (int) $page_columns;
2013      }
2014  
2015      $page = $_POST['page'] ?? '';
2016  
2017      if ( sanitize_key( $page ) !== $page ) {
2018          wp_die( 0 );
2019      }
2020  
2021      $user = wp_get_current_user();
2022      if ( ! $user ) {
2023          wp_die( -1 );
2024      }
2025  
2026      if ( $order ) {
2027          update_user_meta( $user->ID, "meta-box-order_$page", $order );
2028      }
2029  
2030      if ( $page_columns ) {
2031          update_user_meta( $user->ID, "screen_layout_$page", $page_columns );
2032      }
2033  
2034      wp_send_json_success();
2035  }
2036  
2037  /**
2038   * Handles menu quick searching via AJAX.
2039   *
2040   * @since 3.1.0
2041   */
2042  function wp_ajax_menu_quick_search() {
2043      if ( ! current_user_can( 'edit_theme_options' ) ) {
2044          wp_die( -1 );
2045      }
2046  
2047      require_once  ABSPATH . 'wp-admin/includes/nav-menu.php';
2048  
2049      _wp_ajax_menu_quick_search( $_POST );
2050  
2051      wp_die();
2052  }
2053  
2054  /**
2055   * Handles retrieving a permalink via AJAX.
2056   *
2057   * @since 3.1.0
2058   */
2059  function wp_ajax_get_permalink() {
2060      check_ajax_referer( 'getpermalink', 'getpermalinknonce' );
2061      $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
2062      wp_die( get_preview_post_link( $post_id ) );
2063  }
2064  
2065  /**
2066   * Handles retrieving a sample permalink via AJAX.
2067   *
2068   * @since 3.1.0
2069   */
2070  function wp_ajax_sample_permalink() {
2071      check_ajax_referer( 'samplepermalink', 'samplepermalinknonce' );
2072      $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
2073      $title   = $_POST['new_title'] ?? '';
2074      $slug    = $_POST['new_slug'] ?? null;
2075      wp_die( get_sample_permalink_html( $post_id, $title, $slug ) );
2076  }
2077  
2078  /**
2079   * Handles Quick Edit saving a post from a list table via AJAX.
2080   *
2081   * @since 3.1.0
2082   *
2083   * @global string $mode List table view mode.
2084   */
2085  function wp_ajax_inline_save() {
2086      global $mode;
2087  
2088      check_ajax_referer( 'inlineeditnonce', '_inline_edit' );
2089  
2090      if ( ! isset( $_POST['post_ID'] ) || ! (int) $_POST['post_ID'] ) {
2091          wp_die();
2092      }
2093  
2094      $post_id = (int) $_POST['post_ID'];
2095  
2096      if ( 'page' === $_POST['post_type'] ) {
2097          if ( ! current_user_can( 'edit_page', $post_id ) ) {
2098              wp_die( __( 'Sorry, you are not allowed to edit this page.' ) );
2099          }
2100      } else {
2101          if ( ! current_user_can( 'edit_post', $post_id ) ) {
2102              wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
2103          }
2104      }
2105  
2106      $last = wp_check_post_lock( $post_id );
2107  
2108      if ( $last ) {
2109          $last_user      = get_userdata( $last );
2110          $last_user_name = $last_user ? $last_user->display_name : __( 'Someone' );
2111  
2112          /* translators: %s: User's display name. */
2113          $msg_template = __( 'Saving is disabled: %s is currently editing this post.' );
2114  
2115          if ( 'page' === $_POST['post_type'] ) {
2116              /* translators: %s: User's display name. */
2117              $msg_template = __( 'Saving is disabled: %s is currently editing this page.' );
2118          }
2119  
2120          printf( $msg_template, esc_html( $last_user_name ) );
2121          wp_die();
2122      }
2123  
2124      $data = &$_POST;
2125  
2126      $post = get_post( $post_id, ARRAY_A );
2127      if ( ! $post ) {
2128          wp_die();
2129      }
2130  
2131      // Since it's coming from the database.
2132      $post = wp_slash( $post );
2133  
2134      $data['content'] = $post['post_content'];
2135      $data['excerpt'] = $post['post_excerpt'];
2136  
2137      // Rename.
2138      $data['user_ID'] = get_current_user_id();
2139  
2140      if ( isset( $data['post_parent'] ) ) {
2141          $data['parent_id'] = $data['post_parent'];
2142      }
2143  
2144      // Status.
2145      if ( isset( $data['keep_private'] ) && 'private' === $data['keep_private'] ) {
2146          $data['visibility']  = 'private';
2147          $data['post_status'] = 'private';
2148      } elseif ( isset( $data['_status'] ) ) {
2149          $data['post_status'] = $data['_status'];
2150      }
2151  
2152      if ( empty( $data['comment_status'] ) ) {
2153          $data['comment_status'] = 'closed';
2154      }
2155  
2156      if ( empty( $data['ping_status'] ) ) {
2157          $data['ping_status'] = 'closed';
2158      }
2159  
2160      // Exclude terms from taxonomies that are not supposed to appear in Quick Edit.
2161      if ( ! empty( $data['tax_input'] ) ) {
2162          foreach ( $data['tax_input'] as $taxonomy => $terms ) {
2163              $tax_object = get_taxonomy( $taxonomy );
2164              /** This filter is documented in wp-admin/includes/class-wp-posts-list-table.php */
2165              if ( ! apply_filters( 'quick_edit_show_taxonomy', $tax_object->show_in_quick_edit, $taxonomy, $post['post_type'] ) ) {
2166                  unset( $data['tax_input'][ $taxonomy ] );
2167              }
2168          }
2169      }
2170  
2171      // Hack: wp_unique_post_slug() doesn't work for drafts, so we will fake that our post is published.
2172      if ( ! empty( $data['post_name'] ) && in_array( $post['post_status'], array( 'draft', 'pending' ), true ) ) {
2173          $post['post_status'] = 'publish';
2174          $data['post_name']   = wp_unique_post_slug( $data['post_name'], $post['ID'], $post['post_status'], $post['post_type'], $post['post_parent'] );
2175      }
2176  
2177      // Update the post.
2178      edit_post();
2179  
2180      $wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => $_POST['screen'] ) );
2181  
2182      $mode = 'excerpt' === $_POST['post_view'] ? 'excerpt' : 'list';
2183  
2184      $level = 0;
2185      if ( is_post_type_hierarchical( $wp_list_table->screen->post_type ) ) {
2186          $request_post = array( get_post( $_POST['post_ID'] ) );
2187          $parent       = $request_post[0]->post_parent;
2188  
2189          while ( $parent > 0 ) {
2190              $parent_post = get_post( $parent );
2191              $parent      = $parent_post->post_parent;
2192              ++$level;
2193          }
2194      }
2195  
2196      $wp_list_table->display_rows( array( get_post( $_POST['post_ID'] ) ), $level );
2197  
2198      wp_die();
2199  }
2200  
2201  /**
2202   * Handles Quick Edit saving for a term via AJAX.
2203   *
2204   * @since 3.1.0
2205   */
2206  function wp_ajax_inline_save_tax() {
2207      check_ajax_referer( 'taxinlineeditnonce', '_inline_edit' );
2208  
2209      $taxonomy        = sanitize_key( $_POST['taxonomy'] );
2210      $taxonomy_object = get_taxonomy( $taxonomy );
2211  
2212      if ( ! $taxonomy_object ) {
2213          wp_die( 0 );
2214      }
2215  
2216      if ( ! isset( $_POST['tax_ID'] ) || ! (int) $_POST['tax_ID'] ) {
2217          wp_die( -1 );
2218      }
2219  
2220      $id = (int) $_POST['tax_ID'];
2221  
2222      if ( ! current_user_can( 'edit_term', $id ) ) {
2223          wp_die( -1 );
2224      }
2225  
2226      $wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => 'edit-' . $taxonomy ) );
2227  
2228      $tag                  = get_term( $id, $taxonomy );
2229      $_POST['description'] = $tag->description;
2230  
2231      $updated = wp_update_term( $id, $taxonomy, $_POST );
2232  
2233      if ( $updated && ! is_wp_error( $updated ) ) {
2234          $tag = get_term( $updated['term_id'], $taxonomy );
2235          if ( ! $tag || is_wp_error( $tag ) ) {
2236              if ( is_wp_error( $tag ) && $tag->get_error_message() ) {
2237                  wp_die( $tag->get_error_message() );
2238              }
2239              wp_die( __( 'Item not updated.' ) );
2240          }
2241      } else {
2242          if ( is_wp_error( $updated ) && $updated->get_error_message() ) {
2243              wp_die( $updated->get_error_message() );
2244          }
2245          wp_die( __( 'Item not updated.' ) );
2246      }
2247  
2248      $level  = 0;
2249      $parent = $tag->parent;
2250  
2251      while ( $parent > 0 ) {
2252          $parent_tag = get_term( $parent, $taxonomy );
2253          $parent     = $parent_tag->parent;
2254          ++$level;
2255      }
2256  
2257      $wp_list_table->single_row( $tag, $level );
2258      wp_die();
2259  }
2260  
2261  /**
2262   * Handles querying posts for the Find Posts modal via AJAX.
2263   *
2264   * @see window.findPosts
2265   *
2266   * @since 3.1.0
2267   */
2268  function wp_ajax_find_posts() {
2269      check_ajax_referer( 'find-posts' );
2270  
2271      $post_types = get_post_types( array( 'public' => true ), 'objects' );
2272      unset( $post_types['attachment'] );
2273  
2274      $args = array(
2275          'post_type'      => array_keys( $post_types ),
2276          'post_status'    => 'any',
2277          'posts_per_page' => 50,
2278      );
2279  
2280      $search = wp_unslash( $_POST['ps'] );
2281  
2282      if ( '' !== $search ) {
2283          $args['s'] = $search;
2284      }
2285  
2286      $posts = get_posts( $args );
2287  
2288      if ( ! $posts ) {
2289          wp_send_json_error( __( 'No items found.' ) );
2290      }
2291  
2292      $html      = '<table class="widefat"><thead><tr><th class="found-radio"><br /></th><th>' . __( 'Title' ) . '</th><th class="no-break">' . __( 'Type' ) . '</th><th class="no-break">' . __( 'Date' ) . '</th><th class="no-break">' . __( 'Status' ) . '</th></tr></thead><tbody>';
2293      $alternate = '';
2294      foreach ( $posts as $post ) {
2295          $title     = trim( $post->post_title ) ? $post->post_title : __( '(no title)' );
2296          $alternate = ( 'alternate' === $alternate ) ? '' : 'alternate';
2297  
2298          switch ( $post->post_status ) {
2299              case 'publish':
2300              case 'private':
2301                  $stat = __( 'Published' );
2302                  break;
2303              case 'future':
2304                  $stat = __( 'Scheduled' );
2305                  break;
2306              case 'pending':
2307                  $stat = __( 'Pending Review' );
2308                  break;
2309              case 'draft':
2310                  $stat = __( 'Draft' );
2311                  break;
2312          }
2313  
2314          if ( '0000-00-00 00:00:00' === $post->post_date ) {
2315              $time = '';
2316          } else {
2317              /* translators: Date format in table columns, see https://www.php.net/manual/datetime.format.php */
2318              $time = mysql2date( __( 'Y/m/d' ), $post->post_date );
2319          }
2320  
2321          $html .= '<tr class="' . trim( 'found-posts ' . $alternate ) . '"><td class="found-radio"><input type="radio" id="found-' . $post->ID . '" name="found_post_id" value="' . esc_attr( $post->ID ) . '"></td>';
2322          $html .= '<td><label for="found-' . $post->ID . '">' . esc_html( $title ) . '</label></td><td class="no-break">' . esc_html( $post_types[ $post->post_type ]->labels->singular_name ) . '</td><td class="no-break">' . esc_html( $time ) . '</td><td class="no-break">' . esc_html( $stat ) . ' </td></tr>' . "\n\n";
2323      }
2324  
2325      $html .= '</tbody></table>';
2326  
2327      wp_send_json_success( $html );
2328  }
2329  
2330  /**
2331   * Handles saving the widgets order via AJAX.
2332   *
2333   * @since 3.1.0
2334   */
2335  function wp_ajax_widgets_order() {
2336      check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' );
2337  
2338      if ( ! current_user_can( 'edit_theme_options' ) ) {
2339          wp_die( -1 );
2340      }
2341  
2342      unset( $_POST['savewidgets'], $_POST['action'] );
2343  
2344      // Save widgets order for all sidebars.
2345      if ( is_array( $_POST['sidebars'] ) ) {
2346          $sidebars = array();
2347  
2348          foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) {
2349              $sidebar = array();
2350  
2351              if ( ! empty( $val ) ) {
2352                  $val = explode( ',', $val );
2353  
2354                  foreach ( $val as $k => $v ) {
2355                      if ( ! str_contains( $v, 'widget-' ) ) {
2356                          continue;
2357                      }
2358  
2359                      $sidebar[ $k ] = substr( $v, strpos( $v, '_' ) + 1 );
2360                  }
2361              }
2362              $sidebars[ $key ] = $sidebar;
2363          }
2364  
2365          wp_set_sidebars_widgets( $sidebars );
2366          wp_die( 1 );
2367      }
2368  
2369      wp_die( -1 );
2370  }
2371  
2372  /**
2373   * Handles saving a widget via AJAX.
2374   *
2375   * @since 3.1.0
2376   *
2377   * @global array $wp_registered_widgets         Registered widgets.
2378   * @global array $wp_registered_widget_controls Registered widget controls.
2379   * @global array $wp_registered_widget_updates  Registered widget updates.
2380   */
2381  function wp_ajax_save_widget() {
2382      global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates;
2383  
2384      check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' );
2385  
2386      if ( ! current_user_can( 'edit_theme_options' ) || ! isset( $_POST['id_base'] ) ) {
2387          wp_die( -1 );
2388      }
2389  
2390      unset( $_POST['savewidgets'], $_POST['action'] );
2391  
2392      /**
2393       * Fires early when editing the widgets displayed in sidebars.
2394       *
2395       * @since 2.8.0
2396       */
2397      do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2398  
2399      /**
2400       * Fires early when editing the widgets displayed in sidebars.
2401       *
2402       * @since 2.8.0
2403       */
2404      do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2405  
2406      /** This action is documented in wp-admin/widgets-form.php */
2407      do_action( 'sidebar_admin_setup' );
2408  
2409      $id_base      = wp_unslash( $_POST['id_base'] );
2410      $widget_id    = wp_unslash( $_POST['widget-id'] );
2411      $sidebar_id   = $_POST['sidebar'];
2412      $multi_number = ! empty( $_POST['multi_number'] ) ? (int) $_POST['multi_number'] : 0;
2413      $settings     = isset( $_POST[ 'widget-' . $id_base ] ) && is_array( $_POST[ 'widget-' . $id_base ] ) ? $_POST[ 'widget-' . $id_base ] : false;
2414      $error        = '<p>' . __( 'An error has occurred. Please reload the page and try again.' ) . '</p>';
2415  
2416      $sidebars = wp_get_sidebars_widgets();
2417      $sidebar  = $sidebars[ $sidebar_id ] ?? array();
2418  
2419      // Delete.
2420      if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) {
2421  
2422          if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
2423              wp_die( $error );
2424          }
2425  
2426          $sidebar = array_diff( $sidebar, array( $widget_id ) );
2427          $_POST   = array(
2428              'sidebar'            => $sidebar_id,
2429              'widget-' . $id_base => array(),
2430              'the-widget-id'      => $widget_id,
2431              'delete_widget'      => '1',
2432          );
2433  
2434          /** This action is documented in wp-admin/widgets-form.php */
2435          do_action( 'delete_widget', $widget_id, $sidebar_id, $id_base );
2436  
2437      } elseif ( $settings && preg_match( '/__i__|%i%/', key( $settings ) ) ) {
2438          if ( ! $multi_number ) {
2439              wp_die( $error );
2440          }
2441  
2442          $_POST[ 'widget-' . $id_base ] = array( $multi_number => reset( $settings ) );
2443          $widget_id                     = $id_base . '-' . $multi_number;
2444          $sidebar[]                     = $widget_id;
2445      }
2446      $_POST['widget-id'] = $sidebar;
2447  
2448      foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
2449  
2450          if ( $name === $id_base ) {
2451              if ( ! is_callable( $control['callback'] ) ) {
2452                  continue;
2453              }
2454  
2455              ob_start();
2456                  call_user_func_array( $control['callback'], $control['params'] );
2457              ob_end_clean();
2458              break;
2459          }
2460      }
2461  
2462      if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) {
2463          $sidebars[ $sidebar_id ] = $sidebar;
2464          wp_set_sidebars_widgets( $sidebars );
2465          echo "deleted:$widget_id";
2466          wp_die();
2467      }
2468  
2469      if ( ! empty( $_POST['add_new'] ) ) {
2470          wp_die();
2471      }
2472  
2473      $form = $wp_registered_widget_controls[ $widget_id ];
2474      if ( $form ) {
2475          call_user_func_array( $form['callback'], $form['params'] );
2476      }
2477  
2478      wp_die();
2479  }
2480  
2481  /**
2482   * Handles updating a widget via AJAX.
2483   *
2484   * @since 3.9.0
2485   *
2486   * @global WP_Customize_Manager $wp_customize Customizer manager object.
2487   */
2488  function wp_ajax_update_widget() {
2489      global $wp_customize;
2490      $wp_customize->widgets->wp_ajax_update_widget();
2491  }
2492  
2493  /**
2494   * Handles removing inactive widgets via AJAX.
2495   *
2496   * @since 4.4.0
2497   */
2498  function wp_ajax_delete_inactive_widgets() {
2499      check_ajax_referer( 'remove-inactive-widgets', 'removeinactivewidgets' );
2500  
2501      if ( ! current_user_can( 'edit_theme_options' ) ) {
2502          wp_die( -1 );
2503      }
2504  
2505      unset( $_POST['removeinactivewidgets'], $_POST['action'] );
2506      /** This action is documented in wp-admin/includes/ajax-actions.php */
2507      do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2508      /** This action is documented in wp-admin/includes/ajax-actions.php */
2509      do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2510      /** This action is documented in wp-admin/widgets-form.php */
2511      do_action( 'sidebar_admin_setup' );
2512  
2513      $sidebars_widgets = wp_get_sidebars_widgets();
2514  
2515      foreach ( $sidebars_widgets['wp_inactive_widgets'] as $key => $widget_id ) {
2516          $pieces       = explode( '-', $widget_id );
2517          $multi_number = array_pop( $pieces );
2518          $id_base      = implode( '-', $pieces );
2519          $widget       = get_option( 'widget_' . $id_base );
2520          unset( $widget[ $multi_number ] );
2521          update_option( 'widget_' . $id_base, $widget );
2522          unset( $sidebars_widgets['wp_inactive_widgets'][ $key ] );
2523      }
2524  
2525      wp_set_sidebars_widgets( $sidebars_widgets );
2526  
2527      wp_die();
2528  }
2529  
2530  /**
2531   * Handles creating missing image sub-sizes for just uploaded images via AJAX.
2532   *
2533   * @since 5.3.0
2534   */
2535  function wp_ajax_media_create_image_subsizes() {
2536      check_ajax_referer( 'media-form' );
2537  
2538      if ( ! current_user_can( 'upload_files' ) ) {
2539          wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to upload files.' ) ) );
2540      }
2541  
2542      if ( empty( $_POST['attachment_id'] ) ) {
2543          wp_send_json_error( array( 'message' => __( 'Upload failed. Please reload and try again.' ) ) );
2544      }
2545  
2546      $attachment_id = (int) $_POST['attachment_id'];
2547  
2548      if ( ! empty( $_POST['_wp_upload_failed_cleanup'] ) ) {
2549          // Upload failed. Cleanup.
2550          if ( wp_attachment_is_image( $attachment_id ) && current_user_can( 'delete_post', $attachment_id ) ) {
2551              $attachment = get_post( $attachment_id );
2552  
2553              // Created at most 10 min ago.
2554              if ( $attachment && ( time() - strtotime( $attachment->post_date_gmt ) < 600 ) ) {
2555                  wp_delete_attachment( $attachment_id, true );
2556                  wp_send_json_success();
2557              }
2558          }
2559      }
2560  
2561      /*
2562       * Set a custom header with the attachment_id.
2563       * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
2564       */
2565      if ( ! headers_sent() ) {
2566          header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
2567      }
2568  
2569      /*
2570       * This can still be pretty slow and cause timeout or out of memory errors.
2571       * The js that handles the response would need to also handle HTTP 500 errors.
2572       */
2573      wp_update_image_subsizes( $attachment_id );
2574  
2575      if ( ! empty( $_POST['_legacy_support'] ) ) {
2576          // The old (inline) uploader. Only needs the attachment_id.
2577          $response = array( 'id' => $attachment_id );
2578      } else {
2579          // Media modal and Media Library grid view.
2580          $response = wp_prepare_attachment_for_js( $attachment_id );
2581  
2582          if ( ! $response ) {
2583              wp_send_json_error( array( 'message' => __( 'Upload failed.' ) ) );
2584          }
2585      }
2586  
2587      // At this point the image has been uploaded successfully.
2588      wp_send_json_success( $response );
2589  }
2590  
2591  /**
2592   * Handles uploading attachments via AJAX.
2593   *
2594   * @since 3.3.0
2595   */
2596  function wp_ajax_upload_attachment() {
2597      check_ajax_referer( 'media-form' );
2598      /*
2599       * This function does not use wp_send_json_success() / wp_send_json_error()
2600       * as the html4 Plupload handler requires a text/html Content-Type for older IE.
2601       * See https://core.trac.wordpress.org/ticket/31037
2602       */
2603  
2604      if ( ! current_user_can( 'upload_files' ) ) {
2605          echo wp_json_encode(
2606              array(
2607                  'success' => false,
2608                  'data'    => array(
2609                      'message'  => __( 'Sorry, you are not allowed to upload files.' ),
2610                      'filename' => esc_html( $_FILES['async-upload']['name'] ),
2611                  ),
2612              )
2613          );
2614  
2615          wp_die();
2616      }
2617  
2618      if ( isset( $_REQUEST['post_id'] ) ) {
2619          $post_id = $_REQUEST['post_id'];
2620  
2621          if ( ! current_user_can( 'edit_post', $post_id ) ) {
2622              echo wp_json_encode(
2623                  array(
2624                      'success' => false,
2625                      'data'    => array(
2626                          'message'  => __( 'Sorry, you are not allowed to attach files to this post.' ),
2627                          'filename' => esc_html( $_FILES['async-upload']['name'] ),
2628                      ),
2629                  )
2630              );
2631  
2632              wp_die();
2633          }
2634      } else {
2635          $post_id = null;
2636      }
2637  
2638      $post_data = ! empty( $_REQUEST['post_data'] ) ? _wp_get_allowed_postdata( _wp_translate_postdata( false, (array) $_REQUEST['post_data'] ) ) : array();
2639  
2640      if ( is_wp_error( $post_data ) ) {
2641          wp_die( $post_data->get_error_message() );
2642      }
2643  
2644      // If the context is custom header or background, make sure the uploaded file is an image.
2645      if ( isset( $post_data['context'] ) && in_array( $post_data['context'], array( 'custom-header', 'custom-background' ), true ) ) {
2646          $wp_filetype = wp_check_filetype_and_ext( $_FILES['async-upload']['tmp_name'], $_FILES['async-upload']['name'] );
2647  
2648          if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
2649              echo wp_json_encode(
2650                  array(
2651                      'success' => false,
2652                      'data'    => array(
2653                          'message'  => __( 'The uploaded file is not a valid image. Please try again.' ),
2654                          'filename' => esc_html( $_FILES['async-upload']['name'] ),
2655                      ),
2656                  )
2657              );
2658  
2659              wp_die();
2660          }
2661      }
2662  
2663      $attachment_id = media_handle_upload( 'async-upload', $post_id, $post_data );
2664  
2665      if ( is_wp_error( $attachment_id ) ) {
2666          echo wp_json_encode(
2667              array(
2668                  'success' => false,
2669                  'data'    => array(
2670                      'message'  => $attachment_id->get_error_message(),
2671                      'filename' => esc_html( $_FILES['async-upload']['name'] ),
2672                  ),
2673              )
2674          );
2675  
2676          wp_die();
2677      }
2678  
2679      if ( isset( $post_data['context'] ) && isset( $post_data['theme'] ) ) {
2680          if ( 'custom-background' === $post_data['context'] ) {
2681              update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', $post_data['theme'] );
2682          }
2683  
2684          if ( 'custom-header' === $post_data['context'] ) {
2685              update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', $post_data['theme'] );
2686          }
2687      }
2688  
2689      $attachment = wp_prepare_attachment_for_js( $attachment_id );
2690      if ( ! $attachment ) {
2691          wp_die();
2692      }
2693  
2694      echo wp_json_encode(
2695          array(
2696              'success' => true,
2697              'data'    => $attachment,
2698          )
2699      );
2700  
2701      wp_die();
2702  }
2703  
2704  /**
2705   * Handles image editing via AJAX.
2706   *
2707   * @since 3.1.0
2708   */
2709  function wp_ajax_image_editor() {
2710      $attachment_id = (int) $_POST['postid'];
2711  
2712      if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
2713          wp_die( -1 );
2714      }
2715  
2716      check_ajax_referer( "image_editor-$attachment_id" );
2717      require_once  ABSPATH . 'wp-admin/includes/image-edit.php';
2718  
2719      $message = false;
2720  
2721      switch ( $_POST['do'] ) {
2722          case 'save':
2723              $message = wp_save_image( $attachment_id );
2724              if ( ! empty( $message->error ) ) {
2725                  wp_send_json_error( $message );
2726              }
2727  
2728              wp_send_json_success( $message );
2729              break;
2730          case 'scale':
2731              $message = wp_save_image( $attachment_id );
2732              break;
2733          case 'restore':
2734              $message = wp_restore_image( $attachment_id );
2735              break;
2736      }
2737  
2738      ob_start();
2739      wp_image_editor( $attachment_id, $message );
2740      $html = ob_get_clean();
2741  
2742      if ( ! empty( $message->error ) ) {
2743          wp_send_json_error(
2744              array(
2745                  'message' => $message,
2746                  'html'    => $html,
2747              )
2748          );
2749      }
2750  
2751      wp_send_json_success(
2752          array(
2753              'message' => $message,
2754              'html'    => $html,
2755          )
2756      );
2757  }
2758  
2759  /**
2760   * Handles setting the featured image via AJAX.
2761   *
2762   * @since 3.1.0
2763   */
2764  function wp_ajax_set_post_thumbnail() {
2765      $json = ! empty( $_REQUEST['json'] ); // New-style request.
2766  
2767      $post_id = (int) $_POST['post_id'];
2768      if ( ! current_user_can( 'edit_post', $post_id ) ) {
2769          wp_die( -1 );
2770      }
2771  
2772      $thumbnail_id = (int) $_POST['thumbnail_id'];
2773  
2774      if ( $json ) {
2775          check_ajax_referer( "update-post_$post_id" );
2776      } else {
2777          check_ajax_referer( "set_post_thumbnail-$post_id" );
2778      }
2779  
2780      if ( -1 === $thumbnail_id ) {
2781          if ( delete_post_thumbnail( $post_id ) ) {
2782              $return = _wp_post_thumbnail_html( null, $post_id );
2783              $json ? wp_send_json_success( $return ) : wp_die( $return );
2784          } else {
2785              wp_die( 0 );
2786          }
2787      }
2788  
2789      if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) {
2790          $return = _wp_post_thumbnail_html( $thumbnail_id, $post_id );
2791          $json ? wp_send_json_success( $return ) : wp_die( $return );
2792      }
2793  
2794      wp_die( 0 );
2795  }
2796  
2797  /**
2798   * Handles retrieving HTML for the featured image via AJAX.
2799   *
2800   * @since 4.6.0
2801   */
2802  function wp_ajax_get_post_thumbnail_html() {
2803      $post_id = (int) $_POST['post_id'];
2804  
2805      check_ajax_referer( "update-post_$post_id" );
2806  
2807      if ( ! current_user_can( 'edit_post', $post_id ) ) {
2808          wp_die( -1 );
2809      }
2810  
2811      $thumbnail_id = (int) $_POST['thumbnail_id'];
2812  
2813      // For backward compatibility, -1 refers to no featured image.
2814      if ( -1 === $thumbnail_id ) {
2815          $thumbnail_id = null;
2816      }
2817  
2818      $return = _wp_post_thumbnail_html( $thumbnail_id, $post_id );
2819      wp_send_json_success( $return );
2820  }
2821  
2822  /**
2823   * Handles setting the featured image for an attachment via AJAX.
2824   *
2825   * @since 4.0.0
2826   *
2827   * @see set_post_thumbnail()
2828   */
2829  function wp_ajax_set_attachment_thumbnail() {
2830      if ( empty( $_POST['urls'] ) || ! is_array( $_POST['urls'] ) ) {
2831          wp_send_json_error();
2832      }
2833  
2834      $thumbnail_id = (int) $_POST['thumbnail_id'];
2835      if ( empty( $thumbnail_id ) ) {
2836          wp_send_json_error();
2837      }
2838  
2839      if ( false === check_ajax_referer( 'set-attachment-thumbnail', '_ajax_nonce', false ) ) {
2840          wp_send_json_error();
2841      }
2842  
2843      $post_ids = array();
2844      // For each URL, try to find its corresponding post ID.
2845      foreach ( $_POST['urls'] as $url ) {
2846          $post_id = attachment_url_to_postid( $url );
2847          if ( ! empty( $post_id ) ) {
2848              $post_ids[] = $post_id;
2849          }
2850      }
2851  
2852      if ( empty( $post_ids ) ) {
2853          wp_send_json_error();
2854      }
2855  
2856      $success = 0;
2857      // For each found attachment, set its thumbnail.
2858      foreach ( $post_ids as $post_id ) {
2859          if ( ! current_user_can( 'edit_post', $post_id ) ) {
2860              continue;
2861          }
2862  
2863          if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) {
2864              ++$success;
2865          }
2866      }
2867  
2868      if ( 0 === $success ) {
2869          wp_send_json_error();
2870      } else {
2871          wp_send_json_success();
2872      }
2873  
2874      wp_send_json_error();
2875  }
2876  
2877  /**
2878   * Handles formatting a date via AJAX.
2879   *
2880   * @since 3.1.0
2881   */
2882  function wp_ajax_date_format() {
2883      wp_die( date_i18n( sanitize_option( 'date_format', wp_unslash( $_POST['date'] ) ) ) );
2884  }
2885  
2886  /**
2887   * Handles formatting a time via AJAX.
2888   *
2889   * @since 3.1.0
2890   */
2891  function wp_ajax_time_format() {
2892      wp_die( date_i18n( sanitize_option( 'time_format', wp_unslash( $_POST['date'] ) ) ) );
2893  }
2894  
2895  /**
2896   * Handles saving posts from the fullscreen editor via AJAX.
2897   *
2898   * @since 3.1.0
2899   * @deprecated 4.3.0
2900   */
2901  function wp_ajax_wp_fullscreen_save_post() {
2902      $post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0;
2903  
2904      $post = null;
2905  
2906      if ( $post_id ) {
2907          $post = get_post( $post_id );
2908      }
2909  
2910      check_ajax_referer( 'update-post_' . $post_id, '_wpnonce' );
2911  
2912      $post_id = edit_post();
2913  
2914      if ( is_wp_error( $post_id ) ) {
2915          wp_send_json_error();
2916      }
2917  
2918      if ( $post ) {
2919          $last_date = mysql2date( __( 'F j, Y' ), $post->post_modified );
2920          $last_time = mysql2date( __( 'g:i a' ), $post->post_modified );
2921      } else {
2922          $last_date = date_i18n( __( 'F j, Y' ) );
2923          $last_time = date_i18n( __( 'g:i a' ) );
2924      }
2925  
2926      $last_id = get_post_meta( $post_id, '_edit_last', true );
2927      if ( $last_id ) {
2928          $last_user = get_userdata( $last_id );
2929          /* translators: 1: User's display name, 2: Date of last edit, 3: Time of last edit. */
2930          $last_edited = sprintf( __( 'Last edited by %1$s on %2$s at %3$s' ), esc_html( $last_user->display_name ), $last_date, $last_time );
2931      } else {
2932          /* translators: 1: Date of last edit, 2: Time of last edit. */
2933          $last_edited = sprintf( __( 'Last edited on %1$s at %2$s' ), $last_date, $last_time );
2934      }
2935  
2936      wp_send_json_success( array( 'last_edited' => $last_edited ) );
2937  }
2938  
2939  /**
2940   * Handles removing a post lock via AJAX.
2941   *
2942   * @since 3.1.0
2943   */
2944  function wp_ajax_wp_remove_post_lock() {
2945      if ( empty( $_POST['post_ID'] ) || empty( $_POST['active_post_lock'] ) ) {
2946          wp_die( 0 );
2947      }
2948  
2949      $post_id = (int) $_POST['post_ID'];
2950      $post    = get_post( $post_id );
2951  
2952      if ( ! $post ) {
2953          wp_die( 0 );
2954      }
2955  
2956      check_ajax_referer( 'update-post_' . $post_id );
2957  
2958      if ( ! current_user_can( 'edit_post', $post_id ) ) {
2959          wp_die( -1 );
2960      }
2961  
2962      $active_lock = array_map( 'absint', explode( ':', $_POST['active_post_lock'] ) );
2963  
2964      if ( get_current_user_id() !== $active_lock[1] ) {
2965          wp_die( 0 );
2966      }
2967  
2968      /**
2969       * Filters the post lock window duration.
2970       *
2971       * @since 3.3.0
2972       *
2973       * @param int $interval The interval in seconds the post lock duration
2974       *                      should last, plus 5 seconds. Default 150.
2975       */
2976      $new_lock = ( time() - apply_filters( 'wp_check_post_lock_window', 150 ) + 5 ) . ':' . $active_lock[1];
2977      update_post_meta( $post_id, '_edit_lock', $new_lock, implode( ':', $active_lock ) );
2978      wp_die( 1 );
2979  }
2980  
2981  /**
2982   * Handles dismissing a WordPress pointer via AJAX.
2983   *
2984   * @since 3.1.0
2985   */
2986  function wp_ajax_dismiss_wp_pointer() {
2987      $pointer = $_POST['pointer'];
2988  
2989      if ( sanitize_key( $pointer ) !== $pointer ) {
2990          wp_die( 0 );
2991      }
2992  
2993      //  check_ajax_referer( 'dismiss-pointer_' . $pointer );
2994  
2995      $dismissed = array_filter( explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) );
2996  
2997      if ( in_array( $pointer, $dismissed, true ) ) {
2998          wp_die( 0 );
2999      }
3000  
3001      $dismissed[] = $pointer;
3002      $dismissed   = implode( ',', $dismissed );
3003  
3004      update_user_meta( get_current_user_id(), 'dismissed_wp_pointers', $dismissed );
3005      wp_die( 1 );
3006  }
3007  
3008  /**
3009   * Handles getting an attachment via AJAX.
3010   *
3011   * @since 3.5.0
3012   */
3013  function wp_ajax_get_attachment() {
3014      if ( ! isset( $_REQUEST['id'] ) ) {
3015          wp_send_json_error();
3016      }
3017  
3018      $id = absint( $_REQUEST['id'] );
3019      if ( ! $id ) {
3020          wp_send_json_error();
3021      }
3022  
3023      $post = get_post( $id );
3024      if ( ! $post ) {
3025          wp_send_json_error();
3026      }
3027  
3028      if ( 'attachment' !== $post->post_type ) {
3029          wp_send_json_error();
3030      }
3031  
3032      if ( ! current_user_can( 'upload_files' ) ) {
3033          wp_send_json_error();
3034      }
3035  
3036      $attachment = wp_prepare_attachment_for_js( $id );
3037      if ( ! $attachment ) {
3038          wp_send_json_error();
3039      }
3040  
3041      wp_send_json_success( $attachment );
3042  }
3043  
3044  /**
3045   * Handles querying attachments via AJAX.
3046   *
3047   * @since 3.5.0
3048   */
3049  function wp_ajax_query_attachments() {
3050      if ( ! current_user_can( 'upload_files' ) ) {
3051          wp_send_json_error();
3052      }
3053  
3054      $query = isset( $_REQUEST['query'] ) ? (array) $_REQUEST['query'] : array();
3055      $keys  = array(
3056          's',
3057          'order',
3058          'orderby',
3059          'posts_per_page',
3060          'paged',
3061          'post_mime_type',
3062          'post_parent',
3063          'author',
3064          'post__in',
3065          'post__not_in',
3066          'year',
3067          'monthnum',
3068      );
3069  
3070      foreach ( get_taxonomies_for_attachments( 'objects' ) as $taxonomy ) {
3071          if ( $taxonomy->query_var && isset( $query[ $taxonomy->query_var ] ) ) {
3072              $keys[] = $taxonomy->query_var;
3073          }
3074      }
3075  
3076      $query              = array_intersect_key( $query, array_flip( $keys ) );
3077      $query['post_type'] = 'attachment';
3078  
3079      if (
3080          MEDIA_TRASH &&
3081          ! empty( $_REQUEST['query']['post_status'] ) &&
3082          'trash' === $_REQUEST['query']['post_status']
3083      ) {
3084          $query['post_status'] = 'trash';
3085      } else {
3086          $query['post_status'] = 'inherit';
3087      }
3088  
3089      if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) ) {
3090          $query['post_status'] .= ',private';
3091      }
3092  
3093      // Filter query clauses to include filenames.
3094      if ( isset( $query['s'] ) ) {
3095          add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
3096      }
3097  
3098      /**
3099       * Filters the arguments passed to WP_Query during an Ajax
3100       * call for querying attachments.
3101       *
3102       * @since 3.7.0
3103       *
3104       * @see WP_Query::parse_query()
3105       *
3106       * @param array $query An array of query variables.
3107       */
3108      $query             = apply_filters( 'ajax_query_attachments_args', $query );
3109      $attachments_query = new WP_Query( $query );
3110      update_post_parent_caches( $attachments_query->posts );
3111  
3112      $posts       = array_map( 'wp_prepare_attachment_for_js', $attachments_query->posts );
3113      $posts       = array_filter( $posts );
3114      $total_posts = $attachments_query->found_posts;
3115  
3116      if ( $total_posts < 1 ) {
3117          // Out-of-bounds, run the query again without LIMIT for total count.
3118          unset( $query['paged'] );
3119  
3120          $count_query = new WP_Query();
3121          $count_query->query( $query );
3122          $total_posts = $count_query->found_posts;
3123      }
3124  
3125      $posts_per_page = (int) $attachments_query->get( 'posts_per_page' );
3126  
3127      $max_pages = $posts_per_page ? (int) ceil( $total_posts / $posts_per_page ) : 0;
3128  
3129      header( 'X-WP-Total: ' . (int) $total_posts );
3130      header( 'X-WP-TotalPages: ' . $max_pages );
3131  
3132      wp_send_json_success( $posts );
3133  }
3134  
3135  /**
3136   * Handles updating attachment attributes via AJAX.
3137   *
3138   * @since 3.5.0
3139   */
3140  function wp_ajax_save_attachment() {
3141      if ( ! isset( $_REQUEST['id'] ) || ! isset( $_REQUEST['changes'] ) ) {
3142          wp_send_json_error();
3143      }
3144  
3145      $id = absint( $_REQUEST['id'] );
3146      if ( ! $id ) {
3147          wp_send_json_error();
3148      }
3149  
3150      check_ajax_referer( 'update-post_' . $id, 'nonce' );
3151  
3152      if ( ! current_user_can( 'edit_post', $id ) ) {
3153          wp_send_json_error();
3154      }
3155  
3156      $changes = $_REQUEST['changes'];
3157      $post    = get_post( $id, ARRAY_A );
3158      if ( ! $post ) {
3159          wp_send_json_error();
3160      }
3161  
3162      if ( 'attachment' !== $post['post_type'] ) {
3163          wp_send_json_error();
3164      }
3165  
3166      if ( isset( $changes['parent'] ) ) {
3167          $post['post_parent'] = $changes['parent'];
3168      }
3169  
3170      if ( isset( $changes['title'] ) ) {
3171          $post['post_title'] = $changes['title'];
3172      }
3173  
3174      if ( isset( $changes['caption'] ) ) {
3175          $post['post_excerpt'] = $changes['caption'];
3176      }
3177  
3178      if ( isset( $changes['description'] ) ) {
3179          $post['post_content'] = $changes['description'];
3180      }
3181  
3182      if ( MEDIA_TRASH && isset( $changes['status'] ) ) {
3183          $post['post_status'] = $changes['status'];
3184      }
3185  
3186      if ( isset( $changes['alt'] ) ) {
3187          $alt = wp_unslash( $changes['alt'] );
3188          if ( get_post_meta( $id, '_wp_attachment_image_alt', true ) !== $alt ) {
3189              $alt = wp_strip_all_tags( $alt, true );
3190              update_post_meta( $id, '_wp_attachment_image_alt', wp_slash( $alt ) );
3191          }
3192      }
3193  
3194      if ( wp_attachment_is( 'audio', $post['ID'] ) ) {
3195          $changed  = false;
3196          $id3_data = wp_get_attachment_metadata( $post['ID'] );
3197  
3198          if ( ! is_array( $id3_data ) ) {
3199              $changed  = true;
3200              $id3_data = array();
3201          }
3202  
3203          foreach ( wp_get_attachment_id3_keys( (object) $post, 'edit' ) as $key => $label ) {
3204              if ( isset( $changes[ $key ] ) ) {
3205                  $changed          = true;
3206                  $id3_data[ $key ] = sanitize_text_field( wp_unslash( $changes[ $key ] ) );
3207              }
3208          }
3209  
3210          if ( $changed ) {
3211              wp_update_attachment_metadata( $id, $id3_data );
3212          }
3213      }
3214  
3215      if ( MEDIA_TRASH && isset( $changes['status'] ) && 'trash' === $changes['status'] ) {
3216          wp_delete_post( $id );
3217      } else {
3218          wp_update_post( $post );
3219      }
3220  
3221      wp_send_json_success();
3222  }
3223  
3224  /**
3225   * Handles saving backward compatible attachment attributes via AJAX.
3226   *
3227   * @since 3.5.0
3228   */
3229  function wp_ajax_save_attachment_compat() {
3230      if ( ! isset( $_REQUEST['id'] ) ) {
3231          wp_send_json_error();
3232      }
3233  
3234      $id = absint( $_REQUEST['id'] );
3235      if ( ! $id ) {
3236          wp_send_json_error();
3237      }
3238  
3239      if ( empty( $_REQUEST['attachments'] ) || empty( $_REQUEST['attachments'][ $id ] ) ) {
3240          wp_send_json_error();
3241      }
3242  
3243      $attachment_data = $_REQUEST['attachments'][ $id ];
3244  
3245      check_ajax_referer( 'update-post_' . $id, 'nonce' );
3246  
3247      if ( ! current_user_can( 'edit_post', $id ) ) {
3248          wp_send_json_error();
3249      }
3250  
3251      $post = get_post( $id, ARRAY_A );
3252      if ( ! $post ) {
3253          wp_send_json_error();
3254      }
3255  
3256      if ( 'attachment' !== $post['post_type'] ) {
3257          wp_send_json_error();
3258      }
3259  
3260      /** This filter is documented in wp-admin/includes/media.php */
3261      $post = apply_filters( 'attachment_fields_to_save', $post, $attachment_data );
3262  
3263      if ( isset( $post['errors'] ) ) {
3264          $errors = $post['errors']; // @todo return me and display me!
3265          unset( $post['errors'] );
3266      }
3267  
3268      wp_update_post( $post );
3269  
3270      foreach ( get_attachment_taxonomies( $post ) as $taxonomy ) {
3271          if ( isset( $attachment_data[ $taxonomy ] ) ) {
3272              wp_set_object_terms( $id, array_map( 'trim', preg_split( '/,+/', $attachment_data[ $taxonomy ] ) ), $taxonomy, false );
3273          }
3274      }
3275  
3276      $attachment = wp_prepare_attachment_for_js( $id );
3277  
3278      if ( ! $attachment ) {
3279          wp_send_json_error();
3280      }
3281  
3282      wp_send_json_success( $attachment );
3283  }
3284  
3285  /**
3286   * Handles saving the attachment order via AJAX.
3287   *
3288   * @since 3.5.0
3289   */
3290  function wp_ajax_save_attachment_order() {
3291      if ( ! isset( $_REQUEST['post_id'] ) ) {
3292          wp_send_json_error();
3293      }
3294  
3295      $post_id = absint( $_REQUEST['post_id'] );
3296      if ( ! $post_id ) {
3297          wp_send_json_error();
3298      }
3299  
3300      if ( empty( $_REQUEST['attachments'] ) ) {
3301          wp_send_json_error();
3302      }
3303  
3304      check_ajax_referer( 'update-post_' . $post_id, 'nonce' );
3305  
3306      $attachments = $_REQUEST['attachments'];
3307  
3308      if ( ! current_user_can( 'edit_post', $post_id ) ) {
3309          wp_send_json_error();
3310      }
3311  
3312      foreach ( $attachments as $attachment_id => $menu_order ) {
3313          if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
3314              continue;
3315          }
3316  
3317          $attachment = get_post( $attachment_id );
3318  
3319          if ( ! $attachment ) {
3320              continue;
3321          }
3322  
3323          if ( 'attachment' !== $attachment->post_type ) {
3324              continue;
3325          }
3326  
3327          wp_update_post(
3328              array(
3329                  'ID'         => $attachment_id,
3330                  'menu_order' => $menu_order,
3331              )
3332          );
3333      }
3334  
3335      wp_send_json_success();
3336  }
3337  
3338  /**
3339   * Handles sending an attachment to the editor via AJAX.
3340   *
3341   * Generates the HTML to send an attachment to the editor.
3342   * Backward compatible with the {@see 'media_send_to_editor'} filter
3343   * and the chain of filters that follow.
3344   *
3345   * @since 3.5.0
3346   */
3347  function wp_ajax_send_attachment_to_editor() {
3348      check_ajax_referer( 'media-send-to-editor', 'nonce' );
3349  
3350      $attachment = wp_unslash( $_POST['attachment'] );
3351  
3352      $id = (int) $attachment['id'];
3353  
3354      $post = get_post( $id );
3355      if ( ! $post ) {
3356          wp_send_json_error();
3357      }
3358  
3359      if ( 'attachment' !== $post->post_type ) {
3360          wp_send_json_error();
3361      }
3362  
3363      if ( current_user_can( 'edit_post', $id ) ) {
3364          // If this attachment is unattached, attach it. Primarily a back compat thing.
3365          $insert_into_post_id = (int) $_POST['post_id'];
3366  
3367          if ( 0 === $post->post_parent && $insert_into_post_id ) {
3368              wp_update_post(
3369                  array(
3370                      'ID'          => $id,
3371                      'post_parent' => $insert_into_post_id,
3372                  )
3373              );
3374          }
3375      }
3376  
3377      $url = empty( $attachment['url'] ) ? '' : $attachment['url'];
3378      $rel = ( str_contains( $url, 'attachment_id' ) || get_attachment_link( $id ) === $url );
3379  
3380      remove_filter( 'media_send_to_editor', 'image_media_send_to_editor' );
3381  
3382      if ( str_starts_with( $post->post_mime_type, 'image' ) ) {
3383          $align = $attachment['align'] ?? 'none';
3384          $size  = $attachment['image-size'] ?? 'medium';
3385          $alt   = $attachment['image_alt'] ?? '';
3386  
3387          // No whitespace-only captions.
3388          $caption = $attachment['post_excerpt'] ?? '';
3389          if ( '' === trim( $caption ) ) {
3390              $caption = '';
3391          }
3392  
3393          $title = ''; // We no longer insert title tags into <img> tags, as they are redundant.
3394          $html  = get_image_send_to_editor( $id, $caption, $title, $align, $url, $rel, $size, $alt );
3395      } elseif ( wp_attachment_is( 'video', $post ) || wp_attachment_is( 'audio', $post ) ) {
3396          $html = stripslashes_deep( $_POST['html'] );
3397      } else {
3398          $html = $attachment['post_title'] ?? '';
3399          $rel  = $rel ? ' rel="attachment wp-att-' . $id . '"' : ''; // Hard-coded string, $id is already sanitized.
3400  
3401          if ( ! empty( $url ) ) {
3402              $html = '<a href="' . esc_url( $url ) . '"' . $rel . '>' . $html . '</a>';
3403          }
3404      }
3405  
3406      /** This filter is documented in wp-admin/includes/media.php */
3407      $html = apply_filters( 'media_send_to_editor', $html, $id, $attachment );
3408  
3409      wp_send_json_success( $html );
3410  }
3411  
3412  /**
3413   * Handles sending a link to the editor via AJAX.
3414   *
3415   * Generates the HTML to send a non-image embed link to the editor.
3416   *
3417   * Backward compatible with the following filters:
3418   * - file_send_to_editor_url
3419   * - audio_send_to_editor_url
3420   * - video_send_to_editor_url
3421   *
3422   * @since 3.5.0
3423   *
3424   * @global WP_Post  $post     Global post object.
3425   * @global WP_Embed $wp_embed WordPress Embed object.
3426   */
3427  function wp_ajax_send_link_to_editor() {
3428      global $post, $wp_embed;
3429  
3430      check_ajax_referer( 'media-send-to-editor', 'nonce' );
3431  
3432      $src = wp_unslash( $_POST['src'] );
3433      if ( ! $src ) {
3434          wp_send_json_error();
3435      }
3436  
3437      if ( ! strpos( $src, '://' ) ) {
3438          $src = 'http://' . $src;
3439      }
3440  
3441      $src = sanitize_url( $src );
3442      if ( ! $src ) {
3443          wp_send_json_error();
3444      }
3445  
3446      $link_text = trim( wp_unslash( $_POST['link_text'] ) );
3447      if ( ! $link_text ) {
3448          $link_text = wp_basename( $src );
3449      }
3450  
3451      $post = get_post( $_POST['post_id'] ?? 0 );
3452  
3453      // Ping WordPress for an embed.
3454      $check_embed = $wp_embed->run_shortcode( '[embed]' . $src . '[/embed]' );
3455  
3456      // Fallback that WordPress creates when no oEmbed was found.
3457      $fallback = $wp_embed->maybe_make_link( $src );
3458  
3459      if ( $check_embed !== $fallback ) {
3460          // TinyMCE view for [embed] will parse this.
3461          $html = '[embed]' . $src . '[/embed]';
3462      } elseif ( $link_text ) {
3463          $html = '<a href="' . esc_url( $src ) . '">' . $link_text . '</a>';
3464      } else {
3465          $html = '';
3466      }
3467  
3468      // Figure out what filter to run:
3469      $type      = 'file';
3470      $extension = preg_replace( '/^.+?\.([^.]+)$/', '$1', $src );
3471      if ( $extension ) {
3472          $extension_type = wp_ext2type( $extension );
3473          if ( 'audio' === $extension_type || 'video' === $extension_type ) {
3474              $type = $extension_type;
3475          }
3476      }
3477  
3478      /** This filter is documented in wp-admin/includes/media.php */
3479      $html = apply_filters( "{$type}_send_to_editor_url", $html, $src, $link_text );
3480  
3481      wp_send_json_success( $html );
3482  }
3483  
3484  /**
3485   * Handles the Heartbeat API via AJAX.
3486   *
3487   * Runs when the user is logged in.
3488   *
3489   * @since 3.6.0
3490   */
3491  function wp_ajax_heartbeat() {
3492      if ( empty( $_POST['_nonce'] ) ) {
3493          wp_send_json_error();
3494      }
3495  
3496      $response    = array();
3497      $data        = array();
3498      $nonce_state = wp_verify_nonce( $_POST['_nonce'], 'heartbeat-nonce' );
3499  
3500      // 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'.
3501      if ( ! empty( $_POST['screen_id'] ) ) {
3502          $screen_id = sanitize_key( $_POST['screen_id'] );
3503      } else {
3504          $screen_id = 'front';
3505      }
3506  
3507      if ( ! empty( $_POST['data'] ) ) {
3508          $data = wp_unslash( (array) $_POST['data'] );
3509      }
3510  
3511      if ( 1 !== $nonce_state ) {
3512          /**
3513           * Filters the nonces to send to the New/Edit Post screen.
3514           *
3515           * @since 4.3.0
3516           *
3517           * @param array  $response  The Heartbeat response.
3518           * @param array  $data      The $_POST data sent.
3519           * @param string $screen_id The screen ID.
3520           */
3521          $response = apply_filters( 'wp_refresh_nonces', $response, $data, $screen_id );
3522  
3523          if ( false === $nonce_state ) {
3524              // User is logged in but nonces have expired.
3525              $response['nonces_expired'] = true;
3526              wp_send_json( $response );
3527          }
3528      }
3529  
3530      if ( ! empty( $data ) ) {
3531          /**
3532           * Filters the Heartbeat response received.
3533           *
3534           * @since 3.6.0
3535           *
3536           * @param array  $response  The Heartbeat response.
3537           * @param array  $data      The $_POST data sent.
3538           * @param string $screen_id The screen ID.
3539           */
3540          $response = apply_filters( 'heartbeat_received', $response, $data, $screen_id );
3541      }
3542  
3543      /**
3544       * Filters the Heartbeat response sent.
3545       *
3546       * @since 3.6.0
3547       *
3548       * @param array  $response  The Heartbeat response.
3549       * @param string $screen_id The screen ID.
3550       */
3551      $response = apply_filters( 'heartbeat_send', $response, $screen_id );
3552  
3553      /**
3554       * Fires when Heartbeat ticks in logged-in environments.
3555       *
3556       * Allows the transport to be easily replaced with long-polling.
3557       *
3558       * @since 3.6.0
3559       *
3560       * @param array  $response  The Heartbeat response.
3561       * @param string $screen_id The screen ID.
3562       */
3563      do_action( 'heartbeat_tick', $response, $screen_id );
3564  
3565      // Send the current time according to the server.
3566      $response['server_time'] = time();
3567  
3568      wp_send_json( $response );
3569  }
3570  
3571  /**
3572   * Handles getting revision diffs via AJAX.
3573   *
3574   * @since 3.6.0
3575   */
3576  function wp_ajax_get_revision_diffs() {
3577      require  ABSPATH . 'wp-admin/includes/revision.php';
3578  
3579      $post = get_post( (int) $_REQUEST['post_id'] );
3580      if ( ! $post ) {
3581          wp_send_json_error();
3582      }
3583  
3584      if ( ! current_user_can( 'edit_post', $post->ID ) ) {
3585          wp_send_json_error();
3586      }
3587  
3588      // Really just pre-loading the cache here.
3589      $revisions = wp_get_post_revisions( $post->ID, array( 'check_enabled' => false ) );
3590      if ( ! $revisions ) {
3591          wp_send_json_error();
3592      }
3593  
3594      $return = array();
3595  
3596      // Increase the script timeout limit to allow ample time for diff UI setup.
3597      if ( function_exists( 'set_time_limit' ) ) {
3598          set_time_limit( 5 * MINUTE_IN_SECONDS );
3599      }
3600  
3601      foreach ( $_REQUEST['compare'] as $compare_key ) {
3602          list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to
3603  
3604          $return[] = array(
3605              'id'     => $compare_key,
3606              'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ),
3607          );
3608      }
3609      wp_send_json_success( $return );
3610  }
3611  
3612  /**
3613   * Handles auto-saving the selected color scheme for
3614   * a user's own profile via AJAX.
3615   *
3616   * @since 3.8.0
3617   *
3618   * @global array $_wp_admin_css_colors Registered admin CSS color schemes.
3619   */
3620  function wp_ajax_save_user_color_scheme() {
3621      global $_wp_admin_css_colors;
3622  
3623      check_ajax_referer( 'save-color-scheme', 'nonce' );
3624  
3625      $color_scheme = sanitize_key( $_POST['color_scheme'] );
3626  
3627      if ( ! isset( $_wp_admin_css_colors[ $color_scheme ] ) ) {
3628          wp_send_json_error();
3629      }
3630  
3631      $previous_color_scheme = get_user_meta( get_current_user_id(), 'admin_color', true );
3632      update_user_meta( get_current_user_id(), 'admin_color', $color_scheme );
3633  
3634      wp_send_json_success(
3635          array(
3636              'previousScheme' => 'admin-color-' . $previous_color_scheme,
3637              'currentScheme'  => 'admin-color-' . $color_scheme,
3638          )
3639      );
3640  }
3641  
3642  /**
3643   * Handles getting themes from themes_api() via AJAX.
3644   *
3645   * @since 3.9.0
3646   *
3647   * @global array $themes_allowedtags   Allowed HTML tags for theme descriptions.
3648   * @global array $theme_field_defaults Default theme fields.
3649   */
3650  function wp_ajax_query_themes() {
3651      global $themes_allowedtags, $theme_field_defaults;
3652  
3653      if ( ! current_user_can( 'install_themes' ) ) {
3654          wp_send_json_error();
3655      }
3656  
3657      $args = wp_parse_args(
3658          wp_unslash( $_REQUEST['request'] ),
3659          array(
3660              'per_page' => 20,
3661              'fields'   => array_merge(
3662                  (array) $theme_field_defaults,
3663                  array(
3664                      'reviews_url' => true, // Explicitly request the reviews URL to be linked from the Add Themes screen.
3665                  )
3666              ),
3667          )
3668      );
3669  
3670      if ( isset( $args['browse'] ) && 'favorites' === $args['browse'] && ! isset( $args['user'] ) ) {
3671          $user = get_user_option( 'wporg_favorites' );
3672          if ( $user ) {
3673              $args['user'] = $user;
3674          }
3675      }
3676  
3677      $old_filter = $args['browse'] ?? 'search';
3678  
3679      /** This filter is documented in wp-admin/includes/class-wp-theme-install-list-table.php */
3680      $args = apply_filters( 'install_themes_table_api_args_' . $old_filter, $args );
3681  
3682      $api = themes_api( 'query_themes', $args );
3683  
3684      if ( is_wp_error( $api ) ) {
3685          wp_send_json_error();
3686      }
3687  
3688      $update_php = network_admin_url( 'update.php?action=install-theme' );
3689  
3690      $installed_themes = search_theme_directories();
3691  
3692      if ( false === $installed_themes ) {
3693          $installed_themes = array();
3694      }
3695  
3696      foreach ( $installed_themes as $theme_slug => $theme_data ) {
3697          // Ignore child themes.
3698          if ( str_contains( $theme_slug, '/' ) ) {
3699              unset( $installed_themes[ $theme_slug ] );
3700          }
3701      }
3702  
3703      foreach ( $api->themes as &$theme ) {
3704          $theme->install_url = add_query_arg(
3705              array(
3706                  'theme'    => $theme->slug,
3707                  '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
3708              ),
3709              $update_php
3710          );
3711  
3712          if ( current_user_can( 'switch_themes' ) ) {
3713              if ( is_multisite() ) {
3714                  $theme->activate_url = add_query_arg(
3715                      array(
3716                          'action'   => 'enable',
3717                          '_wpnonce' => wp_create_nonce( 'enable-theme_' . $theme->slug ),
3718                          'theme'    => $theme->slug,
3719                      ),
3720                      network_admin_url( 'themes.php' )
3721                  );
3722              } else {
3723                  $theme->activate_url = add_query_arg(
3724                      array(
3725                          'action'     => 'activate',
3726                          '_wpnonce'   => wp_create_nonce( 'switch-theme_' . $theme->slug ),
3727                          'stylesheet' => $theme->slug,
3728                      ),
3729                      admin_url( 'themes.php' )
3730                  );
3731              }
3732          }
3733  
3734          $is_theme_installed = array_key_exists( $theme->slug, $installed_themes );
3735  
3736          // We only care about installed themes.
3737          $theme->block_theme = $is_theme_installed && wp_get_theme( $theme->slug )->is_block_theme();
3738  
3739          if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
3740              $customize_url = $theme->block_theme ? admin_url( 'site-editor.php' ) : wp_customize_url( $theme->slug );
3741  
3742              $theme->customize_url = add_query_arg(
3743                  array(
3744                      'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
3745                  ),
3746                  $customize_url
3747              );
3748          }
3749  
3750          $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
3751          $theme->author      = wp_kses( $theme->author['display_name'], $themes_allowedtags );
3752          $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
3753          $theme->description = wp_kses( $theme->description, $themes_allowedtags );
3754  
3755          $theme->stars = wp_star_rating(
3756              array(
3757                  'rating' => $theme->rating,
3758                  'type'   => 'percent',
3759                  'number' => $theme->num_ratings,
3760                  'echo'   => false,
3761              )
3762          );
3763  
3764          $theme->num_ratings    = number_format_i18n( $theme->num_ratings );
3765          $theme->preview_url    = set_url_scheme( $theme->preview_url );
3766          $theme->compatible_wp  = is_wp_version_compatible( $theme->requires );
3767          $theme->compatible_php = is_php_version_compatible( $theme->requires_php );
3768      }
3769  
3770      wp_send_json_success( $api );
3771  }
3772  
3773  /**
3774   * Applies [embed] Ajax handlers to a string.
3775   *
3776   * @since 4.0.0
3777   *
3778   * @global WP_Post    $post          Global post object.
3779   * @global WP_Embed   $wp_embed      WordPress Embed object.
3780   * @global WP_Scripts $wp_scripts    Script dependencies object.
3781   * @global int        $content_width Shared post content width.
3782   */
3783  function wp_ajax_parse_embed() {
3784      global $post, $wp_embed, $content_width;
3785  
3786      if ( empty( $_POST['shortcode'] ) ) {
3787          wp_send_json_error();
3788      }
3789  
3790      $post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0;
3791  
3792      if ( $post_id > 0 ) {
3793          $post = get_post( $post_id );
3794  
3795          if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
3796              wp_send_json_error();
3797          }
3798          setup_postdata( $post );
3799      } elseif ( ! current_user_can( 'edit_posts' ) ) { // See WP_oEmbed_Controller::get_proxy_item_permissions_check().
3800          wp_send_json_error();
3801      }
3802  
3803      $shortcode = wp_unslash( $_POST['shortcode'] );
3804  
3805      preg_match( '/' . get_shortcode_regex() . '/s', $shortcode, $matches );
3806      $atts = shortcode_parse_atts( $matches[3] );
3807  
3808      if ( ! empty( $matches[5] ) ) {
3809          $url = $matches[5];
3810      } elseif ( ! empty( $atts['src'] ) ) {
3811          $url = $atts['src'];
3812      } else {
3813          $url = '';
3814      }
3815  
3816      $parsed                         = false;
3817      $wp_embed->return_false_on_fail = true;
3818  
3819      if ( 0 === $post_id ) {
3820          /*
3821           * Refresh oEmbeds cached outside of posts that are past their TTL.
3822           * Posts are excluded because they have separate logic for refreshing
3823           * their post meta caches. See WP_Embed::cache_oembed().
3824           */
3825          $wp_embed->usecache = false;
3826      }
3827  
3828      if ( is_ssl() && str_starts_with( $url, 'http://' ) ) {
3829          /*
3830           * Admin is ssl and the user pasted non-ssl URL.
3831           * Check if the provider supports ssl embeds and use that for the preview.
3832           */
3833          $ssl_shortcode = preg_replace( '%^(\\[embed[^\\]]*\\])http://%i', '$1https://', $shortcode );
3834          $parsed        = $wp_embed->run_shortcode( $ssl_shortcode );
3835  
3836          if ( ! $parsed ) {
3837              $no_ssl_support = true;
3838          }
3839      }
3840  
3841      // Set $content_width so any embeds fit in the destination iframe.
3842      if ( isset( $_POST['maxwidth'] ) && is_numeric( $_POST['maxwidth'] ) && $_POST['maxwidth'] > 0 ) {
3843          if ( ! isset( $content_width ) ) {
3844              $content_width = (int) $_POST['maxwidth'];
3845          } else {
3846              $content_width = min( $content_width, (int) $_POST['maxwidth'] );
3847          }
3848      }
3849  
3850      if ( $url && ! $parsed ) {
3851          $parsed = $wp_embed->run_shortcode( $shortcode );
3852      }
3853  
3854      if ( ! $parsed ) {
3855          wp_send_json_error(
3856              array(
3857                  'type'    => 'not-embeddable',
3858                  /* translators: %s: URL that could not be embedded. */
3859                  'message' => sprintf( __( '%s failed to embed.' ), '<code>' . esc_html( $url ) . '</code>' ),
3860              )
3861          );
3862      }
3863  
3864      if ( has_shortcode( $parsed, 'audio' ) || has_shortcode( $parsed, 'video' ) ) {
3865          $styles     = '';
3866          $mce_styles = wpview_media_sandbox_styles();
3867  
3868          foreach ( $mce_styles as $style ) {
3869              $styles .= sprintf( '<link rel="stylesheet" href="%s" />', $style );
3870          }
3871  
3872          $html = do_shortcode( $parsed );
3873  
3874          global $wp_scripts;
3875  
3876          if ( ! empty( $wp_scripts ) ) {
3877              $wp_scripts->done = array();
3878          }
3879  
3880          ob_start();
3881          wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
3882          $scripts = ob_get_clean();
3883  
3884          $parsed = $styles . $html . $scripts;
3885      }
3886  
3887      if ( ! empty( $no_ssl_support ) || ( is_ssl() && ( preg_match( '%<(iframe|script|embed) [^>]*src="http://%', $parsed ) ||
3888          preg_match( '%<link [^>]*href="http://%', $parsed ) ) ) ) {
3889          // Admin is ssl and the embed is not. Iframes, scripts, and other "active content" will be blocked.
3890          wp_send_json_error(
3891              array(
3892                  'type'    => 'not-ssl',
3893                  'message' => __( 'This preview is unavailable in the editor.' ),
3894              )
3895          );
3896      }
3897  
3898      $return = array(
3899          'body' => $parsed,
3900          'attr' => $wp_embed->last_attr,
3901      );
3902  
3903      if ( str_contains( $parsed, 'class="wp-embedded-content' ) ) {
3904          if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
3905              $script_src = includes_url( 'js/wp-embed.js' );
3906          } else {
3907              $script_src = includes_url( 'js/wp-embed.min.js' );
3908          }
3909  
3910          $return['head']    = '<script src="' . $script_src . '"></script>';
3911          $return['sandbox'] = true;
3912      }
3913  
3914      wp_send_json_success( $return );
3915  }
3916  
3917  /**
3918   * @since 4.0.0
3919   *
3920   * @global WP_Post    $post       Global post object.
3921   * @global WP_Scripts $wp_scripts Script dependencies object.
3922   */
3923  function wp_ajax_parse_media_shortcode() {
3924      global $post, $wp_scripts;
3925  
3926      if ( empty( $_POST['shortcode'] ) ) {
3927          wp_send_json_error();
3928      }
3929  
3930      $shortcode = wp_unslash( $_POST['shortcode'] );
3931  
3932      // Only process previews for media related shortcodes:
3933      $found_shortcodes = get_shortcode_tags_in_content( $shortcode );
3934      $media_shortcodes = array(
3935          'audio',
3936          'embed',
3937          'playlist',
3938          'video',
3939          'gallery',
3940      );
3941  
3942      $other_shortcodes = array_diff( $found_shortcodes, $media_shortcodes );
3943  
3944      if ( ! empty( $other_shortcodes ) ) {
3945          wp_send_json_error();
3946      }
3947  
3948      if ( ! empty( $_POST['post_ID'] ) ) {
3949          $post = get_post( (int) $_POST['post_ID'] );
3950      }
3951  
3952      // The embed shortcode requires a post.
3953      if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
3954          if ( in_array( 'embed', $found_shortcodes, true ) ) {
3955              wp_send_json_error();
3956          }
3957      } else {
3958          setup_postdata( $post );
3959      }
3960  
3961      $parsed = do_shortcode( $shortcode );
3962  
3963      if ( empty( $parsed ) ) {
3964          wp_send_json_error(
3965              array(
3966                  'type'    => 'no-items',
3967                  'message' => __( 'No items found.' ),
3968              )
3969          );
3970      }
3971  
3972      $head   = '';
3973      $styles = wpview_media_sandbox_styles();
3974  
3975      foreach ( $styles as $style ) {
3976          $head .= '<link rel="stylesheet" href="' . $style . '">';
3977      }
3978  
3979      if ( ! empty( $wp_scripts ) ) {
3980          $wp_scripts->done = array();
3981      }
3982  
3983      ob_start();
3984  
3985      echo $parsed;
3986  
3987      if ( 'playlist' === $_REQUEST['type'] ) {
3988          wp_underscore_playlist_templates();
3989  
3990          wp_print_scripts( 'wp-playlist' );
3991      } else {
3992          wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
3993      }
3994  
3995      wp_send_json_success(
3996          array(
3997              'head' => $head,
3998              'body' => ob_get_clean(),
3999          )
4000      );
4001  }
4002  
4003  /**
4004   * Handles destroying multiple open sessions for a user via AJAX.
4005   *
4006   * @since 4.1.0
4007   */
4008  function wp_ajax_destroy_sessions() {
4009      $user = get_userdata( (int) $_POST['user_id'] );
4010  
4011      if ( $user ) {
4012          if ( ! current_user_can( 'edit_user', $user->ID ) ) {
4013              $user = false;
4014          } elseif ( ! wp_verify_nonce( $_POST['nonce'], 'update-user_' . $user->ID ) ) {
4015              $user = false;
4016          }
4017      }
4018  
4019      if ( ! $user ) {
4020          wp_send_json_error(
4021              array(
4022                  'message' => __( 'Could not log out user sessions. Please try again.' ),
4023              )
4024          );
4025      }
4026  
4027      $sessions = WP_Session_Tokens::get_instance( $user->ID );
4028  
4029      if ( get_current_user_id() === $user->ID ) {
4030          $sessions->destroy_others( wp_get_session_token() );
4031          $message = __( 'You are now logged out everywhere else.' );
4032      } else {
4033          $sessions->destroy_all();
4034          /* translators: %s: User's display name. */
4035          $message = sprintf( __( '%s has been logged out.' ), $user->display_name );
4036      }
4037  
4038      wp_send_json_success( array( 'message' => $message ) );
4039  }
4040  
4041  /**
4042   * Handles cropping an image via AJAX.
4043   *
4044   * @since 4.3.0
4045   */
4046  function wp_ajax_crop_image() {
4047      $attachment_id = absint( $_POST['id'] );
4048  
4049      check_ajax_referer( 'image_editor-' . $attachment_id, 'nonce' );
4050  
4051      if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
4052          wp_send_json_error();
4053      }
4054  
4055      $context = str_replace( '_', '-', $_POST['context'] );
4056      $data    = array_map( 'absint', $_POST['cropDetails'] );
4057      $cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );
4058  
4059      if ( ! $cropped || is_wp_error( $cropped ) ) {
4060          wp_send_json_error( array( 'message' => __( 'Image could not be processed.' ) ) );
4061      }
4062  
4063      switch ( $context ) {
4064          case 'site-icon':
4065              require_once  ABSPATH . 'wp-admin/includes/class-wp-site-icon.php';
4066              $wp_site_icon = new WP_Site_Icon();
4067  
4068              // Skip creating a new attachment if the attachment is a Site Icon.
4069              if ( get_post_meta( $attachment_id, '_wp_attachment_context', true ) === $context ) {
4070  
4071                  // Delete the temporary cropped file, we don't need it.
4072                  wp_delete_file( $cropped );
4073  
4074                  // Additional sizes in wp_prepare_attachment_for_js().
4075                  add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
4076                  break;
4077              }
4078  
4079              /** This filter is documented in wp-admin/includes/class-custom-image-header.php */
4080              $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
4081  
4082              // Copy attachment properties.
4083              $attachment = wp_copy_parent_attachment_properties( $cropped, $attachment_id, $context );
4084  
4085              // Update the attachment.
4086              add_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
4087              $attachment_id = $wp_site_icon->insert_attachment( $attachment, $cropped );
4088              remove_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
4089  
4090              // Additional sizes in wp_prepare_attachment_for_js().
4091              add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
4092              break;
4093  
4094          default:
4095              /**
4096               * Fires before a cropped image is saved.
4097               *
4098               * Allows to add filters to modify the way a cropped image is saved.
4099               *
4100               * @since 4.3.0
4101               *
4102               * @param string $context       The Customizer control requesting the cropped image.
4103               * @param int    $attachment_id The attachment ID of the original image.
4104               * @param string $cropped       Path to the cropped image file.
4105               */
4106              do_action( 'wp_ajax_crop_image_pre_save', $context, $attachment_id, $cropped );
4107  
4108              /** This filter is documented in wp-admin/includes/class-custom-image-header.php */
4109              $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
4110  
4111              // Copy attachment properties.
4112              $attachment = wp_copy_parent_attachment_properties( $cropped, $attachment_id, $context );
4113  
4114              $attachment_id = wp_insert_attachment( $attachment, $cropped );
4115              $metadata      = wp_generate_attachment_metadata( $attachment_id, $cropped );
4116  
4117              /**
4118               * Filters the cropped image attachment metadata.
4119               *
4120               * @since 4.3.0
4121               *
4122               * @see wp_generate_attachment_metadata()
4123               *
4124               * @param array $metadata Attachment metadata.
4125               */
4126              $metadata = apply_filters( 'wp_ajax_cropped_attachment_metadata', $metadata );
4127              wp_update_attachment_metadata( $attachment_id, $metadata );
4128  
4129              /**
4130               * Filters the attachment ID for a cropped image.
4131               *
4132               * @since 4.3.0
4133               *
4134               * @param int    $attachment_id The attachment ID of the cropped image.
4135               * @param string $context       The Customizer control requesting the cropped image.
4136               */
4137              $attachment_id = apply_filters( 'wp_ajax_cropped_attachment_id', $attachment_id, $context );
4138      }
4139  
4140      wp_send_json_success( wp_prepare_attachment_for_js( $attachment_id ) );
4141  }
4142  
4143  /**
4144   * Handles generating a password via AJAX.
4145   *
4146   * @since 4.4.0
4147   */
4148  function wp_ajax_generate_password() {
4149      wp_send_json_success( wp_generate_password( 24 ) );
4150  }
4151  
4152  /**
4153   * Handles generating a password in the no-privilege context via AJAX.
4154   *
4155   * @since 5.7.0
4156   */
4157  function wp_ajax_nopriv_generate_password() {
4158      wp_send_json_success( wp_generate_password( 24 ) );
4159  }
4160  
4161  /**
4162   * Handles saving the user's WordPress.org username via AJAX.
4163   *
4164   * @since 4.4.0
4165   */
4166  function wp_ajax_save_wporg_username() {
4167      if ( ! current_user_can( 'install_themes' ) && ! current_user_can( 'install_plugins' ) ) {
4168          wp_send_json_error();
4169      }
4170  
4171      check_ajax_referer( 'save_wporg_username_' . get_current_user_id() );
4172  
4173      $username = isset( $_REQUEST['username'] ) ? wp_unslash( $_REQUEST['username'] ) : false;
4174  
4175      if ( ! $username ) {
4176          wp_send_json_error();
4177      }
4178  
4179      wp_send_json_success( update_user_meta( get_current_user_id(), 'wporg_favorites', $username ) );
4180  }
4181  
4182  /**
4183   * Handles installing a theme via AJAX.
4184   *
4185   * @since 4.6.0
4186   *
4187   * @see Theme_Upgrader
4188   *
4189   * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4190   */
4191  function wp_ajax_install_theme() {
4192      check_ajax_referer( 'updates' );
4193  
4194      if ( empty( $_POST['slug'] ) ) {
4195          wp_send_json_error(
4196              array(
4197                  'slug'         => '',
4198                  'errorCode'    => 'no_theme_specified',
4199                  'errorMessage' => __( 'No theme specified.' ),
4200              )
4201          );
4202      }
4203  
4204      $slug = sanitize_key( wp_unslash( $_POST['slug'] ) );
4205  
4206      $status = array(
4207          'install' => 'theme',
4208          'slug'    => $slug,
4209      );
4210  
4211      if ( ! current_user_can( 'install_themes' ) ) {
4212          $status['errorMessage'] = __( 'Sorry, you are not allowed to install themes on this site.' );
4213          wp_send_json_error( $status );
4214      }
4215  
4216      require_once  ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4217      require_once  ABSPATH . 'wp-admin/includes/theme.php';
4218  
4219      $api = themes_api(
4220          'theme_information',
4221          array(
4222              'slug'   => $slug,
4223              'fields' => array( 'sections' => false ),
4224          )
4225      );
4226  
4227      if ( is_wp_error( $api ) ) {
4228          $status['errorMessage'] = $api->get_error_message();
4229          wp_send_json_error( $status );
4230      }
4231  
4232      $skin     = new WP_Ajax_Upgrader_Skin();
4233      $upgrader = new Theme_Upgrader( $skin );
4234      $result   = $upgrader->install( $api->download_link );
4235  
4236      if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4237          $status['debug'] = $skin->get_upgrade_messages();
4238      }
4239  
4240      if ( is_wp_error( $result ) ) {
4241          $status['errorCode']    = $result->get_error_code();
4242          $status['errorMessage'] = $result->get_error_message();
4243          wp_send_json_error( $status );
4244      } elseif ( is_wp_error( $skin->result ) ) {
4245          $status['errorCode']    = $skin->result->get_error_code();
4246          $status['errorMessage'] = $skin->result->get_error_message();
4247          wp_send_json_error( $status );
4248      } elseif ( $skin->get_errors()->has_errors() ) {
4249          $status['errorMessage'] = $skin->get_error_messages();
4250          wp_send_json_error( $status );
4251      } elseif ( is_null( $result ) ) {
4252          global $wp_filesystem;
4253  
4254          $status['errorCode']    = 'unable_to_connect_to_filesystem';
4255          $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4256  
4257          // Pass through the error from WP_Filesystem if one was raised.
4258          if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4259              $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4260          }
4261  
4262          wp_send_json_error( $status );
4263      }
4264  
4265      $status['themeName'] = wp_get_theme( $slug )->get( 'Name' );
4266  
4267      if ( current_user_can( 'switch_themes' ) ) {
4268          if ( is_multisite() ) {
4269              $status['activateUrl'] = add_query_arg(
4270                  array(
4271                      'action'   => 'enable',
4272                      '_wpnonce' => wp_create_nonce( 'enable-theme_' . $slug ),
4273                      'theme'    => $slug,
4274                  ),
4275                  network_admin_url( 'themes.php' )
4276              );
4277          } else {
4278              $status['activateUrl'] = add_query_arg(
4279                  array(
4280                      'action'     => 'activate',
4281                      '_wpnonce'   => wp_create_nonce( 'switch-theme_' . $slug ),
4282                      'stylesheet' => $slug,
4283                  ),
4284                  admin_url( 'themes.php' )
4285              );
4286          }
4287      }
4288  
4289      $theme                = wp_get_theme( $slug );
4290      $status['blockTheme'] = $theme->is_block_theme();
4291  
4292      if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
4293          $status['customizeUrl'] = add_query_arg(
4294              array(
4295                  'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
4296              ),
4297              wp_customize_url( $slug )
4298          );
4299      }
4300  
4301      /*
4302       * See WP_Theme_Install_List_Table::_get_theme_status() if we wanted to check
4303       * on post-installation status.
4304       */
4305      wp_send_json_success( $status );
4306  }
4307  
4308  /**
4309   * Handles updating a theme via AJAX.
4310   *
4311   * @since 4.6.0
4312   *
4313   * @see Theme_Upgrader
4314   *
4315   * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4316   */
4317  function wp_ajax_update_theme() {
4318      check_ajax_referer( 'updates' );
4319  
4320      if ( empty( $_POST['slug'] ) ) {
4321          wp_send_json_error(
4322              array(
4323                  'slug'         => '',
4324                  'errorCode'    => 'no_theme_specified',
4325                  'errorMessage' => __( 'No theme specified.' ),
4326              )
4327          );
4328      }
4329  
4330      $stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) );
4331      $status     = array(
4332          'update'     => 'theme',
4333          'slug'       => $stylesheet,
4334          'oldVersion' => '',
4335          'newVersion' => '',
4336      );
4337  
4338      if ( ! current_user_can( 'update_themes' ) ) {
4339          $status['errorMessage'] = __( 'Sorry, you are not allowed to update themes for this site.' );
4340          wp_send_json_error( $status );
4341      }
4342  
4343      $theme = wp_get_theme( $stylesheet );
4344      if ( $theme->exists() ) {
4345          $status['oldVersion'] = $theme->get( 'Version' );
4346      }
4347  
4348      require_once  ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4349  
4350      $current = get_site_transient( 'update_themes' );
4351      if ( empty( $current ) ) {
4352          wp_update_themes();
4353      }
4354  
4355      $skin     = new WP_Ajax_Upgrader_Skin();
4356      $upgrader = new Theme_Upgrader( $skin );
4357      $result   = $upgrader->bulk_upgrade( array( $stylesheet ) );
4358  
4359      if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4360          $status['debug'] = $skin->get_upgrade_messages();
4361      }
4362  
4363      if ( is_wp_error( $skin->result ) ) {
4364          $status['errorCode']    = $skin->result->get_error_code();
4365          $status['errorMessage'] = $skin->result->get_error_message();
4366          wp_send_json_error( $status );
4367      } elseif ( $skin->get_errors()->has_errors() ) {
4368          $status['errorMessage'] = $skin->get_error_messages();
4369          wp_send_json_error( $status );
4370      } elseif ( is_array( $result ) && ! empty( $result[ $stylesheet ] ) ) {
4371  
4372          // Theme is already at the latest version.
4373          if ( true === $result[ $stylesheet ] ) {
4374              $status['errorMessage'] = $upgrader->strings['up_to_date'];
4375              wp_send_json_error( $status );
4376          }
4377  
4378          $theme = wp_get_theme( $stylesheet );
4379          if ( $theme->exists() ) {
4380              $status['newVersion'] = $theme->get( 'Version' );
4381          }
4382  
4383          wp_send_json_success( $status );
4384      } elseif ( false === $result ) {
4385          global $wp_filesystem;
4386  
4387          $status['errorCode']    = 'unable_to_connect_to_filesystem';
4388          $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4389  
4390          // Pass through the error from WP_Filesystem if one was raised.
4391          if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4392              $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4393          }
4394  
4395          wp_send_json_error( $status );
4396      }
4397  
4398      // An unhandled error occurred.
4399      $status['errorMessage'] = __( 'Theme update failed.' );
4400      wp_send_json_error( $status );
4401  }
4402  
4403  /**
4404   * Handles deleting a theme via AJAX.
4405   *
4406   * @since 4.6.0
4407   *
4408   * @see delete_theme()
4409   *
4410   * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4411   */
4412  function wp_ajax_delete_theme() {
4413      check_ajax_referer( 'updates' );
4414  
4415      if ( empty( $_POST['slug'] ) ) {
4416          wp_send_json_error(
4417              array(
4418                  'slug'         => '',
4419                  'errorCode'    => 'no_theme_specified',
4420                  'errorMessage' => __( 'No theme specified.' ),
4421              )
4422          );
4423      }
4424  
4425      $stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) );
4426      $status     = array(
4427          'delete' => 'theme',
4428          'slug'   => $stylesheet,
4429      );
4430  
4431      if ( ! current_user_can( 'delete_themes' ) ) {
4432          $status['errorMessage'] = __( 'Sorry, you are not allowed to delete themes on this site.' );
4433          wp_send_json_error( $status );
4434      }
4435  
4436      if ( ! wp_get_theme( $stylesheet )->exists() ) {
4437          $status['errorMessage'] = __( 'The requested theme does not exist.' );
4438          wp_send_json_error( $status );
4439      }
4440  
4441      // Check filesystem credentials. `delete_theme()` will bail otherwise.
4442      $url = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );
4443  
4444      ob_start();
4445      $credentials = request_filesystem_credentials( $url );
4446      ob_end_clean();
4447  
4448      if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
4449          global $wp_filesystem;
4450  
4451          $status['errorCode']    = 'unable_to_connect_to_filesystem';
4452          $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4453  
4454          // Pass through the error from WP_Filesystem if one was raised.
4455          if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4456              $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4457          }
4458  
4459          wp_send_json_error( $status );
4460      }
4461  
4462      require_once  ABSPATH . 'wp-admin/includes/theme.php';
4463  
4464      $result = delete_theme( $stylesheet );
4465  
4466      if ( is_wp_error( $result ) ) {
4467          $status['errorMessage'] = $result->get_error_message();
4468          wp_send_json_error( $status );
4469      } elseif ( false === $result ) {
4470          $status['errorMessage'] = __( 'Theme could not be deleted.' );
4471          wp_send_json_error( $status );
4472      }
4473  
4474      wp_send_json_success( $status );
4475  }
4476  
4477  /**
4478   * Handles installing a plugin via AJAX.
4479   *
4480   * @since 4.6.0
4481   *
4482   * @see Plugin_Upgrader
4483   *
4484   * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4485   */
4486  function wp_ajax_install_plugin() {
4487      check_ajax_referer( 'updates' );
4488  
4489      if ( empty( $_POST['slug'] ) ) {
4490          wp_send_json_error(
4491              array(
4492                  'slug'         => '',
4493                  'errorCode'    => 'no_plugin_specified',
4494                  'errorMessage' => __( 'No plugin specified.' ),
4495              )
4496          );
4497      }
4498  
4499      $status = array(
4500          'install' => 'plugin',
4501          'slug'    => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4502      );
4503  
4504      if ( ! current_user_can( 'install_plugins' ) ) {
4505          $status['errorMessage'] = __( 'Sorry, you are not allowed to install plugins on this site.' );
4506          wp_send_json_error( $status );
4507      }
4508  
4509      require_once  ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4510      require_once  ABSPATH . 'wp-admin/includes/plugin-install.php';
4511  
4512      $api = plugins_api(
4513          'plugin_information',
4514          array(
4515              'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4516              'fields' => array(
4517                  'sections' => false,
4518              ),
4519          )
4520      );
4521  
4522      if ( is_wp_error( $api ) ) {
4523          $status['errorMessage'] = $api->get_error_message();
4524          wp_send_json_error( $status );
4525      }
4526  
4527      $status['pluginName'] = $api->name;
4528  
4529      $skin     = new WP_Ajax_Upgrader_Skin();
4530      $upgrader = new Plugin_Upgrader( $skin );
4531      $result   = $upgrader->install( $api->download_link );
4532  
4533      if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4534          $status['debug'] = $skin->get_upgrade_messages();
4535      }
4536  
4537      if ( is_wp_error( $result ) ) {
4538          $status['errorCode']    = $result->get_error_code();
4539          $status['errorMessage'] = $result->get_error_message();
4540          wp_send_json_error( $status );
4541      } elseif ( is_wp_error( $skin->result ) ) {
4542          $status['errorCode']    = $skin->result->get_error_code();
4543          $status['errorMessage'] = $skin->result->get_error_message();
4544          wp_send_json_error( $status );
4545      } elseif ( $skin->get_errors()->has_errors() ) {
4546          $status['errorMessage'] = $skin->get_error_messages();
4547          wp_send_json_error( $status );
4548      } elseif ( is_null( $result ) ) {
4549          global $wp_filesystem;
4550  
4551          $status['errorCode']    = 'unable_to_connect_to_filesystem';
4552          $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4553  
4554          // Pass through the error from WP_Filesystem if one was raised.
4555          if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4556              $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4557          }
4558  
4559          wp_send_json_error( $status );
4560      }
4561  
4562      $install_status = install_plugin_install_status( $api );
4563      $pagenow        = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
4564  
4565      // If installation request is coming from import page, do not return network activation link.
4566      $plugins_url = ( 'import' === $pagenow ) ? admin_url( 'plugins.php' ) : network_admin_url( 'plugins.php' );
4567  
4568      if ( current_user_can( 'activate_plugin', $install_status['file'] ) && is_plugin_inactive( $install_status['file'] ) ) {
4569          $status['activateUrl'] = add_query_arg(
4570              array(
4571                  '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $install_status['file'] ),
4572                  'action'   => 'activate',
4573                  'plugin'   => $install_status['file'],
4574              ),
4575              $plugins_url
4576          );
4577      }
4578  
4579      if ( is_multisite() && current_user_can( 'manage_network_plugins' ) && 'import' !== $pagenow ) {
4580          $status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] );
4581      }
4582  
4583      wp_send_json_success( $status );
4584  }
4585  
4586  /**
4587   * Handles activating a plugin via AJAX.
4588   *
4589   * @since 6.5.0
4590   */
4591  function wp_ajax_activate_plugin() {
4592      check_ajax_referer( 'updates' );
4593  
4594      if ( empty( $_POST['name'] ) || empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
4595          wp_send_json_error(
4596              array(
4597                  'slug'         => '',
4598                  'pluginName'   => '',
4599                  'plugin'       => '',
4600                  'errorCode'    => 'no_plugin_specified',
4601                  'errorMessage' => __( 'No plugin specified.' ),
4602              )
4603          );
4604      }
4605  
4606      $status = array(
4607          'activate'   => 'plugin',
4608          'slug'       => wp_unslash( $_POST['slug'] ),
4609          'pluginName' => wp_unslash( $_POST['name'] ),
4610          'plugin'     => wp_unslash( $_POST['plugin'] ),
4611      );
4612  
4613      if ( ! current_user_can( 'activate_plugin', $status['plugin'] ) ) {
4614          $status['errorMessage'] = __( 'Sorry, you are not allowed to activate plugins on this site.' );
4615          wp_send_json_error( $status );
4616      }
4617  
4618      if ( is_plugin_active( $status['plugin'] ) ) {
4619          $status['errorMessage'] = sprintf(
4620              /* translators: %s: Plugin name. */
4621              __( '%s is already active.' ),
4622              $status['pluginName']
4623          );
4624      }
4625  
4626      $activated = activate_plugin( $status['plugin'] );
4627  
4628      if ( is_wp_error( $activated ) ) {
4629          $status['errorMessage'] = $activated->get_error_message();
4630          wp_send_json_error( $status );
4631      }
4632  
4633      wp_send_json_success( $status );
4634  }
4635  
4636  /**
4637   * Handles updating a plugin via AJAX.
4638   *
4639   * @since 4.2.0
4640   *
4641   * @see Plugin_Upgrader
4642   *
4643   * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4644   */
4645  function wp_ajax_update_plugin() {
4646      check_ajax_referer( 'updates' );
4647  
4648      if ( empty( $_POST['plugin'] ) || empty( $_POST['slug'] ) ) {
4649          wp_send_json_error(
4650              array(
4651                  'slug'         => '',
4652                  'errorCode'    => 'no_plugin_specified',
4653                  'errorMessage' => __( 'No plugin specified.' ),
4654              )
4655          );
4656      }
4657  
4658      $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );
4659  
4660      $status = array(
4661          'update'     => 'plugin',
4662          'slug'       => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4663          'oldVersion' => '',
4664          'newVersion' => '',
4665      );
4666  
4667      if ( ! current_user_can( 'update_plugins' ) || 0 !== validate_file( $plugin ) ) {
4668          $status['errorMessage'] = __( 'Sorry, you are not allowed to update plugins for this site.' );
4669          wp_send_json_error( $status );
4670      }
4671  
4672      $plugin_data          = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
4673      $status['plugin']     = $plugin;
4674      $status['pluginName'] = $plugin_data['Name'];
4675  
4676      if ( $plugin_data['Version'] ) {
4677          /* translators: %s: Plugin version. */
4678          $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
4679      }
4680  
4681      require_once  ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4682  
4683      wp_update_plugins();
4684  
4685      $skin     = new WP_Ajax_Upgrader_Skin();
4686      $upgrader = new Plugin_Upgrader( $skin );
4687      $result   = $upgrader->bulk_upgrade( array( $plugin ) );
4688  
4689      if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4690          $status['debug'] = $skin->get_upgrade_messages();
4691      }
4692  
4693      if ( is_wp_error( $skin->result ) ) {
4694          $status['errorCode']    = $skin->result->get_error_code();
4695          $status['errorMessage'] = $skin->result->get_error_message();
4696          wp_send_json_error( $status );
4697      } elseif ( $skin->get_errors()->has_errors() ) {
4698          $status['errorMessage'] = $skin->get_error_messages();
4699          wp_send_json_error( $status );
4700      } elseif ( is_array( $result ) && ! empty( $result[ $plugin ] ) ) {
4701  
4702          /*
4703           * Plugin is already at the latest version.
4704           *
4705           * This may also be the return value if the `update_plugins` site transient is empty,
4706           * e.g. when you update two plugins in quick succession before the transient repopulates.
4707           *
4708           * Preferably something can be done to ensure `update_plugins` isn't empty.
4709           * For now, surface some sort of error here.
4710           */
4711          if ( true === $result[ $plugin ] ) {
4712              $status['errorMessage'] = $upgrader->strings['up_to_date'];
4713              wp_send_json_error( $status );
4714          }
4715  
4716          $plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] );
4717          $plugin_data = reset( $plugin_data );
4718  
4719          if ( $plugin_data['Version'] ) {
4720              /* translators: %s: Plugin version. */
4721              $status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
4722          }
4723  
4724          wp_send_json_success( $status );
4725      } elseif ( false === $result ) {
4726          global $wp_filesystem;
4727  
4728          $status['errorCode']    = 'unable_to_connect_to_filesystem';
4729          $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4730  
4731          // Pass through the error from WP_Filesystem if one was raised.
4732          if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4733              $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4734          }
4735  
4736          wp_send_json_error( $status );
4737      }
4738  
4739      // An unhandled error occurred.
4740      $status['errorMessage'] = __( 'Plugin update failed.' );
4741      wp_send_json_error( $status );
4742  }
4743  
4744  /**
4745   * Handles deleting a plugin via AJAX.
4746   *
4747   * @since 4.6.0
4748   *
4749   * @see delete_plugins()
4750   *
4751   * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4752   */
4753  function wp_ajax_delete_plugin() {
4754      check_ajax_referer( 'updates' );
4755  
4756      if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
4757          wp_send_json_error(
4758              array(
4759                  'slug'         => '',
4760                  'errorCode'    => 'no_plugin_specified',
4761                  'errorMessage' => __( 'No plugin specified.' ),
4762              )
4763          );
4764      }
4765  
4766      $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );
4767  
4768      $status = array(
4769          'delete' => 'plugin',
4770          'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4771      );
4772  
4773      if ( ! current_user_can( 'delete_plugins' ) || 0 !== validate_file( $plugin ) ) {
4774          $status['errorMessage'] = __( 'Sorry, you are not allowed to delete plugins for this site.' );
4775          wp_send_json_error( $status );
4776      }
4777  
4778      $plugin_data          = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
4779      $status['plugin']     = $plugin;
4780      $status['pluginName'] = $plugin_data['Name'];
4781  
4782      if ( is_plugin_active( $plugin ) ) {
4783          $status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' );
4784          wp_send_json_error( $status );
4785      }
4786  
4787      // Check filesystem credentials. `delete_plugins()` will bail otherwise.
4788      $url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&checked[]=' . $plugin, 'bulk-plugins' );
4789  
4790      ob_start();
4791      $credentials = request_filesystem_credentials( $url );
4792      ob_end_clean();
4793  
4794      if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
4795          global $wp_filesystem;
4796  
4797          $status['errorCode']    = 'unable_to_connect_to_filesystem';
4798          $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4799  
4800          // Pass through the error from WP_Filesystem if one was raised.
4801          if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4802              $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4803          }
4804  
4805          wp_send_json_error( $status );
4806      }
4807  
4808      $result = delete_plugins( array( $plugin ) );
4809  
4810      if ( is_wp_error( $result ) ) {
4811          $status['errorMessage'] = $result->get_error_message();
4812          wp_send_json_error( $status );
4813      } elseif ( false === $result ) {
4814          $status['errorMessage'] = __( 'Plugin could not be deleted.' );
4815          wp_send_json_error( $status );
4816      }
4817  
4818      wp_send_json_success( $status );
4819  }
4820  
4821  /**
4822   * Handles searching plugins via AJAX.
4823   *
4824   * @since 4.6.0
4825   *
4826   * @global string $s Search term.
4827   */
4828  function wp_ajax_search_plugins() {
4829      check_ajax_referer( 'updates' );
4830  
4831      // Ensure after_plugin_row_{$plugin_file} gets hooked.
4832      wp_plugin_update_rows();
4833  
4834      WP_Plugin_Dependencies::initialize();
4835  
4836      $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
4837      if ( 'plugins-network' === $pagenow || 'plugins' === $pagenow ) {
4838          set_current_screen( $pagenow );
4839      }
4840  
4841      /** @var WP_Plugins_List_Table $wp_list_table */
4842      $wp_list_table = _get_list_table(
4843          'WP_Plugins_List_Table',
4844          array(
4845              'screen' => get_current_screen(),
4846          )
4847      );
4848  
4849      $status = array();
4850  
4851      if ( ! $wp_list_table->ajax_user_can() ) {
4852          $status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' );
4853          wp_send_json_error( $status );
4854      }
4855  
4856      // Set the correct requester, so pagination works.
4857      $_SERVER['REQUEST_URI'] = add_query_arg(
4858          array_diff_key(
4859              $_POST,
4860              array(
4861                  '_ajax_nonce' => null,
4862                  'action'      => null,
4863              )
4864          ),
4865          network_admin_url( 'plugins.php', 'relative' )
4866      );
4867  
4868      $GLOBALS['s'] = wp_unslash( $_POST['s'] );
4869  
4870      $wp_list_table->prepare_items();
4871  
4872      ob_start();
4873      $wp_list_table->display();
4874      $status['count'] = count( $wp_list_table->items );
4875      $status['items'] = ob_get_clean();
4876  
4877      wp_send_json_success( $status );
4878  }
4879  
4880  /**
4881   * Handles searching plugins to install via AJAX.
4882   *
4883   * @since 4.6.0
4884   */
4885  function wp_ajax_search_install_plugins() {
4886      check_ajax_referer( 'updates' );
4887  
4888      $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
4889      if ( 'plugin-install-network' === $pagenow || 'plugin-install' === $pagenow ) {
4890          set_current_screen( $pagenow );
4891      }
4892  
4893      /** @var WP_Plugin_Install_List_Table $wp_list_table */
4894      $wp_list_table = _get_list_table(
4895          'WP_Plugin_Install_List_Table',
4896          array(
4897              'screen' => get_current_screen(),
4898          )
4899      );
4900  
4901      $status = array();
4902  
4903      if ( ! $wp_list_table->ajax_user_can() ) {
4904          $status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' );
4905          wp_send_json_error( $status );
4906      }
4907  
4908      // Set the correct requester, so pagination works.
4909      $_SERVER['REQUEST_URI'] = add_query_arg(
4910          array_diff_key(
4911              $_POST,
4912              array(
4913                  '_ajax_nonce' => null,
4914                  'action'      => null,
4915              )
4916          ),
4917          network_admin_url( 'plugin-install.php', 'relative' )
4918      );
4919  
4920      $wp_list_table->prepare_items();
4921  
4922      ob_start();
4923      $wp_list_table->display();
4924      $status['count'] = (int) $wp_list_table->get_pagination_arg( 'total_items' );
4925      $status['items'] = ob_get_clean();
4926  
4927      wp_send_json_success( $status );
4928  }
4929  
4930  /**
4931   * Handles editing a theme or plugin file via AJAX.
4932   *
4933   * @since 4.9.0
4934   *
4935   * @see wp_edit_theme_plugin_file()
4936   */
4937  function wp_ajax_edit_theme_plugin_file() {
4938      $edit_result = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file().
4939  
4940      if ( is_wp_error( $edit_result ) ) {
4941          wp_send_json_error(
4942              array_merge(
4943                  array(
4944                      'code'    => $edit_result->get_error_code(),
4945                      'message' => $edit_result->get_error_message(),
4946                  ),
4947                  (array) $edit_result->get_error_data()
4948              )
4949          );
4950      } else {
4951          wp_send_json_success(
4952              array(
4953                  'message' => __( 'File edited successfully.' ),
4954              )
4955          );
4956      }
4957  }
4958  
4959  /**
4960   * Handles exporting a user's personal data via AJAX.
4961   *
4962   * @since 4.9.6
4963   */
4964  function wp_ajax_wp_privacy_export_personal_data() {
4965  
4966      if ( empty( $_POST['id'] ) ) {
4967          wp_send_json_error( __( 'Missing request ID.' ) );
4968      }
4969  
4970      $request_id = (int) $_POST['id'];
4971  
4972      if ( $request_id < 1 ) {
4973          wp_send_json_error( __( 'Invalid request ID.' ) );
4974      }
4975  
4976      if ( ! current_user_can( 'export_others_personal_data' ) ) {
4977          wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
4978      }
4979  
4980      check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' );
4981  
4982      // Get the request.
4983      $request = wp_get_user_request( $request_id );
4984  
4985      if ( ! $request || 'export_personal_data' !== $request->action_name ) {
4986          wp_send_json_error( __( 'Invalid request type.' ) );
4987      }
4988  
4989      $email_address = $request->email;
4990      if ( ! is_email( $email_address ) ) {
4991          wp_send_json_error( __( 'A valid email address must be given.' ) );
4992      }
4993  
4994      if ( ! isset( $_POST['exporter'] ) ) {
4995          wp_send_json_error( __( 'Missing exporter index.' ) );
4996      }
4997  
4998      $exporter_index = (int) $_POST['exporter'];
4999  
5000      if ( ! isset( $_POST['page'] ) ) {
5001          wp_send_json_error( __( 'Missing page index.' ) );
5002      }
5003  
5004      $page = (int) $_POST['page'];
5005  
5006      $send_as_email = isset( $_POST['sendAsEmail'] ) ? ( 'true' === $_POST['sendAsEmail'] ) : false;
5007  
5008      /**
5009       * Filters the array of exporter callbacks.
5010       *
5011       * @since 4.9.6
5012       *
5013       * @param array $args {
5014       *     An array of callable exporters of personal data. Default empty array.
5015       *
5016       *     @type array ...$0 {
5017       *         Array of personal data exporters.
5018       *
5019       *         @type callable $callback               Callable exporter function that accepts an
5020       *                                                email address and a page number and returns an
5021       *                                                array of name => value pairs of personal data.
5022       *         @type string   $exporter_friendly_name Translated user facing friendly name for the
5023       *                                                exporter.
5024       *     }
5025       * }
5026       */
5027      $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
5028  
5029      if ( ! is_array( $exporters ) ) {
5030          wp_send_json_error( __( 'An exporter has improperly used the registration filter.' ) );
5031      }
5032  
5033      // Do we have any registered exporters?
5034      if ( 0 < count( $exporters ) ) {
5035          if ( $exporter_index < 1 ) {
5036              wp_send_json_error( __( 'Exporter index cannot be negative.' ) );
5037          }
5038  
5039          if ( $exporter_index > count( $exporters ) ) {
5040              wp_send_json_error( __( 'Exporter index is out of range.' ) );
5041          }
5042  
5043          if ( $page < 1 ) {
5044              wp_send_json_error( __( 'Page index cannot be less than one.' ) );
5045          }
5046  
5047          $exporter_keys = array_keys( $exporters );
5048          $exporter_key  = $exporter_keys[ $exporter_index - 1 ];
5049          $exporter      = $exporters[ $exporter_key ];
5050  
5051          if ( ! is_array( $exporter ) ) {
5052              wp_send_json_error(
5053                  /* translators: %s: Exporter array index. */
5054                  sprintf( __( 'Expected an array describing the exporter at index %s.' ), $exporter_key )
5055              );
5056          }
5057  
5058          if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
5059              wp_send_json_error(
5060                  /* translators: %s: Exporter array index. */
5061                  sprintf( __( 'Exporter array at index %s does not include a friendly name.' ), $exporter_key )
5062              );
5063          }
5064  
5065          $exporter_friendly_name = $exporter['exporter_friendly_name'];
5066  
5067          if ( ! array_key_exists( 'callback', $exporter ) ) {
5068              wp_send_json_error(
5069                  /* translators: %s: Exporter friendly name. */
5070                  sprintf( __( 'Exporter does not include a callback: %s.' ), esc_html( $exporter_friendly_name ) )
5071              );
5072          }
5073  
5074          if ( ! is_callable( $exporter['callback'] ) ) {
5075              wp_send_json_error(
5076                  /* translators: %s: Exporter friendly name. */
5077                  sprintf( __( 'Exporter callback is not a valid callback: %s.' ), esc_html( $exporter_friendly_name ) )
5078              );
5079          }
5080  
5081          $callback = $exporter['callback'];
5082          $response = call_user_func( $callback, $email_address, $page );
5083  
5084          if ( is_wp_error( $response ) ) {
5085              wp_send_json_error( $response );
5086          }
5087  
5088          if ( ! is_array( $response ) ) {
5089              wp_send_json_error(
5090                  /* translators: %s: Exporter friendly name. */
5091                  sprintf( __( 'Expected response as an array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
5092              );
5093          }
5094  
5095          if ( ! array_key_exists( 'data', $response ) ) {
5096              wp_send_json_error(
5097                  /* translators: %s: Exporter friendly name. */
5098                  sprintf( __( 'Expected data in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
5099              );
5100          }
5101  
5102          if ( ! is_array( $response['data'] ) ) {
5103              wp_send_json_error(
5104                  /* translators: %s: Exporter friendly name. */
5105                  sprintf( __( 'Expected data array in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
5106              );
5107          }
5108  
5109          if ( ! array_key_exists( 'done', $response ) ) {
5110              wp_send_json_error(
5111                  /* translators: %s: Exporter friendly name. */
5112                  sprintf( __( 'Expected done (boolean) in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
5113              );
5114          }
5115      } else {
5116          // No exporters, so we're done.
5117          $exporter_key = '';
5118  
5119          $response = array(
5120              'data' => array(),
5121              'done' => true,
5122          );
5123      }
5124  
5125      /**
5126       * Filters a page of personal data exporter data. Used to build the export report.
5127       *
5128       * Allows the export response to be consumed by destinations in addition to Ajax.
5129       *
5130       * @since 4.9.6
5131       *
5132       * @param array  $response        The personal data for the given exporter and page number.
5133       * @param int    $exporter_index  The index of the exporter that provided this data.
5134       * @param string $email_address   The email address associated with this personal data.
5135       * @param int    $page            The page number for this response.
5136       * @param int    $request_id      The privacy request post ID associated with this request.
5137       * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
5138       * @param string $exporter_key    The key (slug) of the exporter that provided this data.
5139       */
5140      $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key );
5141  
5142      if ( is_wp_error( $response ) ) {
5143          wp_send_json_error( $response );
5144      }
5145  
5146      wp_send_json_success( $response );
5147  }
5148  
5149  /**
5150   * Handles erasing personal data via AJAX.
5151   *
5152   * @since 4.9.6
5153   */
5154  function wp_ajax_wp_privacy_erase_personal_data() {
5155  
5156      if ( empty( $_POST['id'] ) ) {
5157          wp_send_json_error( __( 'Missing request ID.' ) );
5158      }
5159  
5160      $request_id = (int) $_POST['id'];
5161  
5162      if ( $request_id < 1 ) {
5163          wp_send_json_error( __( 'Invalid request ID.' ) );
5164      }
5165  
5166      // Both capabilities are required to avoid confusion, see `_wp_personal_data_removal_page()`.
5167      if ( ! current_user_can( 'erase_others_personal_data' ) || ! current_user_can( 'delete_users' ) ) {
5168          wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
5169      }
5170  
5171      check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' );
5172  
5173      // Get the request.
5174      $request = wp_get_user_request( $request_id );
5175  
5176      if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
5177          wp_send_json_error( __( 'Invalid request type.' ) );
5178      }
5179  
5180      $email_address = $request->email;
5181  
5182      if ( ! is_email( $email_address ) ) {
5183          wp_send_json_error( __( 'Invalid email address in request.' ) );
5184      }
5185  
5186      if ( ! isset( $_POST['eraser'] ) ) {
5187          wp_send_json_error( __( 'Missing eraser index.' ) );
5188      }
5189  
5190      $eraser_index = (int) $_POST['eraser'];
5191  
5192      if ( ! isset( $_POST['page'] ) ) {
5193          wp_send_json_error( __( 'Missing page index.' ) );
5194      }
5195  
5196      $page = (int) $_POST['page'];
5197  
5198      /**
5199       * Filters the array of personal data eraser callbacks.
5200       *
5201       * @since 4.9.6
5202       *
5203       * @param array $args {
5204       *     An array of callable erasers of personal data. Default empty array.
5205       *
5206       *     @type array ...$0 {
5207       *         Array of personal data exporters.
5208       *
5209       *         @type callable $callback               Callable eraser that accepts an email address and a page
5210       *                                                number, and returns an array with boolean values for
5211       *                                                whether items were removed or retained and any messages
5212       *                                                from the eraser, as well as if additional pages are
5213       *                                                available.
5214       *         @type string   $exporter_friendly_name Translated user facing friendly name for the eraser.
5215       *     }
5216       * }
5217       */
5218      $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() );
5219  
5220      // Do we have any registered erasers?
5221      if ( 0 < count( $erasers ) ) {
5222  
5223          if ( $eraser_index < 1 ) {
5224              wp_send_json_error( __( 'Eraser index cannot be less than one.' ) );
5225          }
5226  
5227          if ( $eraser_index > count( $erasers ) ) {
5228              wp_send_json_error( __( 'Eraser index is out of range.' ) );
5229          }
5230  
5231          if ( $page < 1 ) {
5232              wp_send_json_error( __( 'Page index cannot be less than one.' ) );
5233          }
5234  
5235          $eraser_keys = array_keys( $erasers );
5236          $eraser_key  = $eraser_keys[ $eraser_index - 1 ];
5237          $eraser      = $erasers[ $eraser_key ];
5238  
5239          if ( ! is_array( $eraser ) ) {
5240              /* translators: %d: Eraser array index. */
5241              wp_send_json_error( sprintf( __( 'Expected an array describing the eraser at index %d.' ), $eraser_index ) );
5242          }
5243  
5244          if ( ! array_key_exists( 'eraser_friendly_name', $eraser ) ) {
5245              /* translators: %d: Eraser array index. */
5246              wp_send_json_error( sprintf( __( 'Eraser array at index %d does not include a friendly name.' ), $eraser_index ) );
5247          }
5248  
5249          $eraser_friendly_name = $eraser['eraser_friendly_name'];
5250  
5251          if ( ! array_key_exists( 'callback', $eraser ) ) {
5252              wp_send_json_error(
5253                  sprintf(
5254                      /* translators: %s: Eraser friendly name. */
5255                      __( 'Eraser does not include a callback: %s.' ),
5256                      esc_html( $eraser_friendly_name )
5257                  )
5258              );
5259          }
5260  
5261          if ( ! is_callable( $eraser['callback'] ) ) {
5262              wp_send_json_error(
5263                  sprintf(
5264                      /* translators: %s: Eraser friendly name. */
5265                      __( 'Eraser callback is not valid: %s.' ),
5266                      esc_html( $eraser_friendly_name )
5267                  )
5268              );
5269          }
5270  
5271          $callback = $eraser['callback'];
5272          $response = call_user_func( $callback, $email_address, $page );
5273  
5274          if ( is_wp_error( $response ) ) {
5275              wp_send_json_error( $response );
5276          }
5277  
5278          if ( ! is_array( $response ) ) {
5279              wp_send_json_error(
5280                  sprintf(
5281                      /* translators: 1: Eraser friendly name, 2: Eraser array index. */
5282                      __( 'Did not receive array from %1$s eraser (index %2$d).' ),
5283                      esc_html( $eraser_friendly_name ),
5284                      $eraser_index
5285                  )
5286              );
5287          }
5288  
5289          if ( ! array_key_exists( 'items_removed', $response ) ) {
5290              wp_send_json_error(
5291                  sprintf(
5292                      /* translators: 1: Eraser friendly name, 2: Eraser array index. */
5293                      __( 'Expected items_removed key in response array from %1$s eraser (index %2$d).' ),
5294                      esc_html( $eraser_friendly_name ),
5295                      $eraser_index
5296                  )
5297              );
5298          }
5299  
5300          if ( ! array_key_exists( 'items_retained', $response ) ) {
5301              wp_send_json_error(
5302                  sprintf(
5303                      /* translators: 1: Eraser friendly name, 2: Eraser array index. */
5304                      __( 'Expected items_retained key in response array from %1$s eraser (index %2$d).' ),
5305                      esc_html( $eraser_friendly_name ),
5306                      $eraser_index
5307                  )
5308              );
5309          }
5310  
5311          if ( ! array_key_exists( 'messages', $response ) ) {
5312              wp_send_json_error(
5313                  sprintf(
5314                      /* translators: 1: Eraser friendly name, 2: Eraser array index. */
5315                      __( 'Expected messages key in response array from %1$s eraser (index %2$d).' ),
5316                      esc_html( $eraser_friendly_name ),
5317                      $eraser_index
5318                  )
5319              );
5320          }
5321  
5322          if ( ! is_array( $response['messages'] ) ) {
5323              wp_send_json_error(
5324                  sprintf(
5325                      /* translators: 1: Eraser friendly name, 2: Eraser array index. */
5326                      __( 'Expected messages key to reference an array in response array from %1$s eraser (index %2$d).' ),
5327                      esc_html( $eraser_friendly_name ),
5328                      $eraser_index
5329                  )
5330              );
5331          }
5332  
5333          if ( ! array_key_exists( 'done', $response ) ) {
5334              wp_send_json_error(
5335                  sprintf(
5336                      /* translators: 1: Eraser friendly name, 2: Eraser array index. */
5337                      __( 'Expected done flag in response array from %1$s eraser (index %2$d).' ),
5338                      esc_html( $eraser_friendly_name ),
5339                      $eraser_index
5340                  )
5341              );
5342          }
5343      } else {
5344          // No erasers, so we're done.
5345          $eraser_key = '';
5346  
5347          $response = array(
5348              'items_removed'  => false,
5349              'items_retained' => false,
5350              'messages'       => array(),
5351              'done'           => true,
5352          );
5353      }
5354  
5355      /**
5356       * Filters a page of personal data eraser data.
5357       *
5358       * Allows the erasure response to be consumed by destinations in addition to Ajax.
5359       *
5360       * @since 4.9.6
5361       *
5362       * @param array  $response        {
5363       *     The personal data for the given exporter and page number.
5364       *
5365       *     @type bool     $items_removed  Whether items were actually removed or not.
5366       *     @type bool     $items_retained Whether items were retained or not.
5367       *     @type string[] $messages       An array of messages to add to the personal data export file.
5368       *     @type bool     $done           Whether the eraser is finished or not.
5369       * }
5370       * @param int    $eraser_index    The index of the eraser that provided this data.
5371       * @param string $email_address   The email address associated with this personal data.
5372       * @param int    $page            The page number for this response.
5373       * @param int    $request_id      The privacy request post ID associated with this request.
5374       * @param string $eraser_key      The key (slug) of the eraser that provided this data.
5375       */
5376      $response = apply_filters( 'wp_privacy_personal_data_erasure_page', $response, $eraser_index, $email_address, $page, $request_id, $eraser_key );
5377  
5378      if ( is_wp_error( $response ) ) {
5379          wp_send_json_error( $response );
5380      }
5381  
5382      wp_send_json_success( $response );
5383  }
5384  
5385  /**
5386   * Handles site health checks on server communication via AJAX.
5387   *
5388   * @since 5.2.0
5389   * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_dotorg_communication()
5390   * @see WP_REST_Site_Health_Controller::test_dotorg_communication()
5391   */
5392  function wp_ajax_health_check_dotorg_communication() {
5393      _doing_it_wrong(
5394          'wp_ajax_health_check_dotorg_communication',
5395          sprintf(
5396              /* translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. */
5397              __( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5398              'wp_ajax_health_check_dotorg_communication',
5399              'WP_REST_Site_Health_Controller::test_dotorg_communication'
5400          ),
5401          '5.6.0'
5402      );
5403  
5404      check_ajax_referer( 'health-check-site-status' );
5405  
5406      if ( ! current_user_can( 'view_site_health_checks' ) ) {
5407          wp_send_json_error();
5408      }
5409  
5410      if ( ! class_exists( 'WP_Site_Health' ) ) {
5411          require_once  ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
5412      }
5413  
5414      $site_health = WP_Site_Health::get_instance();
5415      wp_send_json_success( $site_health->get_test_dotorg_communication() );
5416  }
5417  
5418  /**
5419   * Handles site health checks on background updates via AJAX.
5420   *
5421   * @since 5.2.0
5422   * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_background_updates()
5423   * @see WP_REST_Site_Health_Controller::test_background_updates()
5424   */
5425  function wp_ajax_health_check_background_updates() {
5426      _doing_it_wrong(
5427          'wp_ajax_health_check_background_updates',
5428          sprintf(
5429              /* translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. */
5430              __( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5431              'wp_ajax_health_check_background_updates',
5432              'WP_REST_Site_Health_Controller::test_background_updates'
5433          ),
5434          '5.6.0'
5435      );
5436  
5437      check_ajax_referer( 'health-check-site-status' );
5438  
5439      if ( ! current_user_can( 'view_site_health_checks' ) ) {
5440          wp_send_json_error();
5441      }
5442  
5443      if ( ! class_exists( 'WP_Site_Health' ) ) {
5444          require_once  ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
5445      }
5446  
5447      $site_health = WP_Site_Health::get_instance();
5448      wp_send_json_success( $site_health->get_test_background_updates() );
5449  }
5450  
5451  /**
5452   * Handles site health checks on loopback requests via AJAX.
5453   *
5454   * @since 5.2.0
5455   * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_loopback_requests()
5456   * @see WP_REST_Site_Health_Controller::test_loopback_requests()
5457   */
5458  function wp_ajax_health_check_loopback_requests() {
5459      _doing_it_wrong(
5460          'wp_ajax_health_check_loopback_requests',
5461          sprintf(
5462              /* translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. */
5463              __( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5464              'wp_ajax_health_check_loopback_requests',
5465              'WP_REST_Site_Health_Controller::test_loopback_requests'
5466          ),
5467          '5.6.0'
5468      );
5469  
5470      check_ajax_referer( 'health-check-site-status' );
5471  
5472      if ( ! current_user_can( 'view_site_health_checks' ) ) {
5473          wp_send_json_error();
5474      }
5475  
5476      if ( ! class_exists( 'WP_Site_Health' ) ) {
5477          require_once  ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
5478      }
5479  
5480      $site_health = WP_Site_Health::get_instance();
5481      wp_send_json_success( $site_health->get_test_loopback_requests() );
5482  }
5483  
5484  /**
5485   * Handles site health check to update the result status via AJAX.
5486   *
5487   * @since 5.2.0
5488   */
5489  function wp_ajax_health_check_site_status_result() {
5490      check_ajax_referer( 'health-check-site-status-result' );
5491  
5492      if ( ! current_user_can( 'view_site_health_checks' ) ) {
5493          wp_send_json_error();
5494      }
5495  
5496      set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) );
5497  
5498      wp_send_json_success();
5499  }
5500  
5501  /**
5502   * Handles site health check to get directories and database sizes via AJAX.
5503   *
5504   * @since 5.2.0
5505   * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::get_directory_sizes()
5506   * @see WP_REST_Site_Health_Controller::get_directory_sizes()
5507   */
5508  function wp_ajax_health_check_get_sizes() {
5509      _doing_it_wrong(
5510          'wp_ajax_health_check_get_sizes',
5511          sprintf(
5512              /* translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. */
5513              __( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5514              'wp_ajax_health_check_get_sizes',
5515              'WP_REST_Site_Health_Controller::get_directory_sizes'
5516          ),
5517          '5.6.0'
5518      );
5519  
5520      check_ajax_referer( 'health-check-site-status-result' );
5521  
5522      if ( ! current_user_can( 'view_site_health_checks' ) || is_multisite() ) {
5523          wp_send_json_error();
5524      }
5525  
5526      if ( ! class_exists( 'WP_Debug_Data' ) ) {
5527          require_once  ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
5528      }
5529  
5530      $sizes_data = WP_Debug_Data::get_sizes();
5531      $all_sizes  = array( 'raw' => 0 );
5532  
5533      foreach ( $sizes_data as $name => $value ) {
5534          $name = sanitize_text_field( $name );
5535          $data = array();
5536  
5537          if ( isset( $value['size'] ) ) {
5538              if ( is_string( $value['size'] ) ) {
5539                  $data['size'] = sanitize_text_field( $value['size'] );
5540              } else {
5541                  $data['size'] = (int) $value['size'];
5542              }
5543          }
5544  
5545          if ( isset( $value['debug'] ) ) {
5546              if ( is_string( $value['debug'] ) ) {
5547                  $data['debug'] = sanitize_text_field( $value['debug'] );
5548              } else {
5549                  $data['debug'] = (int) $value['debug'];
5550              }
5551          }
5552  
5553          if ( ! empty( $value['raw'] ) ) {
5554              $data['raw'] = (int) $value['raw'];
5555          }
5556  
5557          $all_sizes[ $name ] = $data;
5558      }
5559  
5560      if ( isset( $all_sizes['total_size']['debug'] ) && 'not available' === $all_sizes['total_size']['debug'] ) {
5561          wp_send_json_error( $all_sizes );
5562      }
5563  
5564      wp_send_json_success( $all_sizes );
5565  }
5566  
5567  /**
5568   * Handles renewing the REST API nonce via AJAX.
5569   *
5570   * @since 5.3.0
5571   */
5572  function wp_ajax_rest_nonce() {
5573      exit( wp_create_nonce( 'wp_rest' ) );
5574  }
5575  
5576  /**
5577   * Handles enabling or disable plugin and theme auto-updates via AJAX.
5578   *
5579   * @since 5.5.0
5580   */
5581  function wp_ajax_toggle_auto_updates() {
5582      check_ajax_referer( 'updates' );
5583  
5584      if ( empty( $_POST['type'] ) || empty( $_POST['asset'] ) || empty( $_POST['state'] ) ) {
5585          wp_send_json_error( array( 'error' => __( 'Invalid data. No selected item.' ) ) );
5586      }
5587  
5588      $asset = sanitize_text_field( urldecode( $_POST['asset'] ) );
5589  
5590      if ( 'enable' !== $_POST['state'] && 'disable' !== $_POST['state'] ) {
5591          wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown state.' ) ) );
5592      }
5593      $state = $_POST['state'];
5594  
5595      if ( 'plugin' !== $_POST['type'] && 'theme' !== $_POST['type'] ) {
5596          wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
5597      }
5598      $type = $_POST['type'];
5599  
5600      switch ( $type ) {
5601          case 'plugin':
5602              if ( ! current_user_can( 'update_plugins' ) ) {
5603                  $error_message = __( 'Sorry, you are not allowed to modify plugins.' );
5604                  wp_send_json_error( array( 'error' => $error_message ) );
5605              }
5606  
5607              $option = 'auto_update_plugins';
5608              /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
5609              $all_items = apply_filters( 'all_plugins', get_plugins() );
5610              break;
5611          case 'theme':
5612              if ( ! current_user_can( 'update_themes' ) ) {
5613                  $error_message = __( 'Sorry, you are not allowed to modify themes.' );
5614                  wp_send_json_error( array( 'error' => $error_message ) );
5615              }
5616  
5617              $option    = 'auto_update_themes';
5618              $all_items = wp_get_themes();
5619              break;
5620          default:
5621              wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
5622      }
5623  
5624      if ( ! array_key_exists( $asset, $all_items ) ) {
5625          $error_message = __( 'Invalid data. The item does not exist.' );
5626          wp_send_json_error( array( 'error' => $error_message ) );
5627      }
5628  
5629      $auto_updates = (array) get_site_option( $option, array() );
5630  
5631      if ( 'disable' === $state ) {
5632          $auto_updates = array_diff( $auto_updates, array( $asset ) );
5633      } else {
5634          $auto_updates[] = $asset;
5635          $auto_updates   = array_unique( $auto_updates );
5636      }
5637  
5638      // Remove items that have been deleted since the site option was last updated.
5639      $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
5640  
5641      update_site_option( $option, $auto_updates );
5642  
5643      wp_send_json_success();
5644  }
5645  
5646  /**
5647   * Handles sending a password reset link via AJAX.
5648   *
5649   * @since 5.7.0
5650   */
5651  function wp_ajax_send_password_reset() {
5652  
5653      // Validate the nonce for this action.
5654      $user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
5655      check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' );
5656  
5657      // Verify user capabilities.
5658      if ( ! current_user_can( 'edit_user', $user_id ) ) {
5659          wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) );
5660      }
5661  
5662      // Send the password reset link.
5663      $user    = get_userdata( $user_id );
5664      $results = retrieve_password( $user->user_login );
5665  
5666      if ( true === $results ) {
5667          wp_send_json_success(
5668              /* translators: %s: User's display name. */
5669              sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name )
5670          );
5671      } else {
5672          wp_send_json_error( $results->get_error_message() );
5673      }
5674  }


Generated : Thu Aug 6 08:20:21 2026 Cross-referenced by PHPXref