[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-comments-controller.php (source)

   1  <?php
   2  /**
   3   * REST API: WP_REST_Comments_Controller class
   4   *
   5   * @package WordPress
   6   * @subpackage REST_API
   7   * @since 4.7.0
   8   */
   9  
  10  /**
  11   * Core controller used to access comments via the REST API.
  12   *
  13   * @since 4.7.0
  14   *
  15   * @see WP_REST_Controller
  16   */
  17  class WP_REST_Comments_Controller extends WP_REST_Controller {
  18  
  19      /**
  20       * Instance of a comment meta fields object.
  21       *
  22       * @since 4.7.0
  23       * @var WP_REST_Comment_Meta_Fields
  24       */
  25      protected $meta;
  26  
  27      /**
  28       * Pre-fetched reaction summaries keyed by note comment ID.
  29       *
  30       * Populated by get_items() to avoid N+1 queries when listing notes
  31       * with their reaction summaries. Reset after each get_items() call.
  32       *
  33       * @since 7.2.0
  34       * @var array<int, array<string, array{count: int, current_user_reaction: int}>>|null
  35       */
  36      protected $reaction_summaries = null;
  37  
  38      /**
  39       * Retrieves the hex keys of the emoji a note reaction accepts.
  40       *
  41       * Each key is the emoji's lowercase code points, padded to four digits,
  42       * matching the client's `emojiToHexKey()`. A reaction stores its key in
  43       * `comment_content`.
  44       *
  45       * @since 7.2.0
  46       *
  47       * @return string[] Hex keys for heart, celebration, smile, eyes and rocket.
  48       *
  49       * @phpstan-return non-empty-list<lowercase-string&non-falsy-string>
  50       */
  51  	private static function get_note_reaction_keys(): array {
  52          return array( '2764', '1f389', '1f604', '1f440', '1f680' );
  53      }
  54  
  55      /**
  56       * Constructor.
  57       *
  58       * @since 4.7.0
  59       */
  60  	public function __construct() {
  61          $this->namespace = 'wp/v2';
  62          $this->rest_base = 'comments';
  63  
  64          $this->meta = new WP_REST_Comment_Meta_Fields();
  65      }
  66  
  67      /**
  68       * Registers the routes for comments.
  69       *
  70       * @since 4.7.0
  71       *
  72       * @see register_rest_route()
  73       */
  74  	public function register_routes() {
  75  
  76          register_rest_route(
  77              $this->namespace,
  78              '/' . $this->rest_base,
  79              array(
  80                  array(
  81                      'methods'             => WP_REST_Server::READABLE,
  82                      'callback'            => array( $this, 'get_items' ),
  83                      'permission_callback' => array( $this, 'get_items_permissions_check' ),
  84                      'args'                => $this->get_collection_params(),
  85                  ),
  86                  array(
  87                      'methods'             => WP_REST_Server::CREATABLE,
  88                      'callback'            => array( $this, 'create_item' ),
  89                      'permission_callback' => array( $this, 'create_item_permissions_check' ),
  90                      'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
  91                  ),
  92                  'schema' => array( $this, 'get_public_item_schema' ),
  93              )
  94          );
  95  
  96          register_rest_route(
  97              $this->namespace,
  98              '/' . $this->rest_base . '/(?P<id>[\d]+)',
  99              array(
 100                  'args'   => array(
 101                      'id' => array(
 102                          'description' => __( 'Unique identifier for the comment.' ),
 103                          'type'        => 'integer',
 104                      ),
 105                  ),
 106                  array(
 107                      'methods'             => WP_REST_Server::READABLE,
 108                      'callback'            => array( $this, 'get_item' ),
 109                      'permission_callback' => array( $this, 'get_item_permissions_check' ),
 110                      'args'                => array(
 111                          'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
 112                          'password' => array(
 113                              'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ),
 114                              'type'        => 'string',
 115                          ),
 116                      ),
 117                  ),
 118                  array(
 119                      'methods'             => WP_REST_Server::EDITABLE,
 120                      'callback'            => array( $this, 'update_item' ),
 121                      'permission_callback' => array( $this, 'update_item_permissions_check' ),
 122                      'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
 123                  ),
 124                  array(
 125                      'methods'             => WP_REST_Server::DELETABLE,
 126                      'callback'            => array( $this, 'delete_item' ),
 127                      'permission_callback' => array( $this, 'delete_item_permissions_check' ),
 128                      'args'                => array(
 129                          'force'    => array(
 130                              'type'        => 'boolean',
 131                              'default'     => false,
 132                              'description' => __( 'Whether to bypass Trash and force deletion.' ),
 133                          ),
 134                          'password' => array(
 135                              'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ),
 136                              'type'        => 'string',
 137                          ),
 138                      ),
 139                  ),
 140                  'schema' => array( $this, 'get_public_item_schema' ),
 141              )
 142          );
 143      }
 144  
 145      /**
 146       * Checks if a given request has access to read comments.
 147       *
 148       * @since 4.7.0
 149       *
 150       * @param WP_REST_Request $request Full details about the request.
 151       * @return true|WP_Error True if the request has read access, error object otherwise.
 152       */
 153  	public function get_items_permissions_check( $request ) {
 154          $is_note          = in_array( $request['type'], array( 'note', 'reaction' ), true );
 155          $is_edit_context  = 'edit' === $request['context'];
 156          $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
 157          $forbidden_params = array();
 158  
 159          if ( ! empty( $request['post'] ) ) {
 160              foreach ( (array) $request['post'] as $post_id ) {
 161                  $post = get_post( $post_id );
 162  
 163                  if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) {
 164                      return new WP_Error(
 165                          'rest_cannot_read_post',
 166                          __( 'Sorry, you are not allowed to read the post for this comment.' ),
 167                          array( 'status' => rest_authorization_required_code() )
 168                      );
 169                  } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
 170                      return new WP_Error(
 171                          'rest_cannot_read',
 172                          __( 'Sorry, you are not allowed to read comments without a post.' ),
 173                          array( 'status' => rest_authorization_required_code() )
 174                      );
 175                  }
 176  
 177                  if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
 178                      if ( current_user_can( 'edit_post', $post->ID ) ) {
 179                          return new WP_Error(
 180                              'rest_comment_not_supported_post_type',
 181                              __( 'Sorry, this post type does not support notes.' ),
 182                              array( 'status' => 403 )
 183                          );
 184                      }
 185  
 186                      foreach ( $protected_params as $param ) {
 187                          if ( 'status' === $param ) {
 188                              if ( array( 'approve' ) !== $request[ $param ] ) {
 189                                  $forbidden_params[] = $param;
 190                              }
 191                          } elseif ( 'type' === $param ) {
 192                              if ( 'comment' !== $request[ $param ] ) {
 193                                  $forbidden_params[] = $param;
 194                              }
 195                          } elseif ( ! empty( $request[ $param ] ) ) {
 196                              $forbidden_params[] = $param;
 197                          }
 198                      }
 199                      return new WP_Error(
 200                          'rest_forbidden_param',
 201                          /* translators: %s: List of forbidden parameters. */
 202                          sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ),
 203                          array( 'status' => rest_authorization_required_code() )
 204                      );
 205                  }
 206              }
 207          }
 208  
 209          // Re-map edit context capabilities when requesting `note` for a post.
 210          if ( $is_edit_context && $is_note && ! empty( $request['post'] ) ) {
 211              foreach ( (array) $request['post'] as $post_id ) {
 212                  if ( ! current_user_can( 'edit_post', $post_id ) ) {
 213                      return new WP_Error(
 214                          'rest_forbidden_context',
 215                          __( 'Sorry, you are not allowed to edit comments.' ),
 216                          array( 'status' => rest_authorization_required_code() )
 217                      );
 218                  }
 219              }
 220          } elseif ( $is_edit_context && ! current_user_can( 'moderate_comments' ) ) {
 221              return new WP_Error(
 222                  'rest_forbidden_context',
 223                  __( 'Sorry, you are not allowed to edit comments.' ),
 224                  array( 'status' => rest_authorization_required_code() )
 225              );
 226          }
 227  
 228          if ( ! current_user_can( 'edit_posts' ) ) {
 229              foreach ( $protected_params as $param ) {
 230                  if ( 'status' === $param ) {
 231                      if ( array( 'approve' ) !== $request[ $param ] ) {
 232                          $forbidden_params[] = $param;
 233                      }
 234                  } elseif ( 'type' === $param ) {
 235                      if ( 'comment' !== $request[ $param ] ) {
 236                          $forbidden_params[] = $param;
 237                      }
 238                  } elseif ( ! empty( $request[ $param ] ) ) {
 239                      $forbidden_params[] = $param;
 240                  }
 241              }
 242  
 243              if ( ! empty( $forbidden_params ) ) {
 244                  return new WP_Error(
 245                      'rest_forbidden_param',
 246                      /* translators: %s: List of forbidden parameters. */
 247                      sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ),
 248                      array( 'status' => rest_authorization_required_code() )
 249                  );
 250              }
 251          }
 252  
 253          return true;
 254      }
 255  
 256      /**
 257       * Retrieves a list of comment items.
 258       *
 259       * @since 4.7.0
 260       *
 261       * @param WP_REST_Request $request Full details about the request.
 262       * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
 263       */
 264  	public function get_items( $request ) {
 265  
 266          // Retrieve the list of registered collection query parameters.
 267          $registered = $this->get_collection_params();
 268  
 269          /*
 270           * This array defines mappings between public API query parameters whose
 271           * values are accepted as-passed, and their internal WP_Query parameter
 272           * name equivalents (some are the same). Only values which are also
 273           * present in $registered will be set.
 274           */
 275          $parameter_mappings = array(
 276              'author'         => 'author__in',
 277              'author_email'   => 'author_email',
 278              'author_exclude' => 'author__not_in',
 279              'exclude'        => 'comment__not_in',
 280              'include'        => 'comment__in',
 281              'offset'         => 'offset',
 282              'order'          => 'order',
 283              'parent'         => 'parent__in',
 284              'parent_exclude' => 'parent__not_in',
 285              'per_page'       => 'number',
 286              'post'           => 'post__in',
 287              'search'         => 'search',
 288              'status'         => 'status',
 289              'type'           => 'type',
 290          );
 291  
 292          $prepared_args = array();
 293  
 294          /*
 295           * For each known parameter which is both registered and present in the request,
 296           * set the parameter's value on the query $prepared_args.
 297           */
 298          foreach ( $parameter_mappings as $api_param => $wp_param ) {
 299              if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
 300                  $prepared_args[ $wp_param ] = $request[ $api_param ];
 301              }
 302          }
 303  
 304          // Ensure certain parameter values default to empty strings.
 305          foreach ( array( 'author_email', 'search' ) as $param ) {
 306              if ( ! isset( $prepared_args[ $param ] ) ) {
 307                  $prepared_args[ $param ] = '';
 308              }
 309          }
 310  
 311          if ( isset( $registered['orderby'] ) ) {
 312              $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
 313          }
 314  
 315          $prepared_args['no_found_rows'] = false;
 316  
 317          $prepared_args['update_comment_post_cache'] = true;
 318  
 319          $prepared_args['date_query'] = array();
 320  
 321          // Set before into date query. Date query must be specified as an array of an array.
 322          if ( isset( $registered['before'], $request['before'] ) ) {
 323              $prepared_args['date_query'][0]['before'] = $request['before'];
 324          }
 325  
 326          // Set after into date query. Date query must be specified as an array of an array.
 327          if ( isset( $registered['after'], $request['after'] ) ) {
 328              $prepared_args['date_query'][0]['after'] = $request['after'];
 329          }
 330  
 331          if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
 332              $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
 333          }
 334  
 335          $is_head_request = $request->is_method( 'HEAD' );
 336          if ( $is_head_request ) {
 337              // Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination.
 338              $prepared_args['fields'] = 'ids';
 339              // Disable priming comment meta for HEAD requests to improve performance.
 340              $prepared_args['update_comment_meta_cache'] = false;
 341          }
 342  
 343          /**
 344           * Filters WP_Comment_Query arguments when querying comments via the REST API.
 345           *
 346           * @since 4.7.0
 347           *
 348           * @link https://developer.wordpress.org/reference/classes/wp_comment_query/
 349           *
 350           * @param array           $prepared_args Array of arguments for WP_Comment_Query.
 351           * @param WP_REST_Request $request       The REST API request.
 352           */
 353          $prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request );
 354  
 355          $query        = new WP_Comment_Query();
 356          $query_result = $query->query( $prepared_args );
 357          $comments     = array();
 358  
 359          if ( ! $is_head_request ) {
 360              /*
 361               * When listing notes that include the reaction_summary field,
 362               * pre-fetch all summaries in a single aggregated query to
 363               * avoid an N+1 query in prepare_item_for_response().
 364               */
 365              $fields = $this->get_fields_for_response( $request );
 366              if (
 367                  ! empty( $request['type'] ) &&
 368                  'note' === $request['type'] &&
 369                  rest_is_field_included( 'reaction_summary', $fields )
 370              ) {
 371                  $note_ids = array_map( 'intval', wp_list_pluck( $query_result, 'comment_ID' ) );
 372                  if ( ! empty( $note_ids ) ) {
 373                      $this->prefetch_reaction_summaries( $note_ids );
 374                  }
 375              }
 376  
 377              foreach ( $query_result as $comment ) {
 378                  if ( ! $this->check_read_permission( $comment, $request ) ) {
 379                      continue;
 380                  }
 381  
 382                  $data       = $this->prepare_item_for_response( $comment, $request );
 383                  $comments[] = $this->prepare_response_for_collection( $data );
 384              }
 385  
 386              $this->reaction_summaries = null;
 387          }
 388  
 389          $total_comments = (int) $query->found_comments;
 390          $max_pages      = (int) $query->max_num_pages;
 391  
 392          if ( $total_comments < 1 ) {
 393              // Out-of-bounds, run the query without pagination/offset to get the total count.
 394              unset( $prepared_args['number'], $prepared_args['offset'] );
 395  
 396              $query                                      = new WP_Comment_Query();
 397              $prepared_args['count']                     = true;
 398              $prepared_args['orderby']                   = 'none';
 399              $prepared_args['update_comment_meta_cache'] = false;
 400  
 401              $total_comments = $query->query( $prepared_args );
 402              $max_pages      = (int) ceil( $total_comments / $request['per_page'] );
 403          }
 404  
 405          $response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $comments );
 406          $response->header( 'X-WP-Total', (string) $total_comments );
 407          $response->header( 'X-WP-TotalPages', (string) $max_pages );
 408  
 409          $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
 410  
 411          if ( $request['page'] > 1 ) {
 412              $prev_page = $request['page'] - 1;
 413  
 414              if ( $prev_page > $max_pages ) {
 415                  $prev_page = $max_pages;
 416              }
 417  
 418              $prev_link = add_query_arg( 'page', $prev_page, $base );
 419              $response->link_header( 'prev', $prev_link );
 420          }
 421  
 422          if ( $max_pages > $request['page'] ) {
 423              $next_page = $request['page'] + 1;
 424              $next_link = add_query_arg( 'page', $next_page, $base );
 425  
 426              $response->link_header( 'next', $next_link );
 427          }
 428  
 429          return $response;
 430      }
 431  
 432      /**
 433       * Get the comment, if the ID is valid.
 434       *
 435       * @since 4.7.2
 436       *
 437       * @param int $id Supplied ID.
 438       * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise.
 439       */
 440  	protected function get_comment( $id ) {
 441          $error = new WP_Error(
 442              'rest_comment_invalid_id',
 443              __( 'Invalid comment ID.' ),
 444              array( 'status' => 404 )
 445          );
 446  
 447          if ( (int) $id <= 0 ) {
 448              return $error;
 449          }
 450  
 451          $id      = (int) $id;
 452          $comment = get_comment( $id );
 453          if ( empty( $comment ) ) {
 454              return $error;
 455          }
 456  
 457          if ( ! empty( $comment->comment_post_ID ) ) {
 458              $post = get_post( (int) $comment->comment_post_ID );
 459  
 460              if ( empty( $post ) ) {
 461                  return new WP_Error(
 462                      'rest_post_invalid_id',
 463                      __( 'Invalid post ID.' ),
 464                      array( 'status' => 404 )
 465                  );
 466              }
 467          }
 468  
 469          return $comment;
 470      }
 471  
 472      /**
 473       * Checks if a given request has access to read the comment.
 474       *
 475       * @since 4.7.0
 476       *
 477       * @param WP_REST_Request $request Full details about the request.
 478       * @return true|WP_Error True if the request has read access for the item, error object otherwise.
 479       */
 480  	public function get_item_permissions_check( $request ) {
 481          $comment = $this->get_comment( $request['id'] );
 482          if ( is_wp_error( $comment ) ) {
 483              return $comment;
 484          }
 485  
 486          // Re-map edit context capabilities when requesting `note` or `reaction` type.
 487          $edit_cap = in_array( $comment->comment_type, array( 'note', 'reaction' ), true ) ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' );
 488          if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( ...$edit_cap ) ) {
 489              return new WP_Error(
 490                  'rest_forbidden_context',
 491                  __( 'Sorry, you are not allowed to edit comments.' ),
 492                  array( 'status' => rest_authorization_required_code() )
 493              );
 494          }
 495  
 496          $post = get_post( $comment->comment_post_ID );
 497  
 498          if ( ! $this->check_read_permission( $comment, $request ) ) {
 499              return new WP_Error(
 500                  'rest_cannot_read',
 501                  __( 'Sorry, you are not allowed to read this comment.' ),
 502                  array( 'status' => rest_authorization_required_code() )
 503              );
 504          }
 505  
 506          if ( $post && ! $this->check_read_post_permission( $post, $request ) ) {
 507              return new WP_Error(
 508                  'rest_cannot_read_post',
 509                  __( 'Sorry, you are not allowed to read the post for this comment.' ),
 510                  array( 'status' => rest_authorization_required_code() )
 511              );
 512          }
 513  
 514          return true;
 515      }
 516  
 517      /**
 518       * Retrieves a comment.
 519       *
 520       * @since 4.7.0
 521       *
 522       * @param WP_REST_Request $request Full details about the request.
 523       * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
 524       */
 525  	public function get_item( $request ) {
 526          $comment = $this->get_comment( $request['id'] );
 527          if ( is_wp_error( $comment ) ) {
 528              return $comment;
 529          }
 530  
 531          $data     = $this->prepare_item_for_response( $comment, $request );
 532          $response = rest_ensure_response( $data );
 533  
 534          return $response;
 535      }
 536  
 537      /**
 538       * Checks if a given request has access to create a comment.
 539       *
 540       * @since 4.7.0
 541       *
 542       * @param WP_REST_Request $request Full details about the request.
 543       * @return true|WP_Error True if the request has access to create items, error object otherwise.
 544       */
 545  	public function create_item_permissions_check( $request ) {
 546          $is_note = ! empty( $request['type'] ) && in_array( $request['type'], array( 'note', 'reaction' ), true );
 547  
 548          if ( ! is_user_logged_in() && $is_note ) {
 549              return new WP_Error(
 550                  'rest_comment_login_required',
 551                  __( 'Sorry, you must be logged in to comment.' ),
 552                  array( 'status' => 401 )
 553              );
 554          }
 555  
 556          if ( ! is_user_logged_in() ) {
 557              if ( get_option( 'comment_registration' ) ) {
 558                  return new WP_Error(
 559                      'rest_comment_login_required',
 560                      __( 'Sorry, you must be logged in to comment.' ),
 561                      array( 'status' => 401 )
 562                  );
 563              }
 564  
 565              /**
 566               * Filters whether comments can be created via the REST API without authentication.
 567               *
 568               * Enables creating comments for anonymous users.
 569               *
 570               * @since 4.7.0
 571               *
 572               * @param bool            $allow_anonymous Whether to allow anonymous comments to
 573               *                                         be created. Default `false`.
 574               * @param WP_REST_Request $request         Request used to generate the
 575               *                                         response.
 576               */
 577              $allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request );
 578  
 579              if ( ! $allow_anonymous ) {
 580                  return new WP_Error(
 581                      'rest_comment_login_required',
 582                      __( 'Sorry, you must be logged in to comment.' ),
 583                      array( 'status' => 401 )
 584                  );
 585              }
 586          }
 587  
 588          // Limit who can set comment `author`, `author_ip` or `status` to anything other than the default.
 589          if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
 590              return new WP_Error(
 591                  'rest_comment_invalid_author',
 592                  /* translators: %s: Request parameter. */
 593                  sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author' ),
 594                  array( 'status' => rest_authorization_required_code() )
 595              );
 596          }
 597  
 598          /*
 599           * A reaction is always first-person. create_item() enforces one emoji per
 600           * user per note against the current user, and update_item() refuses to
 601           * reattribute one, so a reaction stored against somebody else would be a
 602           * row the uniqueness check and the reaction summary can never see.
 603           */
 604          if (
 605              ! empty( $request['type'] ) && 'reaction' === $request['type'] &&
 606              isset( $request['author'] ) && get_current_user_id() !== (int) $request['author']
 607          ) {
 608              return new WP_Error(
 609                  'rest_comment_invalid_author',
 610                  __( 'Sorry, you are not allowed to add a reaction on behalf of another user.' ),
 611                  array( 'status' => rest_authorization_required_code() )
 612              );
 613          }
 614  
 615          if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) {
 616              if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) {
 617                  return new WP_Error(
 618                      'rest_comment_invalid_author_ip',
 619                      /* translators: %s: Request parameter. */
 620                      sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author_ip' ),
 621                      array( 'status' => rest_authorization_required_code() )
 622                  );
 623              }
 624          }
 625  
 626          if ( $is_note && ! empty( $request['post'] ) ) {
 627              $target_check = $this->check_target_post_permission( (int) $request['post'], $request, true );
 628  
 629              if ( is_wp_error( $target_check ) ) {
 630                  return $target_check;
 631              }
 632          }
 633  
 634          if ( empty( $request['post'] ) ) {
 635              return new WP_Error(
 636                  'rest_comment_invalid_post_id',
 637                  __( 'Sorry, you are not allowed to create this comment without a post.' ),
 638                  array( 'status' => 403 )
 639              );
 640          }
 641  
 642          $edit_cap = $is_note ? array( 'edit_post', (int) $request['post'] ) : array( 'moderate_comments' );
 643          if ( isset( $request['status'] ) && ! current_user_can( ...$edit_cap ) ) {
 644              return new WP_Error(
 645                  'rest_comment_invalid_status',
 646                  /* translators: %s: Request parameter. */
 647                  sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'status' ),
 648                  array( 'status' => rest_authorization_required_code() )
 649              );
 650          }
 651  
 652          /*
 653           * A note's target post was fully checked by check_target_post_permission()
 654           * above. Everything below applies to other comments only: a note is allowed
 655           * on a draft and on a post whose discussion is closed, and the rest would
 656           * repeat what that check already did.
 657           */
 658          if ( $is_note ) {
 659              return true;
 660          }
 661  
 662          $post = get_post( (int) $request['post'] );
 663  
 664          if ( ! $post ) {
 665              return new WP_Error(
 666                  'rest_comment_invalid_post_id',
 667                  __( 'Sorry, you are not allowed to create this comment without a post.' ),
 668                  array( 'status' => 403 )
 669              );
 670          }
 671  
 672          if ( 'draft' === $post->post_status ) {
 673              return new WP_Error(
 674                  'rest_comment_draft_post',
 675                  __( 'Sorry, you are not allowed to create a comment on this post.' ),
 676                  array( 'status' => 403 )
 677              );
 678          }
 679  
 680          if ( 'trash' === $post->post_status ) {
 681              return new WP_Error(
 682                  'rest_comment_trash_post',
 683                  __( 'Sorry, you are not allowed to create a comment on this post.' ),
 684                  array( 'status' => 403 )
 685              );
 686          }
 687  
 688          if ( ! $this->check_read_post_permission( $post, $request ) ) {
 689              return new WP_Error(
 690                  'rest_cannot_read_post',
 691                  __( 'Sorry, you are not allowed to read the post for this comment.' ),
 692                  array( 'status' => rest_authorization_required_code() )
 693              );
 694          }
 695  
 696          if ( ! comments_open( $post->ID ) ) {
 697              return new WP_Error(
 698                  'rest_comment_closed',
 699                  __( 'Sorry, comments are closed for this item.' ),
 700                  array( 'status' => 403 )
 701              );
 702          }
 703  
 704          return true;
 705      }
 706  
 707      /**
 708       * Creates a comment.
 709       *
 710       * @since 4.7.0
 711       * @since 7.2.0 Added support for the `reaction` comment type.
 712       *
 713       * @param WP_REST_Request $request Full details about the request.
 714       * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
 715       */
 716  	public function create_item( $request ) {
 717          if ( ! empty( $request['id'] ) ) {
 718              return new WP_Error(
 719                  'rest_comment_exists',
 720                  __( 'Cannot create existing comment.' ),
 721                  array( 'status' => 400 )
 722              );
 723          }
 724  
 725          // Do not allow comments to be created with a non-core type.
 726          if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array( 'comment', 'note', 'reaction' ), true ) ) {
 727              return new WP_Error(
 728                  'rest_invalid_comment_type',
 729                  __( 'Cannot create a comment with that type.' ),
 730                  array( 'status' => 400 )
 731              );
 732          }
 733  
 734          /*
 735           * The canonical reaction key, populated once validated below so the
 736           * stored content matches what was validated (not the raw input).
 737           */
 738          $reaction_key = null;
 739  
 740          // Validate reaction-specific constraints.
 741          if ( ! empty( $request['type'] ) && 'reaction' === $request['type'] ) {
 742              // Reaction parent must be specified.
 743              if ( empty( $request['parent'] ) ) {
 744                  return new WP_Error(
 745                      'rest_comment_invalid_parent',
 746                      __( 'A reaction must have a parent note.' ),
 747                      array( 'status' => 400 )
 748                  );
 749              }
 750  
 751              // Reaction parent must exist and be a note.
 752              $parent_comment = get_comment( $request['parent'] );
 753              if ( ! $parent_comment || 'note' !== $parent_comment->comment_type ) {
 754                  return new WP_Error(
 755                      'rest_comment_invalid_parent',
 756                      __( 'A reaction must be attached to a note.' ),
 757                      array( 'status' => 400 )
 758                  );
 759              }
 760  
 761              /*
 762               * The parent note must belong to the post the reaction targets.
 763               * create_item_permissions_check() requires `post` and checks that the
 764               * user can edit it, so this runs before the parent's status checks:
 765               * a note on another post gets the same error whatever its status,
 766               * rather than revealing whether it is trashed, spammed or resolved.
 767               */
 768              if ( ! empty( $request['post'] ) && (int) $parent_comment->comment_post_ID !== (int) $request['post'] ) {
 769                  return new WP_Error(
 770                      'rest_comment_invalid_parent',
 771                      __( 'A reaction must be attached to a note on the same post.' ),
 772                      array( 'status' => 400 )
 773                  );
 774              }
 775  
 776              // A reaction under a trashed or spammed note would escape the trash cascade.
 777              if ( in_array( $parent_comment->comment_approved, array( 'trash', 'spam' ), true ) ) {
 778                  return new WP_Error(
 779                      'rest_comment_invalid_parent',
 780                      __( 'A reaction cannot be added to a trashed or spam note.' ),
 781                      array( 'status' => 400 )
 782                  );
 783              }
 784  
 785              /*
 786               * Resolving a thread approves its root note, and the editor disables
 787               * reactions from then on. Hold requests from stale editor sessions
 788               * to that too, for the root note and for every reply in its thread.
 789               */
 790              $thread_root = $parent_comment;
 791              $visited     = array( (int) $thread_root->comment_ID => true );
 792              while ( $thread_root->comment_parent ) {
 793                  $ancestor = get_comment( $thread_root->comment_parent );
 794  
 795                  // Stop at a missing ancestor or a corrupt, cyclic chain.
 796                  if ( ! $ancestor || isset( $visited[ (int) $ancestor->comment_ID ] ) ) {
 797                      break;
 798                  }
 799  
 800                  $visited[ (int) $ancestor->comment_ID ] = true;
 801                  $thread_root                            = $ancestor;
 802              }
 803  
 804              if ( '1' === $thread_root->comment_approved ) {
 805                  return new WP_Error(
 806                      'rest_comment_invalid_parent',
 807                      __( 'A reaction cannot be added to a resolved note.' ),
 808                      array( 'status' => 400 )
 809                  );
 810              }
 811  
 812              /*
 813               * Validate the reaction content: the hex key of one of the curated
 814               * reaction emoji, as listed by self::get_note_reaction_keys() (e.g.
 815               * `2764` for the heart). Raw emoji bytes are rejected because the
 816               * comments table is not guaranteed to be utf8mb4 across all WordPress
 817               * installs; clients are expected to normalize before submitting.
 818               *
 819               * Read the content the same two ways prepare_item_for_database()
 820               * does, so `content` and `content.raw` are both accepted.
 821               */
 822              $raw_content = '';
 823              if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
 824                  $raw_content = $request['content'];
 825              } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
 826                  $raw_content = $request['content']['raw'];
 827              }
 828  
 829              $emoji_key = trim( wp_strip_all_tags( $raw_content ) );
 830  
 831              if ( ! in_array( $emoji_key, self::get_note_reaction_keys(), true ) ) {
 832                  return new WP_Error(
 833                      'rest_comment_invalid_reaction',
 834                      __( 'Invalid reaction emoji.' ),
 835                      array( 'status' => 400 )
 836                  );
 837              }
 838  
 839              /*
 840               * A reaction is always approved. The uniqueness check, the race
 841               * cleanup below and the reaction summary only see approved rows, so
 842               * a reaction created in any other status could never be counted,
 843               * deduplicated or, since reactions cannot be updated, fixed.
 844               */
 845              if ( isset( $request['status'] ) && ! in_array( $request['status'], array( 'approve', 'approved', '1' ), true ) ) {
 846                  return new WP_Error(
 847                      'rest_comment_invalid_status',
 848                      __( 'A reaction cannot be created with that status.' ),
 849                      array( 'status' => 400 )
 850                  );
 851              }
 852  
 853              /*
 854               * Enforce uniqueness: one emoji per user per note.
 855               *
 856               * Scope to active (approved) reactions only — trashed reactions
 857               * are invisible to the user and must not block re-adding the
 858               * same emoji.
 859               */
 860              $existing = get_comments(
 861                  array(
 862                      'parent'  => $request['parent'],
 863                      'user_id' => get_current_user_id(),
 864                      'type'    => 'reaction',
 865                      'status'  => 'approve',
 866                  )
 867              );
 868  
 869              foreach ( $existing as $existing_reaction ) {
 870                  if ( wp_strip_all_tags( $existing_reaction->comment_content ) === $emoji_key ) {
 871                      return new WP_Error(
 872                          'rest_comment_duplicate_reaction',
 873                          __( 'You have already reacted with this emoji.' ),
 874                          array( 'status' => 409 )
 875                      );
 876                  }
 877              }
 878  
 879              $reaction_key = $emoji_key;
 880          }
 881  
 882          $prepared_comment = $this->prepare_item_for_database( $request );
 883          if ( is_wp_error( $prepared_comment ) ) {
 884              return $prepared_comment;
 885          }
 886  
 887          $prepared_comment['comment_type'] = $request['type'];
 888  
 889          /*
 890           * Persist the validated, canonical reaction key rather than the raw
 891           * request content, so stored values stay consistent for grouping and
 892           * counting (e.g. "<b>2764</b>" is stored as "2764").
 893           */
 894          if ( null !== $reaction_key ) {
 895              $prepared_comment['comment_content'] = $reaction_key;
 896          }
 897  
 898          if ( ! isset( $prepared_comment['comment_content'] ) ) {
 899              $prepared_comment['comment_content'] = '';
 900          }
 901  
 902          // Include note metadata into check_is_comment_content_allowed.
 903          if ( isset( $request['meta']['_wp_note_status'] ) ) {
 904              $prepared_comment['meta']['_wp_note_status'] = $request['meta']['_wp_note_status'];
 905          }
 906  
 907          if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) {
 908              return new WP_Error(
 909                  'rest_comment_content_invalid',
 910                  __( 'Invalid comment content.' ),
 911                  array( 'status' => 400 )
 912              );
 913          }
 914  
 915          // Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
 916          if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
 917              $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
 918          }
 919  
 920          // Set author data if the user's logged in.
 921          $missing_author = empty( $prepared_comment['user_id'] )
 922              && empty( $prepared_comment['comment_author'] )
 923              && empty( $prepared_comment['comment_author_email'] )
 924              && empty( $prepared_comment['comment_author_url'] );
 925  
 926          if ( is_user_logged_in() && $missing_author ) {
 927              $user = wp_get_current_user();
 928  
 929              $prepared_comment['user_id']              = $user->ID;
 930              $prepared_comment['comment_author']       = $user->display_name;
 931              $prepared_comment['comment_author_email'] = $user->user_email;
 932              $prepared_comment['comment_author_url']   = $user->user_url;
 933          }
 934  
 935          /*
 936           * Pin a reaction to the current user, whatever author details the request
 937           * carried. Author fields alone leave `user_id` at 0, which the uniqueness
 938           * check and the reaction summary both key on.
 939           */
 940          if ( null !== $reaction_key ) {
 941              $user = wp_get_current_user();
 942  
 943              $prepared_comment['user_id']              = $user->ID;
 944              $prepared_comment['comment_author']       = $user->display_name;
 945              $prepared_comment['comment_author_email'] = $user->user_email;
 946              $prepared_comment['comment_author_url']   = $user->user_url;
 947          }
 948  
 949          // Honor the discussion setting that requires a name and email address of the comment author.
 950          if ( get_option( 'require_name_email' ) ) {
 951              if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) {
 952                  return new WP_Error(
 953                      'rest_comment_author_data_required',
 954                      __( 'Creating a comment requires valid author name and email values.' ),
 955                      array( 'status' => 400 )
 956                  );
 957              }
 958          }
 959  
 960          if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
 961              $prepared_comment['comment_author_email'] = '';
 962          }
 963  
 964          if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
 965              $prepared_comment['comment_author_url'] = '';
 966          }
 967  
 968          if ( ! isset( $prepared_comment['comment_agent'] ) ) {
 969              $prepared_comment['comment_agent'] = '';
 970          }
 971  
 972          $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment );
 973  
 974          if ( is_wp_error( $check_comment_lengths ) ) {
 975              $error_code = $check_comment_lengths->get_error_code();
 976              return new WP_Error(
 977                  $error_code,
 978                  __( 'Comment field exceeds maximum length allowed.' ),
 979                  array( 'status' => 400 )
 980              );
 981          }
 982  
 983          // Don't check for duplicates or flooding for notes or reactions.
 984          $prepared_comment['comment_approved'] =
 985              in_array( $prepared_comment['comment_type'], array( 'note', 'reaction' ), true ) ?
 986              '1' :
 987              wp_allow_comment( $prepared_comment, true );
 988  
 989          if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
 990              $error_code    = $prepared_comment['comment_approved']->get_error_code();
 991              $error_message = $prepared_comment['comment_approved']->get_error_message();
 992  
 993              if ( 'comment_duplicate' === $error_code ) {
 994                  return new WP_Error(
 995                      $error_code,
 996                      $error_message,
 997                      array( 'status' => 409 )
 998                  );
 999              }
1000  
1001              if ( 'comment_flood' === $error_code ) {
1002                  return new WP_Error(
1003                      $error_code,
1004                      $error_message,
1005                      array( 'status' => 400 )
1006                  );
1007              }
1008  
1009              return $prepared_comment['comment_approved'];
1010          }
1011  
1012          /**
1013           * Filters a comment before it is inserted via the REST API.
1014           *
1015           * Allows modification of the comment right before it is inserted via wp_insert_comment().
1016           * Returning a WP_Error value from the filter will short-circuit insertion and allow
1017           * skipping further processing.
1018           *
1019           * @since 4.7.0
1020           * @since 4.8.0 `$prepared_comment` can now be a WP_Error to short-circuit insertion.
1021           *
1022           * @param array|WP_Error  $prepared_comment The prepared comment data for wp_insert_comment().
1023           * @param WP_REST_Request $request          Request used to insert the comment.
1024           */
1025          $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
1026          if ( is_wp_error( $prepared_comment ) ) {
1027              return $prepared_comment;
1028          }
1029  
1030          $comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) );
1031  
1032          if ( ! $comment_id ) {
1033              return new WP_Error(
1034                  'rest_comment_failed_create',
1035                  __( 'Creating comment failed.' ),
1036                  array( 'status' => 500 )
1037              );
1038          }
1039  
1040          /*
1041           * The pre-insert uniqueness check is not atomic, so two concurrent
1042           * requests for the same user/note/emoji can both insert an approved
1043           * row. Converge on a single row deterministically: keep the earliest
1044           * matching reaction (lowest comment ID) and delete any later
1045           * duplicates. Every concurrent request applies the same rule, so they
1046           * all settle on the same surviving row. If this request's own row lost
1047           * the race, repoint the response to the survivor.
1048           */
1049          if ( null !== $reaction_key ) {
1050              $matching   = get_comments(
1051                  array(
1052                      'parent'  => $request['parent'],
1053                      'user_id' => get_current_user_id(),
1054                      'type'    => 'reaction',
1055                      'status'  => 'approve',
1056                      'orderby' => 'comment_ID',
1057                      'order'   => 'ASC',
1058                  )
1059              );
1060              $duplicates = array();
1061              foreach ( $matching as $candidate ) {
1062                  if ( wp_strip_all_tags( $candidate->comment_content ) === $reaction_key ) {
1063                      $duplicates[] = (int) $candidate->comment_ID;
1064                  }
1065              }
1066  
1067              /*
1068               * Repoint whenever any matching row survives, not only when this
1069               * request still sees its own duplicate: a competing request may
1070               * already have deleted this request's row, leaving a single
1071               * survivor that is not `$comment_id`.
1072               */
1073              if ( ! empty( $duplicates ) ) {
1074                  $survivor_id = array_shift( $duplicates );
1075                  foreach ( $duplicates as $duplicate_id ) {
1076                      wp_delete_comment( $duplicate_id, true );
1077                  }
1078                  $comment_id = $survivor_id;
1079              }
1080          }
1081  
1082          /*
1083           * Reactions are inserted approved and their status was validated above.
1084           * Skipping them here also keeps a request from changing the status of a
1085           * row that the race cleanup above may have handed it from another request.
1086           */
1087          if ( isset( $request['status'] ) && null === $reaction_key ) {
1088              $this->handle_status_param( $request['status'], $comment_id );
1089          }
1090  
1091          $comment = get_comment( $comment_id );
1092  
1093          /**
1094           * Fires after a comment is created or updated via the REST API.
1095           *
1096           * @since 4.7.0
1097           *
1098           * @param WP_Comment      $comment  Inserted or updated comment object.
1099           * @param WP_REST_Request $request  Request object.
1100           * @param bool            $creating True when creating a comment, false
1101           *                                  when updating.
1102           */
1103          do_action( 'rest_insert_comment', $comment, $request, true );
1104  
1105          $schema = $this->get_item_schema();
1106  
1107          if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
1108              $meta_update = $this->meta->update_value( $request['meta'], $comment_id );
1109  
1110              if ( is_wp_error( $meta_update ) ) {
1111                  return $meta_update;
1112              }
1113          }
1114  
1115          $fields_update = $this->update_additional_fields_for_object( $comment, $request );
1116  
1117          if ( is_wp_error( $fields_update ) ) {
1118              return $fields_update;
1119          }
1120  
1121          $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
1122          $request->set_param( 'context', $context );
1123  
1124          /**
1125           * Fires completely after a comment is created or updated via the REST API.
1126           *
1127           * @since 5.0.0
1128           *
1129           * @param WP_Comment      $comment  Inserted or updated comment object.
1130           * @param WP_REST_Request $request  Request object.
1131           * @param bool            $creating True when creating a comment, false
1132           *                                  when updating.
1133           */
1134          do_action( 'rest_after_insert_comment', $comment, $request, true );
1135  
1136          $response = $this->prepare_item_for_response( $comment, $request );
1137          $response = rest_ensure_response( $response );
1138  
1139          $response->set_status( 201 );
1140          $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );
1141  
1142          return $response;
1143      }
1144  
1145      /**
1146       * Checks if a given REST request has access to update a comment.
1147       *
1148       * @since 4.7.0
1149       * @since 7.1.1 Target post permissions are checked when a comment's parent post is changed.
1150       * @since 7.2.0 Reactions cannot be updated.
1151       *
1152       * @param WP_REST_Request $request Full details about the request.
1153       * @return true|WP_Error True if the request has access to update the item, error object otherwise.
1154       */
1155  	public function update_item_permissions_check( $request ) {
1156          $comment = $this->get_comment( $request['id'] );
1157          if ( is_wp_error( $comment ) ) {
1158              return $comment;
1159          }
1160  
1161          /*
1162           * Reactions are immutable. create_item() validates the author, parent
1163           * note, target post and canonical emoji hex key as a set, and none of that
1164           * is re-checked here. Allowing an update would let anyone who can edit
1165           * the note's post reattribute a reaction to another user, move it to a
1166           * note on a post they cannot edit, or store a duplicate or invalid
1167           * key. Removing a reaction is a delete.
1168           */
1169          if ( 'reaction' === $comment->comment_type ) {
1170              return new WP_Error(
1171                  'rest_comment_update_not_allowed',
1172                  __( 'Reactions cannot be edited. Remove the reaction and add a new one instead.' ),
1173                  array( 'status' => 403 )
1174              );
1175          }
1176  
1177          if ( ! $this->check_edit_permission( $comment ) ) {
1178              return new WP_Error(
1179                  'rest_cannot_edit',
1180                  __( 'Sorry, you are not allowed to edit this comment.' ),
1181                  array( 'status' => rest_authorization_required_code() )
1182              );
1183          }
1184  
1185          /*
1186           * check_edit_permission() above only establishes that the comment may be
1187           * edited where it currently sits, because 'edit_comment' maps to 'edit_post'
1188           * on the comment's current parent. When the parent is being changed, the new
1189           * parent has to be authorized as well. Without this, a user holding
1190           * edit_comment on their own comment or note could reparent it onto any post,
1191           * including posts they can neither read nor edit.
1192           */
1193          if ( isset( $request['post'] ) && (int) $request['post'] !== (int) $comment->comment_post_ID ) {
1194              $target_check = $this->check_target_post_permission(
1195                  (int) $request['post'],
1196                  $request,
1197                  'note' === $comment->comment_type
1198              );
1199  
1200              if ( is_wp_error( $target_check ) ) {
1201                  return $target_check;
1202              }
1203          }
1204  
1205          return true;
1206      }
1207  
1208      /**
1209       * Updates a comment.
1210       *
1211       * @since 4.7.0
1212       *
1213       * @param WP_REST_Request $request Full details about the request.
1214       * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
1215       */
1216  	public function update_item( $request ) {
1217          $comment = $this->get_comment( $request['id'] );
1218          if ( is_wp_error( $comment ) ) {
1219              return $comment;
1220          }
1221  
1222          $id = $comment->comment_ID;
1223  
1224          if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) {
1225              return new WP_Error(
1226                  'rest_comment_invalid_type',
1227                  __( 'Sorry, you are not allowed to change the comment type.' ),
1228                  array( 'status' => 404 )
1229              );
1230          }
1231  
1232          $prepared_args = $this->prepare_item_for_database( $request );
1233  
1234          if ( is_wp_error( $prepared_args ) ) {
1235              return $prepared_args;
1236          }
1237  
1238          if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
1239              $post = get_post( $prepared_args['comment_post_ID'] );
1240  
1241              if ( empty( $post ) ) {
1242                  return new WP_Error(
1243                      'rest_comment_invalid_post_id',
1244                      __( 'Invalid post ID.' ),
1245                      array( 'status' => 403 )
1246                  );
1247              }
1248          }
1249  
1250          if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
1251              // Only the comment status is being changed.
1252              $change = $this->handle_status_param( $request['status'], $id );
1253  
1254              if ( ! $change ) {
1255                  return new WP_Error(
1256                      'rest_comment_failed_edit',
1257                      __( 'Updating comment status failed.' ),
1258                      array( 'status' => 500 )
1259                  );
1260              }
1261          } elseif ( ! empty( $prepared_args ) ) {
1262              if ( is_wp_error( $prepared_args ) ) {
1263                  return $prepared_args;
1264              }
1265              if ( ! $this->check_is_comment_content_allowed( $prepared_args ) ) {
1266                  return new WP_Error(
1267                      'rest_comment_content_invalid',
1268                      __( 'Invalid comment content.' ),
1269                      array( 'status' => 400 )
1270                  );
1271              }
1272  
1273              $prepared_args['comment_ID'] = $id;
1274  
1275              $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
1276  
1277              if ( is_wp_error( $check_comment_lengths ) ) {
1278                  $error_code = $check_comment_lengths->get_error_code();
1279                  return new WP_Error(
1280                      $error_code,
1281                      __( 'Comment field exceeds maximum length allowed.' ),
1282                      array( 'status' => 400 )
1283                  );
1284              }
1285  
1286              $updated = wp_update_comment( wp_slash( (array) $prepared_args ), true );
1287  
1288              if ( is_wp_error( $updated ) ) {
1289                  return new WP_Error(
1290                      'rest_comment_failed_edit',
1291                      __( 'Updating comment failed.' ),
1292                      array( 'status' => 500 )
1293                  );
1294              }
1295  
1296              if ( isset( $request['status'] ) ) {
1297                  $this->handle_status_param( $request['status'], $id );
1298              }
1299          }
1300  
1301          $comment = get_comment( $id );
1302  
1303          /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
1304          do_action( 'rest_insert_comment', $comment, $request, false );
1305  
1306          $schema = $this->get_item_schema();
1307  
1308          if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
1309              $meta_update = $this->meta->update_value( $request['meta'], $id );
1310  
1311              if ( is_wp_error( $meta_update ) ) {
1312                  return $meta_update;
1313              }
1314          }
1315  
1316          $fields_update = $this->update_additional_fields_for_object( $comment, $request );
1317  
1318          if ( is_wp_error( $fields_update ) ) {
1319              return $fields_update;
1320          }
1321  
1322          $request->set_param( 'context', 'edit' );
1323  
1324          /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
1325          do_action( 'rest_after_insert_comment', $comment, $request, false );
1326  
1327          $response = $this->prepare_item_for_response( $comment, $request );
1328  
1329          return rest_ensure_response( $response );
1330      }
1331  
1332      /**
1333       * Checks if a given request has access to delete a comment.
1334       *
1335       * @since 4.7.0
1336       *
1337       * @param WP_REST_Request $request Full details about the request.
1338       * @return true|WP_Error True if the request has access to delete the item, error object otherwise.
1339       */
1340  	public function delete_item_permissions_check( $request ) {
1341          $comment = $this->get_comment( $request['id'] );
1342          if ( is_wp_error( $comment ) ) {
1343              return $comment;
1344          }
1345  
1346          if ( ! $this->check_edit_permission( $comment ) ) {
1347              return new WP_Error(
1348                  'rest_cannot_delete',
1349                  __( 'Sorry, you are not allowed to delete this comment.' ),
1350                  array( 'status' => rest_authorization_required_code() )
1351              );
1352          }
1353          return true;
1354      }
1355  
1356      /**
1357       * Deletes a comment.
1358       *
1359       * @since 4.7.0
1360       *
1361       * @param WP_REST_Request $request Full details about the request.
1362       * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
1363       */
1364  	public function delete_item( $request ) {
1365          $comment = $this->get_comment( $request['id'] );
1366          if ( is_wp_error( $comment ) ) {
1367              return $comment;
1368          }
1369  
1370          $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
1371  
1372          /**
1373           * Filters whether a comment can be trashed via the REST API.
1374           *
1375           * Return false to disable trash support for the comment.
1376           *
1377           * @since 4.7.0
1378           *
1379           * @param bool       $supports_trash Whether the comment supports trashing.
1380           * @param WP_Comment $comment        The comment object being considered for trashing support.
1381           */
1382          $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
1383  
1384          $request->set_param( 'context', 'edit' );
1385  
1386          if ( $force ) {
1387              $previous = $this->prepare_item_for_response( $comment, $request );
1388              $result   = wp_delete_comment( $comment->comment_ID, true );
1389              $response = new WP_REST_Response();
1390              $response->set_data(
1391                  array(
1392                      'deleted'  => true,
1393                      'previous' => $previous->get_data(),
1394                  )
1395              );
1396          } else {
1397              // If this type doesn't support trashing, error out.
1398              if ( ! $supports_trash ) {
1399                  return new WP_Error(
1400                      'rest_trash_not_supported',
1401                      /* translators: %s: force=true */
1402                      sprintf( __( "The comment does not support trashing. Set '%s' to delete." ), 'force=true' ),
1403                      array( 'status' => 501 )
1404                  );
1405              }
1406  
1407              if ( 'trash' === $comment->comment_approved ) {
1408                  return new WP_Error(
1409                      'rest_already_trashed',
1410                      __( 'The comment has already been trashed.' ),
1411                      array( 'status' => 410 )
1412                  );
1413              }
1414  
1415              $result   = wp_trash_comment( $comment->comment_ID );
1416              $comment  = get_comment( $comment->comment_ID );
1417              $response = $this->prepare_item_for_response( $comment, $request );
1418          }
1419  
1420          if ( ! $result ) {
1421              return new WP_Error(
1422                  'rest_cannot_delete',
1423                  __( 'The comment cannot be deleted.' ),
1424                  array( 'status' => 500 )
1425              );
1426          }
1427  
1428          /**
1429           * Fires after a comment is deleted via the REST API.
1430           *
1431           * @since 4.7.0
1432           *
1433           * @param WP_Comment       $comment  The deleted comment data.
1434           * @param WP_REST_Response $response The response returned from the API.
1435           * @param WP_REST_Request  $request  The request sent to the API.
1436           */
1437          do_action( 'rest_delete_comment', $comment, $response, $request );
1438  
1439          return $response;
1440      }
1441  
1442      /**
1443       * Prepares a single comment output for response.
1444       *
1445       * @since 4.7.0
1446       * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support.
1447       *
1448       * @param WP_Comment      $item    Comment object.
1449       * @param WP_REST_Request $request Request object.
1450       * @return WP_REST_Response Response object.
1451       */
1452  	public function prepare_item_for_response( $item, $request ) {
1453          // Restores the more descriptive, specific name for use within this method.
1454          $comment = $item;
1455  
1456          // Don't prepare the response body for HEAD requests.
1457          if ( $request->is_method( 'HEAD' ) ) {
1458              /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
1459              return apply_filters( 'rest_prepare_comment', new WP_REST_Response( array() ), $comment, $request );
1460          }
1461  
1462          $fields = $this->get_fields_for_response( $request );
1463          $data   = array();
1464  
1465          if ( in_array( 'id', $fields, true ) ) {
1466              $data['id'] = (int) $comment->comment_ID;
1467          }
1468  
1469          if ( in_array( 'post', $fields, true ) ) {
1470              $data['post'] = (int) $comment->comment_post_ID;
1471          }
1472  
1473          if ( in_array( 'parent', $fields, true ) ) {
1474              $data['parent'] = (int) $comment->comment_parent;
1475          }
1476  
1477          if ( in_array( 'author', $fields, true ) ) {
1478              $data['author'] = (int) $comment->user_id;
1479          }
1480  
1481          if ( in_array( 'author_name', $fields, true ) ) {
1482              $data['author_name'] = $comment->comment_author;
1483          }
1484  
1485          if ( in_array( 'author_email', $fields, true ) ) {
1486              $data['author_email'] = $comment->comment_author_email;
1487          }
1488  
1489          if ( in_array( 'author_url', $fields, true ) ) {
1490              $data['author_url'] = $comment->comment_author_url;
1491          }
1492  
1493          if ( in_array( 'author_ip', $fields, true ) ) {
1494              $data['author_ip'] = $comment->comment_author_IP;
1495          }
1496  
1497          if ( in_array( 'author_user_agent', $fields, true ) ) {
1498              $data['author_user_agent'] = $comment->comment_agent;
1499          }
1500  
1501          if ( in_array( 'date', $fields, true ) ) {
1502              $data['date'] = mysql_to_rfc3339( $comment->comment_date );
1503          }
1504  
1505          if ( in_array( 'date_gmt', $fields, true ) ) {
1506              $data['date_gmt'] = mysql_to_rfc3339( $comment->comment_date_gmt );
1507          }
1508  
1509          if ( in_array( 'content', $fields, true ) ) {
1510              $data['content'] = array(
1511                  /** This filter is documented in wp-includes/comment-template.php */
1512                  'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment, array() ),
1513                  'raw'      => $comment->comment_content,
1514              );
1515          }
1516  
1517          if ( in_array( 'link', $fields, true ) ) {
1518              $data['link'] = get_comment_link( $comment );
1519          }
1520  
1521          if ( in_array( 'status', $fields, true ) ) {
1522              $data['status'] = $this->prepare_status_response( $comment->comment_approved );
1523          }
1524  
1525          if ( in_array( 'type', $fields, true ) ) {
1526              $data['type'] = get_comment_type( $comment->comment_ID );
1527          }
1528  
1529          if ( in_array( 'author_avatar_urls', $fields, true ) ) {
1530              $data['author_avatar_urls'] = rest_get_avatar_urls( $comment );
1531          }
1532  
1533          if ( in_array( 'meta', $fields, true ) ) {
1534              $data['meta'] = $this->meta->get_value( $comment->comment_ID, $request );
1535          }
1536  
1537          if ( in_array( 'reaction_summary', $fields, true ) && 'note' === $comment->comment_type ) {
1538              $note_id = (int) $comment->comment_ID;
1539  
1540              if ( null !== $this->reaction_summaries && isset( $this->reaction_summaries[ $note_id ] ) ) {
1541                  $data['reaction_summary'] = $this->reaction_summaries[ $note_id ];
1542              } else {
1543                  // Single-item path (get_item or single create/update): query individually.
1544                  $this->prefetch_reaction_summaries( array( $note_id ) );
1545                  $data['reaction_summary'] = $this->reaction_summaries[ $note_id ] ?? array();
1546                  // Reset so subsequent unrelated calls do not see this entry.
1547                  $this->reaction_summaries = null;
1548              }
1549          }
1550  
1551          $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
1552          $data    = $this->add_additional_fields_to_object( $data, $request );
1553          $data    = $this->filter_response_by_context( $data, $context );
1554  
1555          // Wrap the data in a response object.
1556          $response = rest_ensure_response( $data );
1557  
1558          if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
1559              $links = $this->prepare_links( $comment );
1560              $response->add_links( $links );
1561  
1562              if ( ! empty( $links['self']['href'] ) ) {
1563                  $actions = $this->get_available_actions( $comment, $request );
1564  
1565                  $self = $links['self']['href'];
1566  
1567                  foreach ( $actions as $rel ) {
1568                      $response->add_link( $rel, $self );
1569                  }
1570              }
1571          }
1572  
1573          /**
1574           * Filters a comment returned from the REST API.
1575           *
1576           * Allows modification of the comment right before it is returned.
1577           *
1578           * @since 4.7.0
1579           *
1580           * @param WP_REST_Response  $response The response object.
1581           * @param WP_Comment        $comment  The original comment object.
1582           * @param WP_REST_Request   $request  Request used to generate the response.
1583           */
1584          return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
1585      }
1586  
1587      /**
1588       * Prepares links for the request.
1589       *
1590       * @since 4.7.0
1591       *
1592       * @param WP_Comment $comment Comment object.
1593       * @return array Links for the given comment.
1594       */
1595  	protected function prepare_links( $comment ) {
1596          $links = array(
1597              'self'       => array(
1598                  'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ),
1599              ),
1600              'collection' => array(
1601                  'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
1602              ),
1603          );
1604  
1605          if ( 0 !== (int) $comment->user_id ) {
1606              $links['author'] = array(
1607                  'href'       => rest_url( 'wp/v2/users/' . $comment->user_id ),
1608                  'embeddable' => true,
1609              );
1610          }
1611  
1612          if ( 0 !== (int) $comment->comment_post_ID ) {
1613              $post       = get_post( $comment->comment_post_ID );
1614              $post_route = rest_get_route_for_post( $post );
1615  
1616              if ( ! empty( $post->ID ) && $post_route ) {
1617                  $links['up'] = array(
1618                      'href'       => rest_url( $post_route ),
1619                      'embeddable' => true,
1620                      'post_type'  => $post->post_type,
1621                  );
1622              }
1623          }
1624  
1625          if ( 0 !== (int) $comment->comment_parent ) {
1626              $links['in-reply-to'] = array(
1627                  'href'       => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ),
1628                  'embeddable' => true,
1629              );
1630          }
1631  
1632          /*
1633           * Only grab one comment to verify the comment has children. Reactions are
1634           * left out: they are summarized in `reaction_summary`, and counting them
1635           * would advertise a `children` link on a note that has no replies.
1636           */
1637          $comment_children = $comment->get_children(
1638              array(
1639                  'count'        => true,
1640                  'orderby'      => 'none',
1641                  'type'         => 'all',
1642                  'type__not_in' => array( 'reaction' ),
1643              )
1644          );
1645  
1646          if ( ! empty( $comment_children ) ) {
1647              $args = array(
1648                  'parent' => $comment->comment_ID,
1649              );
1650  
1651              $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
1652  
1653              $links['children'] = array(
1654                  'href'       => $rest_url,
1655                  'embeddable' => true,
1656              );
1657          }
1658  
1659          // Embedding children for notes requires `type` and `status` inheritance.
1660          if ( isset( $links['children'] ) && 'note' === $comment->comment_type ) {
1661              $args = array(
1662                  'parent' => $comment->comment_ID,
1663                  'type'   => $comment->comment_type,
1664                  'status' => 'all',
1665              );
1666  
1667              $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
1668  
1669              $links['children'] = array(
1670                  'href'       => $rest_url,
1671                  'embeddable' => true,
1672              );
1673          }
1674  
1675          return $links;
1676      }
1677  
1678      /**
1679       * Gets the link relations available for the comment and current user.
1680       *
1681       * @since 7.2.0
1682       *
1683       * @param WP_Comment      $comment Comment object.
1684       * @param WP_REST_Request $request Request object.
1685       * @return string[] List of link relations.
1686       *
1687       * @phpstan-return array<non-falsy-string>
1688       */
1689  	protected function get_available_actions( $comment, $request ) {
1690          if ( 'edit' !== $request['context'] ) {
1691              return array();
1692          }
1693  
1694          $rels = array();
1695  
1696          if ( 'trash' !== $comment->comment_approved && $this->check_edit_permission( $comment ) ) {
1697              /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
1698              if ( apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment ) ) {
1699                  $rels[] = 'https://api.w.org/action-trash';
1700              }
1701          }
1702  
1703          return $rels;
1704      }
1705  
1706      /**
1707       * Prepends internal property prefix to query parameters to match our response fields.
1708       *
1709       * @since 4.7.0
1710       *
1711       * @param string $query_param Query parameter.
1712       * @return string The normalized query parameter.
1713       */
1714  	protected function normalize_query_param( $query_param ) {
1715          $prefix = 'comment_';
1716  
1717          switch ( $query_param ) {
1718              case 'id':
1719                  $normalized = $prefix . 'ID';
1720                  break;
1721              case 'post':
1722                  $normalized = $prefix . 'post_ID';
1723                  break;
1724              case 'parent':
1725                  $normalized = $prefix . 'parent';
1726                  break;
1727              case 'include':
1728                  $normalized = 'comment__in';
1729                  break;
1730              default:
1731                  $normalized = $prefix . $query_param;
1732                  break;
1733          }
1734  
1735          return $normalized;
1736      }
1737  
1738      /**
1739       * Checks comment_approved to set comment status for single comment output.
1740       *
1741       * @since 4.7.0
1742       *
1743       * @param string $comment_approved Comment status.
1744       * @return string Comment status.
1745       */
1746  	protected function prepare_status_response( $comment_approved ) {
1747  
1748          switch ( $comment_approved ) {
1749              case 'hold':
1750              case '0':
1751                  $status = 'hold';
1752                  break;
1753  
1754              case 'approve':
1755              case '1':
1756                  $status = 'approved';
1757                  break;
1758  
1759              case 'spam':
1760              case 'trash':
1761              default:
1762                  $status = $comment_approved;
1763                  break;
1764          }
1765  
1766          return $status;
1767      }
1768  
1769      /**
1770       * Prepares a single comment to be inserted into the database.
1771       *
1772       * @since 4.7.0
1773       *
1774       * @param WP_REST_Request $request Request object.
1775       * @return array|WP_Error Prepared comment, otherwise WP_Error object.
1776       */
1777  	protected function prepare_item_for_database( $request ) {
1778          $prepared_comment = array();
1779  
1780          /*
1781           * Allow the comment_content to be set via the 'content' or
1782           * the 'content.raw' properties of the Request object.
1783           */
1784          if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
1785              $prepared_comment['comment_content'] = trim( $request['content'] );
1786          } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
1787              $prepared_comment['comment_content'] = trim( $request['content']['raw'] );
1788          }
1789  
1790          if ( isset( $request['post'] ) ) {
1791              $prepared_comment['comment_post_ID'] = (int) $request['post'];
1792          }
1793  
1794          if ( isset( $request['parent'] ) ) {
1795              $prepared_comment['comment_parent'] = $request['parent'];
1796          }
1797  
1798          if ( isset( $request['author'] ) ) {
1799              $user = new WP_User( $request['author'] );
1800  
1801              if ( $user->exists() ) {
1802                  $prepared_comment['user_id']              = $user->ID;
1803                  $prepared_comment['comment_author']       = $user->display_name;
1804                  $prepared_comment['comment_author_email'] = $user->user_email;
1805                  $prepared_comment['comment_author_url']   = $user->user_url;
1806              } else {
1807                  return new WP_Error(
1808                      'rest_comment_author_invalid',
1809                      __( 'Invalid comment author ID.' ),
1810                      array( 'status' => 400 )
1811                  );
1812              }
1813          }
1814  
1815          if ( isset( $request['author_name'] ) ) {
1816              $prepared_comment['comment_author'] = $request['author_name'];
1817          }
1818  
1819          if ( isset( $request['author_email'] ) ) {
1820              $prepared_comment['comment_author_email'] = $request['author_email'];
1821          }
1822  
1823          if ( isset( $request['author_url'] ) ) {
1824              $prepared_comment['comment_author_url'] = $request['author_url'];
1825          }
1826  
1827          if ( isset( $request['author_ip'] ) && current_user_can( 'moderate_comments' ) ) {
1828              $prepared_comment['comment_author_IP'] = $request['author_ip'];
1829          } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) {
1830              $prepared_comment['comment_author_IP'] = $_SERVER['REMOTE_ADDR'];
1831          } else {
1832              $prepared_comment['comment_author_IP'] = '127.0.0.1';
1833          }
1834  
1835          if ( ! empty( $request['author_user_agent'] ) ) {
1836              $prepared_comment['comment_agent'] = $request['author_user_agent'];
1837          } elseif ( $request->get_header( 'user_agent' ) ) {
1838              $prepared_comment['comment_agent'] = $request->get_header( 'user_agent' );
1839          }
1840  
1841          if ( ! empty( $request['date'] ) ) {
1842              $date_data = rest_get_date_with_gmt( $request['date'] );
1843  
1844              if ( ! empty( $date_data ) ) {
1845                  list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
1846              }
1847          } elseif ( ! empty( $request['date_gmt'] ) ) {
1848              $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
1849  
1850              if ( ! empty( $date_data ) ) {
1851                  list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
1852              }
1853          }
1854  
1855          /**
1856           * Filters a comment added via the REST API after it is prepared for insertion into the database.
1857           *
1858           * Allows modification of the comment right after it is prepared for the database.
1859           *
1860           * @since 4.7.0
1861           *
1862           * @param array           $prepared_comment The prepared comment data for `wp_insert_comment`.
1863           * @param WP_REST_Request $request          The current request.
1864           */
1865          return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
1866      }
1867  
1868      /**
1869       * Retrieves the comment's schema, conforming to JSON Schema.
1870       *
1871       * @since 4.7.0
1872       *
1873       * @return array
1874       */
1875  	public function get_item_schema() {
1876          if ( $this->schema ) {
1877              return $this->add_additional_fields_schema( $this->schema );
1878          }
1879  
1880          $schema = array(
1881              '$schema'    => 'http://json-schema.org/draft-04/schema#',
1882              'title'      => 'comment',
1883              'type'       => 'object',
1884              'properties' => array(
1885                  'id'                => array(
1886                      'description' => __( 'Unique identifier for the comment.' ),
1887                      'type'        => 'integer',
1888                      'context'     => array( 'view', 'edit', 'embed' ),
1889                      'readonly'    => true,
1890                  ),
1891                  'author'            => array(
1892                      'description' => __( 'The ID of the user object, if author was a user.' ),
1893                      'type'        => 'integer',
1894                      'context'     => array( 'view', 'edit', 'embed' ),
1895                  ),
1896                  'author_email'      => array(
1897                      'description' => __( 'Email address for the comment author.' ),
1898                      'type'        => 'string',
1899                      'format'      => 'email',
1900                      'context'     => array( 'edit' ),
1901                      'arg_options' => array(
1902                          'sanitize_callback' => array( $this, 'check_comment_author_email' ),
1903                          'validate_callback' => null, // Skip built-in validation of 'email'.
1904                      ),
1905                  ),
1906                  'author_ip'         => array(
1907                      'description' => __( 'IP address for the comment author.' ),
1908                      'type'        => 'string',
1909                      'format'      => 'ip',
1910                      'context'     => array( 'edit' ),
1911                  ),
1912                  'author_name'       => array(
1913                      'description' => __( 'Display name for the comment author.' ),
1914                      'type'        => 'string',
1915                      'context'     => array( 'view', 'edit', 'embed' ),
1916                      'arg_options' => array(
1917                          'sanitize_callback' => 'sanitize_text_field',
1918                      ),
1919                  ),
1920                  'author_url'        => array(
1921                      'description' => __( 'URL for the comment author.' ),
1922                      'type'        => 'string',
1923                      'format'      => 'uri',
1924                      'context'     => array( 'view', 'edit', 'embed' ),
1925                  ),
1926                  'author_user_agent' => array(
1927                      'description' => __( 'User agent for the comment author.' ),
1928                      'type'        => 'string',
1929                      'context'     => array( 'edit' ),
1930                      'arg_options' => array(
1931                          'sanitize_callback' => 'sanitize_text_field',
1932                      ),
1933                  ),
1934                  'content'           => array(
1935                      'description' => __( 'The content for the comment.' ),
1936                      'type'        => 'object',
1937                      'context'     => array( 'view', 'edit', 'embed' ),
1938                      'arg_options' => array(
1939                          'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
1940                          'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
1941                      ),
1942                      'properties'  => array(
1943                          'raw'      => array(
1944                              'description' => __( 'Content for the comment, as it exists in the database.' ),
1945                              'type'        => 'string',
1946                              'context'     => array( 'edit' ),
1947                          ),
1948                          'rendered' => array(
1949                              'description' => __( 'HTML content for the comment, transformed for display.' ),
1950                              'type'        => 'string',
1951                              'context'     => array( 'view', 'edit', 'embed' ),
1952                              'readonly'    => true,
1953                          ),
1954                      ),
1955                  ),
1956                  'date'              => array(
1957                      'description' => __( "The date the comment was published, in the site's timezone." ),
1958                      'type'        => 'string',
1959                      'format'      => 'date-time',
1960                      'context'     => array( 'view', 'edit', 'embed' ),
1961                  ),
1962                  'date_gmt'          => array(
1963                      'description' => __( 'The date the comment was published, as GMT.' ),
1964                      'type'        => 'string',
1965                      'format'      => 'date-time',
1966                      'context'     => array( 'view', 'edit' ),
1967                  ),
1968                  'link'              => array(
1969                      'description' => __( 'URL to the comment.' ),
1970                      'type'        => 'string',
1971                      'format'      => 'uri',
1972                      'context'     => array( 'view', 'edit', 'embed' ),
1973                      'readonly'    => true,
1974                  ),
1975                  'parent'            => array(
1976                      'description' => __( 'The ID for the parent of the comment.' ),
1977                      'type'        => 'integer',
1978                      'context'     => array( 'view', 'edit', 'embed' ),
1979                      'default'     => 0,
1980                  ),
1981                  'post'              => array(
1982                      'description' => __( 'The ID of the associated post object.' ),
1983                      'type'        => 'integer',
1984                      'context'     => array( 'view', 'edit' ),
1985                      'default'     => 0,
1986                  ),
1987                  'status'            => array(
1988                      'description' => __( 'State of the comment.' ),
1989                      'type'        => 'string',
1990                      'context'     => array( 'view', 'edit' ),
1991                      'arg_options' => array(
1992                          'sanitize_callback' => 'sanitize_key',
1993                      ),
1994                  ),
1995                  'type'              => array(
1996                      'description' => __( 'Type of the comment.' ),
1997                      'type'        => 'string',
1998                      'context'     => array( 'view', 'edit', 'embed' ),
1999                      'readonly'    => true,
2000                      'default'     => 'comment',
2001                  ),
2002                  'reaction_summary'  => array(
2003                      'description'          => __( 'Aggregated reaction counts for this note, keyed by emoji hex key.' ),
2004                      'type'                 => 'object',
2005                      'context'              => array( 'view', 'edit' ),
2006                      'readonly'             => true,
2007                      'additionalProperties' => array(
2008                          'type'       => 'object',
2009                          'properties' => array(
2010                              'count'                 => array(
2011                                  'description' => __( 'Total number of reactions with this emoji.' ),
2012                                  'type'        => 'integer',
2013                                  'minimum'     => 0,
2014                              ),
2015                              'current_user_reaction' => array(
2016                                  'description' => __( "The current user's reaction comment ID for this emoji, or 0 if they have not reacted." ),
2017                                  'type'        => 'integer',
2018                                  'minimum'     => 0,
2019                              ),
2020                          ),
2021                      ),
2022                  ),
2023              ),
2024          );
2025  
2026          if ( get_option( 'show_avatars' ) ) {
2027              $avatar_properties = array();
2028  
2029              $avatar_sizes = rest_get_avatar_sizes();
2030  
2031              foreach ( $avatar_sizes as $size ) {
2032                  $avatar_properties[ $size ] = array(
2033                      /* translators: %d: Avatar image size in pixels. */
2034                      'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
2035                      'type'        => 'string',
2036                      'format'      => 'uri',
2037                      'context'     => array( 'embed', 'view', 'edit' ),
2038                  );
2039              }
2040  
2041              $schema['properties']['author_avatar_urls'] = array(
2042                  'description' => __( 'Avatar URLs for the comment author.' ),
2043                  'type'        => 'object',
2044                  'context'     => array( 'view', 'edit', 'embed' ),
2045                  'readonly'    => true,
2046                  'properties'  => $avatar_properties,
2047              );
2048          }
2049  
2050          $schema['properties']['meta'] = $this->meta->get_field_schema();
2051  
2052          $this->schema = $schema;
2053  
2054          return $this->add_additional_fields_schema( $this->schema );
2055      }
2056  
2057      /**
2058       * Retrieves the query params for collections.
2059       *
2060       * @since 4.7.0
2061       *
2062       * @return array Comments collection parameters.
2063       */
2064  	public function get_collection_params() {
2065          $query_params = parent::get_collection_params();
2066  
2067          $query_params['context']['default'] = 'view';
2068  
2069          $query_params['after'] = array(
2070              'description' => __( 'Limit response to comments published after a given ISO8601 compliant date.' ),
2071              'type'        => 'string',
2072              'format'      => 'date-time',
2073          );
2074  
2075          $query_params['author'] = array(
2076              'description' => __( 'Limit result set to comments assigned to specific user IDs. Requires authorization.' ),
2077              'type'        => 'array',
2078              'items'       => array(
2079                  'type' => 'integer',
2080              ),
2081          );
2082  
2083          $query_params['author_exclude'] = array(
2084              'description' => __( 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.' ),
2085              'type'        => 'array',
2086              'items'       => array(
2087                  'type' => 'integer',
2088              ),
2089          );
2090  
2091          $query_params['author_email'] = array(
2092              'default'     => null,
2093              'description' => __( 'Limit result set to that from a specific author email. Requires authorization.' ),
2094              'format'      => 'email',
2095              'type'        => 'string',
2096          );
2097  
2098          $query_params['before'] = array(
2099              'description' => __( 'Limit response to comments published before a given ISO8601 compliant date.' ),
2100              'type'        => 'string',
2101              'format'      => 'date-time',
2102          );
2103  
2104          $query_params['exclude'] = array(
2105              'description' => __( 'Ensure result set excludes specific IDs.' ),
2106              'type'        => 'array',
2107              'items'       => array(
2108                  'type' => 'integer',
2109              ),
2110              'default'     => array(),
2111          );
2112  
2113          $query_params['include'] = array(
2114              'description' => __( 'Limit result set to specific IDs.' ),
2115              'type'        => 'array',
2116              'items'       => array(
2117                  'type' => 'integer',
2118              ),
2119              'default'     => array(),
2120          );
2121  
2122          $query_params['offset'] = array(
2123              'description' => __( 'Offset the result set by a specific number of items.' ),
2124              'type'        => 'integer',
2125          );
2126  
2127          $query_params['order'] = array(
2128              'description' => __( 'Order sort attribute ascending or descending.' ),
2129              'type'        => 'string',
2130              'default'     => 'desc',
2131              'enum'        => array(
2132                  'asc',
2133                  'desc',
2134              ),
2135          );
2136  
2137          $query_params['orderby'] = array(
2138              'description' => __( 'Sort collection by comment attribute.' ),
2139              'type'        => 'string',
2140              'default'     => 'date_gmt',
2141              'enum'        => array(
2142                  'date',
2143                  'date_gmt',
2144                  'id',
2145                  'include',
2146                  'post',
2147                  'parent',
2148                  'type',
2149              ),
2150          );
2151  
2152          $query_params['parent'] = array(
2153              'default'     => array(),
2154              'description' => __( 'Limit result set to comments of specific parent IDs.' ),
2155              'type'        => 'array',
2156              'items'       => array(
2157                  'type' => 'integer',
2158              ),
2159          );
2160  
2161          $query_params['parent_exclude'] = array(
2162              'default'     => array(),
2163              'description' => __( 'Ensure result set excludes specific parent IDs.' ),
2164              'type'        => 'array',
2165              'items'       => array(
2166                  'type' => 'integer',
2167              ),
2168          );
2169  
2170          $query_params['post'] = array(
2171              'default'     => array(),
2172              'description' => __( 'Limit result set to comments assigned to specific post IDs.' ),
2173              'type'        => 'array',
2174              'items'       => array(
2175                  'type' => 'integer',
2176              ),
2177          );
2178  
2179          $query_params['status'] = array(
2180              'default'           => array( 'approve' ),
2181              'description'       => __( 'Limit result set to comments assigned one or more statuses. Requires authorization.' ),
2182              'type'              => 'array',
2183              'items'             => array(
2184                  'type' => 'string',
2185              ),
2186              'sanitize_callback' => array( $this, 'sanitize_comment_statuses' ),
2187              'validate_callback' => 'rest_validate_request_arg',
2188          );
2189  
2190          $query_params['type'] = array(
2191              'default'           => 'comment',
2192              'description'       => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ),
2193              'sanitize_callback' => 'sanitize_key',
2194              'type'              => 'string',
2195              'validate_callback' => 'rest_validate_request_arg',
2196          );
2197  
2198          $query_params['password'] = array(
2199              'description' => __( 'The password for the post if it is password protected.' ),
2200              'type'        => 'string',
2201          );
2202  
2203          /**
2204           * Filters REST API collection parameters for the comments controller.
2205           *
2206           * This filter registers the collection parameter, but does not map the
2207           * collection parameter to an internal WP_Comment_Query parameter. Use the
2208           * `rest_comment_query` filter to set WP_Comment_Query parameters.
2209           *
2210           * @since 4.7.0
2211           *
2212           * @param array $query_params JSON Schema-formatted collection parameters.
2213           */
2214          return apply_filters( 'rest_comment_collection_params', $query_params );
2215      }
2216  
2217      /**
2218       * Sets the comment_status of a given comment object when creating or updating a comment.
2219       *
2220       * @since 4.7.0
2221       *
2222       * @param string|int $new_status New comment status.
2223       * @param int        $comment_id Comment ID.
2224       * @return bool Whether the status was changed.
2225       */
2226  	protected function handle_status_param( $new_status, $comment_id ) {
2227          $old_status = wp_get_comment_status( $comment_id );
2228  
2229          if ( $new_status === $old_status ) {
2230              return false;
2231          }
2232  
2233          switch ( $new_status ) {
2234              case 'approved':
2235              case 'approve':
2236              case '1':
2237                  $changed = wp_set_comment_status( $comment_id, 'approve' );
2238                  break;
2239              case 'hold':
2240              case '0':
2241                  $changed = wp_set_comment_status( $comment_id, 'hold' );
2242                  break;
2243              case 'spam':
2244                  $changed = wp_spam_comment( $comment_id );
2245                  break;
2246              case 'unspam':
2247                  $changed = wp_unspam_comment( $comment_id );
2248                  break;
2249              case 'trash':
2250                  $changed = wp_trash_comment( $comment_id );
2251                  break;
2252              case 'untrash':
2253                  $changed = wp_untrash_comment( $comment_id );
2254                  break;
2255              default:
2256                  $changed = false;
2257                  break;
2258          }
2259  
2260          return $changed;
2261      }
2262  
2263      /**
2264       * Checks if the post can be read.
2265       *
2266       * Correctly handles posts with the inherit status.
2267       *
2268       * @since 4.7.0
2269       *
2270       * @param WP_Post         $post    Post object.
2271       * @param WP_REST_Request $request Request data to check.
2272       * @return bool Whether post can be read.
2273       */
2274  	protected function check_read_post_permission( $post, $request ) {
2275          $post_type = get_post_type_object( $post->post_type );
2276  
2277          // Return false if custom post type doesn't exist
2278          if ( ! $post_type ) {
2279              return false;
2280          }
2281  
2282          $posts_controller = $post_type->get_rest_controller();
2283  
2284          /*
2285           * Ensure the posts controller is specifically a WP_REST_Posts_Controller instance
2286           * before using methods specific to that controller.
2287           */
2288          if ( ! $posts_controller instanceof WP_REST_Posts_Controller ) {
2289              $posts_controller = new WP_REST_Posts_Controller( $post->post_type );
2290          }
2291  
2292          $has_password_filter = false;
2293  
2294          // Only check password if a specific post was queried for or a single comment
2295          $requested_post    = ! empty( $request['post'] ) && ( ! is_array( $request['post'] ) || 1 === count( $request['post'] ) );
2296          $requested_comment = ! empty( $request['id'] );
2297          if ( ( $requested_post || $requested_comment ) && $posts_controller->can_access_password_content( $post, $request ) ) {
2298              add_filter( 'post_password_required', '__return_false' );
2299  
2300              $has_password_filter = true;
2301          }
2302  
2303          if ( post_password_required( $post ) ) {
2304              $result = current_user_can( 'edit_post', $post->ID );
2305          } else {
2306              $result = $posts_controller->check_read_permission( $post );
2307          }
2308  
2309          if ( $has_password_filter ) {
2310              remove_filter( 'post_password_required', '__return_false' );
2311          }
2312  
2313          return $result;
2314      }
2315  
2316      /**
2317       * Pre-fetches reaction summaries for a set of note IDs.
2318       *
2319       * Runs two aggregated queries (one for the per-emoji counts, one for the
2320       * current user's own reactions) and stores the result in
2321       * $this->reaction_summaries, keyed by note comment ID. This lets a
2322       * batched note listing return reaction_summary for many notes without
2323       * issuing a per-note query.
2324       *
2325       * @since 7.2.0
2326       *
2327       * @global wpdb $wpdb WordPress database abstraction object.
2328       *
2329       * @param int[] $note_ids Array of note comment IDs.
2330       */
2331  	protected function prefetch_reaction_summaries( $note_ids ) {
2332          global $wpdb;
2333  
2334          $this->reaction_summaries = array();
2335  
2336          if ( empty( $note_ids ) ) {
2337              return;
2338          }
2339  
2340          $current_user_id = get_current_user_id();
2341  
2342          $parent_in = $wpdb->prepare(
2343              'comment_parent IN (' . implode( ',', array_fill( 0, count( $note_ids ), '%d' ) ) . ')',
2344              ...$note_ids
2345          );
2346  
2347          // Query 1: Get aggregated counts per emoji per note.
2348          $query  = "SELECT comment_parent, comment_content, COUNT(*) AS reaction_count FROM $wpdb->comments WHERE comment_type = 'reaction' AND comment_approved = '1' AND ";
2349          $query .= $parent_in;
2350          $query .= ' GROUP BY comment_parent, comment_content';
2351          // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2352          $counts = $wpdb->get_results( $query );
2353  
2354          // Query 2: Get the current user's own reaction IDs (if logged in).
2355          $my_reactions = array();
2356          if ( $current_user_id ) {
2357              $query  = "SELECT comment_ID, comment_parent, comment_content FROM $wpdb->comments WHERE comment_type = 'reaction' AND comment_approved = '1' AND ";
2358              $query .= $parent_in;
2359              $query .= $wpdb->prepare( ' AND user_id = %d', $current_user_id );
2360              // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2361              $user_rows = $wpdb->get_results( $query );
2362  
2363              if ( $user_rows ) {
2364                  foreach ( $user_rows as $row ) {
2365                      $key                  = (int) $row->comment_parent . ':' . wp_strip_all_tags( $row->comment_content );
2366                      $my_reactions[ $key ] = (int) $row->comment_ID;
2367                  }
2368              }
2369          }
2370  
2371          // Initialize empty summaries for every requested note ID.
2372          foreach ( $note_ids as $note_id ) {
2373              $this->reaction_summaries[ $note_id ] = array();
2374          }
2375  
2376          if ( ! $counts ) {
2377              return;
2378          }
2379  
2380          foreach ( $counts as $row ) {
2381              $note_id   = (int) $row->comment_parent;
2382              $emoji_key = wp_strip_all_tags( $row->comment_content );
2383              $key       = $note_id . ':' . $emoji_key;
2384  
2385              $this->reaction_summaries[ $note_id ][ $emoji_key ] = array(
2386                  'count'                 => (int) $row->reaction_count,
2387                  'current_user_reaction' => $my_reactions[ $key ] ?? 0,
2388              );
2389          }
2390      }
2391  
2392      /**
2393       * Checks if the comment can be read.
2394       *
2395       * @since 4.7.0
2396       *
2397       * @param WP_Comment      $comment Comment object.
2398       * @param WP_REST_Request $request Request data to check.
2399       * @return bool Whether the comment can be read.
2400       */
2401  	protected function check_read_permission( $comment, $request ) {
2402          if ( ! in_array( $comment->comment_type, array( 'note', 'reaction' ), true ) && ! empty( $comment->comment_post_ID ) ) {
2403              $post = get_post( $comment->comment_post_ID );
2404              if ( $post ) {
2405                  if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) {
2406                      return true;
2407                  }
2408              }
2409          }
2410  
2411          if ( 0 === get_current_user_id() ) {
2412              return false;
2413          }
2414  
2415          if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
2416              return false;
2417          }
2418  
2419          if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
2420              return true;
2421          }
2422  
2423          return current_user_can( 'edit_comment', $comment->comment_ID );
2424      }
2425  
2426      /**
2427       * Checks if a comment can be edited or deleted.
2428       *
2429       * @since 4.7.0
2430       *
2431       * @param WP_Comment $comment Comment object.
2432       * @return bool Whether the comment can be edited or deleted.
2433       */
2434  	protected function check_edit_permission( $comment ) {
2435          if ( 0 === (int) get_current_user_id() ) {
2436              return false;
2437          }
2438  
2439          if ( current_user_can( 'moderate_comments' ) ) {
2440              return true;
2441          }
2442  
2443          return current_user_can( 'edit_comment', $comment->comment_ID );
2444      }
2445  
2446      /**
2447       * Checks a comment author email for validity.
2448       *
2449       * Accepts either a valid email address or empty string as a valid comment
2450       * author email address. Setting the comment author email to an empty
2451       * string is allowed when a comment is being updated.
2452       *
2453       * @since 4.7.0
2454       *
2455       * @param string          $value   Author email value submitted.
2456       * @param WP_REST_Request $request Full details about the request.
2457       * @param string          $param   The parameter name.
2458       * @return string|WP_Error The sanitized email address, if valid,
2459       *                         otherwise an error.
2460       */
2461  	public function check_comment_author_email( $value, $request, $param ) {
2462          $email = (string) $value;
2463          if ( empty( $email ) ) {
2464              return $email;
2465          }
2466  
2467          $check_email = rest_validate_request_arg( $email, $request, $param );
2468          if ( is_wp_error( $check_email ) ) {
2469              return $check_email;
2470          }
2471  
2472          return $email;
2473      }
2474  
2475      /**
2476       * If empty comments are not allowed, checks if the provided comment content is not empty.
2477       *
2478       * @since 5.6.0
2479       *
2480       * @param array $prepared_comment The prepared comment data.
2481       * @return bool True if the content is allowed, false otherwise.
2482       */
2483  	protected function check_is_comment_content_allowed( $prepared_comment ) {
2484          if ( ! isset( $prepared_comment['comment_content'] ) ) {
2485              return true;
2486          }
2487  
2488          $check = wp_parse_args(
2489              $prepared_comment,
2490              array(
2491                  'comment_post_ID'      => 0,
2492                  'comment_author'       => null,
2493                  'comment_author_email' => null,
2494                  'comment_author_url'   => null,
2495                  'comment_parent'       => 0,
2496                  'user_id'              => 0,
2497              )
2498          );
2499  
2500          /** This filter is documented in wp-includes/comment.php */
2501          $allow_empty = apply_filters( 'allow_empty_comment', false, $check );
2502  
2503          if ( $allow_empty ) {
2504              return true;
2505          }
2506  
2507          // Allow empty notes only when resolution metadata is valid.
2508          if (
2509              isset( $check['comment_type'] ) &&
2510              'note' === $check['comment_type'] &&
2511              isset( $check['meta']['_wp_note_status'] ) &&
2512              in_array( $check['meta']['_wp_note_status'], array( 'resolved', 'reopen' ), true )
2513          ) {
2514              return true;
2515          }
2516  
2517          /*
2518           * Do not allow a comment to be created with missing or empty
2519           * comment_content. See wp_handle_comment_submission().
2520           */
2521          return '' !== $check['comment_content'];
2522      }
2523  
2524      /**
2525       * Sanitizes a single comment status or a list of comment statuses.
2526       *
2527       * @since 7.2.0
2528       *
2529       * @param string[]|string $statuses Comment status or array of comment statuses.
2530       * @return string[] Sanitized array of comment statuses.
2531       * @phpstan-return list<non-empty-lowercase-string>
2532       */
2533  	public function sanitize_comment_statuses( $statuses ): array {
2534          $statuses = array_unique( array_map( 'sanitize_key', wp_parse_list( $statuses ) ) );
2535  
2536          // Only drop empty values. A literal '0' is a queryable status, as comments are held with `comment_approved` of '0'.
2537          $statuses = array_filter(
2538              $statuses,
2539              static function ( $status ) {
2540                  return '' !== $status;
2541              }
2542          );
2543  
2544          return array_values( $statuses );
2545      }
2546  
2547      /**
2548       * Checks that a post can receive a comment or a note from the current user.
2549       *
2550       * Used when creating a note and when changing the parent post of an existing
2551       * comment or note, so that attaching content to a post is authorized the same
2552       * way whichever path it arrives by.
2553       *
2554       * @since 7.1.1
2555       *
2556       * @param int             $post_id Target post ID.
2557       * @param WP_REST_Request $request Full details about the request.
2558       * @param bool            $is_note Optional. Whether the comment is a note. Default false.
2559       * @return true|WP_Error True if the post can receive the comment, error object otherwise.
2560       */
2561  	protected function check_target_post_permission( int $post_id, WP_REST_Request $request, bool $is_note = false ) {
2562          if ( ! $post_id ) {
2563              return new WP_Error(
2564                  'rest_comment_invalid_post_id',
2565                  __( 'Sorry, you are not allowed to create this comment without a post.' ),
2566                  array( 'status' => 403 )
2567              );
2568          }
2569  
2570          /*
2571           * Notes are editorial content, so they may only be attached to a post the
2572           * user can edit. Any other comment needs either comment moderation rights
2573           * or edit access to the post, which is what check_edit_permission() grants
2574           * on the post a comment is moving away from. Requiring the same at the
2575           * destination means both ends of a move are authorized alike.
2576           */
2577          if ( $is_note ) {
2578              $can_target_post = current_user_can( 'edit_post', $post_id );
2579          } else {
2580              $can_target_post = current_user_can( 'moderate_comments' ) || current_user_can( 'edit_post', $post_id );
2581          }
2582  
2583          if ( ! $can_target_post ) {
2584              if ( $is_note ) {
2585                  return new WP_Error(
2586                      'rest_cannot_create_note',
2587                      __( 'Sorry, you are not allowed to create notes for this post.' ),
2588                      array( 'status' => rest_authorization_required_code() )
2589                  );
2590              }
2591  
2592              return new WP_Error(
2593                  'rest_cannot_edit',
2594                  __( 'Sorry, you are not allowed to edit this comment.' ),
2595                  array( 'status' => rest_authorization_required_code() )
2596              );
2597          }
2598  
2599          $post = get_post( $post_id );
2600  
2601          if ( ! $post ) {
2602              return new WP_Error(
2603                  'rest_comment_invalid_post_id',
2604                  __( 'Sorry, you are not allowed to create this comment without a post.' ),
2605                  array( 'status' => 403 )
2606              );
2607          }
2608  
2609          /*
2610           * The remaining rules mirror the create-time checks for notes only. They are
2611           * deliberately not applied to other comments, because moderators move comments
2612           * onto posts whose discussion has closed and onto drafts today. Enforcing the
2613           * create-time rules there would break that without blocking anything the
2614           * capability check above already permits.
2615           */
2616          if ( ! $is_note ) {
2617              return true;
2618          }
2619  
2620          if ( ! $this->check_post_type_supports_notes( $post->post_type ) ) {
2621              return new WP_Error(
2622                  'rest_comment_not_supported_post_type',
2623                  __( 'Sorry, this post type does not support notes.' ),
2624                  array( 'status' => 403 )
2625              );
2626          }
2627  
2628          if ( 'trash' === $post->post_status ) {
2629              return new WP_Error(
2630                  'rest_comment_trash_post',
2631                  __( 'Sorry, you are not allowed to create a comment on this post.' ),
2632                  array( 'status' => 403 )
2633              );
2634          }
2635  
2636          if ( ! $this->check_read_post_permission( $post, $request ) ) {
2637              return new WP_Error(
2638                  'rest_cannot_read_post',
2639                  __( 'Sorry, you are not allowed to read the post for this comment.' ),
2640                  array( 'status' => rest_authorization_required_code() )
2641              );
2642          }
2643  
2644          return true;
2645      }
2646  
2647      /**
2648       * Check if post type supports notes.
2649       *
2650       * @since 6.9.0
2651       *
2652       * @param string $post_type Post type name.
2653       * @return bool True if post type supports notes, false otherwise.
2654       */
2655  	private function check_post_type_supports_notes( $post_type ) {
2656          $supports = get_all_post_type_supports( $post_type );
2657  
2658          if ( ! isset( $supports['editor'] ) ) {
2659              return false;
2660          }
2661  
2662          if ( ! is_array( $supports['editor'] ) ) {
2663              return false;
2664          }
2665  
2666          return array_any( $supports['editor'], fn( $item ) => ! empty( $item['notes'] ) );
2667      }
2668  }


Generated : Thu Oct 22 08:20:34 2026 Cross-referenced by PHPXref