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


Generated : Mon Oct 26 08:20:34 2026 Cross-referenced by PHPXref