| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 * Constructor. 29 * 30 * @since 4.7.0 31 */ 32 public function __construct() { 33 $this->namespace = 'wp/v2'; 34 $this->rest_base = 'comments'; 35 36 $this->meta = new WP_REST_Comment_Meta_Fields(); 37 } 38 39 /** 40 * Registers the routes for comments. 41 * 42 * @since 4.7.0 43 * 44 * @see register_rest_route() 45 */ 46 public function register_routes() { 47 48 register_rest_route( 49 $this->namespace, 50 '/' . $this->rest_base, 51 array( 52 array( 53 'methods' => WP_REST_Server::READABLE, 54 'callback' => array( $this, 'get_items' ), 55 'permission_callback' => array( $this, 'get_items_permissions_check' ), 56 'args' => $this->get_collection_params(), 57 ), 58 array( 59 'methods' => WP_REST_Server::CREATABLE, 60 'callback' => array( $this, 'create_item' ), 61 'permission_callback' => array( $this, 'create_item_permissions_check' ), 62 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 63 ), 64 'schema' => array( $this, 'get_public_item_schema' ), 65 ) 66 ); 67 68 register_rest_route( 69 $this->namespace, 70 '/' . $this->rest_base . '/(?P<id>[\d]+)', 71 array( 72 'args' => array( 73 'id' => array( 74 'description' => __( 'Unique identifier for the comment.' ), 75 'type' => 'integer', 76 ), 77 ), 78 array( 79 'methods' => WP_REST_Server::READABLE, 80 'callback' => array( $this, 'get_item' ), 81 'permission_callback' => array( $this, 'get_item_permissions_check' ), 82 'args' => array( 83 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 84 'password' => array( 85 'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ), 86 'type' => 'string', 87 ), 88 ), 89 ), 90 array( 91 'methods' => WP_REST_Server::EDITABLE, 92 'callback' => array( $this, 'update_item' ), 93 'permission_callback' => array( $this, 'update_item_permissions_check' ), 94 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 95 ), 96 array( 97 'methods' => WP_REST_Server::DELETABLE, 98 'callback' => array( $this, 'delete_item' ), 99 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 100 'args' => array( 101 'force' => array( 102 'type' => 'boolean', 103 'default' => false, 104 'description' => __( 'Whether to bypass Trash and force deletion.' ), 105 ), 106 'password' => array( 107 'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ), 108 'type' => 'string', 109 ), 110 ), 111 ), 112 'schema' => array( $this, 'get_public_item_schema' ), 113 ) 114 ); 115 } 116 117 /** 118 * Checks if a given request has access to read comments. 119 * 120 * @since 4.7.0 121 * 122 * @param WP_REST_Request $request Full details about the request. 123 * @return true|WP_Error True if the request has read access, error object otherwise. 124 */ 125 public function get_items_permissions_check( $request ) { 126 $is_note = 'note' === $request['type']; 127 $is_edit_context = 'edit' === $request['context']; 128 129 if ( ! empty( $request['post'] ) ) { 130 foreach ( (array) $request['post'] as $post_id ) { 131 $post = get_post( $post_id ); 132 133 if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) { 134 return new WP_Error( 135 'rest_comment_not_supported_post_type', 136 __( 'Sorry, this post type does not support notes.' ), 137 array( 'status' => 403 ) 138 ); 139 } 140 141 if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) { 142 return new WP_Error( 143 'rest_cannot_read_post', 144 __( 'Sorry, you are not allowed to read the post for this comment.' ), 145 array( 'status' => rest_authorization_required_code() ) 146 ); 147 } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) { 148 return new WP_Error( 149 'rest_cannot_read', 150 __( 'Sorry, you are not allowed to read comments without a post.' ), 151 array( 'status' => rest_authorization_required_code() ) 152 ); 153 } 154 } 155 } 156 157 // Re-map edit context capabilities when requesting `note` for a post. 158 if ( $is_edit_context && $is_note && ! empty( $request['post'] ) ) { 159 foreach ( (array) $request['post'] as $post_id ) { 160 if ( ! current_user_can( 'edit_post', $post_id ) ) { 161 return new WP_Error( 162 'rest_forbidden_context', 163 __( 'Sorry, you are not allowed to edit comments.' ), 164 array( 'status' => rest_authorization_required_code() ) 165 ); 166 } 167 } 168 } elseif ( $is_edit_context && ! current_user_can( 'moderate_comments' ) ) { 169 return new WP_Error( 170 'rest_forbidden_context', 171 __( 'Sorry, you are not allowed to edit comments.' ), 172 array( 'status' => rest_authorization_required_code() ) 173 ); 174 } 175 176 if ( ! current_user_can( 'edit_posts' ) ) { 177 $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' ); 178 $forbidden_params = array(); 179 180 foreach ( $protected_params as $param ) { 181 if ( 'status' === $param ) { 182 if ( 'approve' !== $request[ $param ] ) { 183 $forbidden_params[] = $param; 184 } 185 } elseif ( 'type' === $param ) { 186 if ( 'comment' !== $request[ $param ] ) { 187 $forbidden_params[] = $param; 188 } 189 } elseif ( ! empty( $request[ $param ] ) ) { 190 $forbidden_params[] = $param; 191 } 192 } 193 194 if ( ! empty( $forbidden_params ) ) { 195 return new WP_Error( 196 'rest_forbidden_param', 197 /* translators: %s: List of forbidden parameters. */ 198 sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ), 199 array( 'status' => rest_authorization_required_code() ) 200 ); 201 } 202 } 203 204 return true; 205 } 206 207 /** 208 * Retrieves a list of comment items. 209 * 210 * @since 4.7.0 211 * 212 * @param WP_REST_Request $request Full details about the request. 213 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. 214 */ 215 public function get_items( $request ) { 216 217 // Retrieve the list of registered collection query parameters. 218 $registered = $this->get_collection_params(); 219 220 /* 221 * This array defines mappings between public API query parameters whose 222 * values are accepted as-passed, and their internal WP_Query parameter 223 * name equivalents (some are the same). Only values which are also 224 * present in $registered will be set. 225 */ 226 $parameter_mappings = array( 227 'author' => 'author__in', 228 'author_email' => 'author_email', 229 'author_exclude' => 'author__not_in', 230 'exclude' => 'comment__not_in', 231 'include' => 'comment__in', 232 'offset' => 'offset', 233 'order' => 'order', 234 'parent' => 'parent__in', 235 'parent_exclude' => 'parent__not_in', 236 'per_page' => 'number', 237 'post' => 'post__in', 238 'search' => 'search', 239 'status' => 'status', 240 'type' => 'type', 241 ); 242 243 $prepared_args = array(); 244 245 /* 246 * For each known parameter which is both registered and present in the request, 247 * set the parameter's value on the query $prepared_args. 248 */ 249 foreach ( $parameter_mappings as $api_param => $wp_param ) { 250 if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { 251 $prepared_args[ $wp_param ] = $request[ $api_param ]; 252 } 253 } 254 255 // Ensure certain parameter values default to empty strings. 256 foreach ( array( 'author_email', 'search' ) as $param ) { 257 if ( ! isset( $prepared_args[ $param ] ) ) { 258 $prepared_args[ $param ] = ''; 259 } 260 } 261 262 if ( isset( $registered['orderby'] ) ) { 263 $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); 264 } 265 266 $prepared_args['no_found_rows'] = false; 267 268 $prepared_args['update_comment_post_cache'] = true; 269 270 $prepared_args['date_query'] = array(); 271 272 // Set before into date query. Date query must be specified as an array of an array. 273 if ( isset( $registered['before'], $request['before'] ) ) { 274 $prepared_args['date_query'][0]['before'] = $request['before']; 275 } 276 277 // Set after into date query. Date query must be specified as an array of an array. 278 if ( isset( $registered['after'], $request['after'] ) ) { 279 $prepared_args['date_query'][0]['after'] = $request['after']; 280 } 281 282 if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { 283 $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); 284 } 285 286 $is_head_request = $request->is_method( 'HEAD' ); 287 if ( $is_head_request ) { 288 // Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination. 289 $prepared_args['fields'] = 'ids'; 290 // Disable priming comment meta for HEAD requests to improve performance. 291 $prepared_args['update_comment_meta_cache'] = false; 292 } 293 294 /** 295 * Filters WP_Comment_Query arguments when querying comments via the REST API. 296 * 297 * @since 4.7.0 298 * 299 * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ 300 * 301 * @param array $prepared_args Array of arguments for WP_Comment_Query. 302 * @param WP_REST_Request $request The REST API request. 303 */ 304 $prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request ); 305 306 $query = new WP_Comment_Query(); 307 $query_result = $query->query( $prepared_args ); 308 309 if ( ! $is_head_request ) { 310 $comments = array(); 311 312 foreach ( $query_result as $comment ) { 313 if ( ! $this->check_read_permission( $comment, $request ) ) { 314 continue; 315 } 316 317 $data = $this->prepare_item_for_response( $comment, $request ); 318 $comments[] = $this->prepare_response_for_collection( $data ); 319 } 320 } 321 322 $total_comments = (int) $query->found_comments; 323 $max_pages = (int) $query->max_num_pages; 324 325 if ( $total_comments < 1 ) { 326 // Out-of-bounds, run the query without pagination/offset to get the total count. 327 unset( $prepared_args['number'], $prepared_args['offset'] ); 328 329 $query = new WP_Comment_Query(); 330 $prepared_args['count'] = true; 331 $prepared_args['orderby'] = 'none'; 332 $prepared_args['update_comment_meta_cache'] = false; 333 334 $total_comments = $query->query( $prepared_args ); 335 $max_pages = (int) ceil( $total_comments / $request['per_page'] ); 336 } 337 338 $response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $comments ); 339 $response->header( 'X-WP-Total', $total_comments ); 340 $response->header( 'X-WP-TotalPages', $max_pages ); 341 342 $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); 343 344 if ( $request['page'] > 1 ) { 345 $prev_page = $request['page'] - 1; 346 347 if ( $prev_page > $max_pages ) { 348 $prev_page = $max_pages; 349 } 350 351 $prev_link = add_query_arg( 'page', $prev_page, $base ); 352 $response->link_header( 'prev', $prev_link ); 353 } 354 355 if ( $max_pages > $request['page'] ) { 356 $next_page = $request['page'] + 1; 357 $next_link = add_query_arg( 'page', $next_page, $base ); 358 359 $response->link_header( 'next', $next_link ); 360 } 361 362 return $response; 363 } 364 365 /** 366 * Get the comment, if the ID is valid. 367 * 368 * @since 4.7.2 369 * 370 * @param int $id Supplied ID. 371 * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. 372 */ 373 protected function get_comment( $id ) { 374 $error = new WP_Error( 375 'rest_comment_invalid_id', 376 __( 'Invalid comment ID.' ), 377 array( 'status' => 404 ) 378 ); 379 380 if ( (int) $id <= 0 ) { 381 return $error; 382 } 383 384 $id = (int) $id; 385 $comment = get_comment( $id ); 386 if ( empty( $comment ) ) { 387 return $error; 388 } 389 390 if ( ! empty( $comment->comment_post_ID ) ) { 391 $post = get_post( (int) $comment->comment_post_ID ); 392 393 if ( empty( $post ) ) { 394 return new WP_Error( 395 'rest_post_invalid_id', 396 __( 'Invalid post ID.' ), 397 array( 'status' => 404 ) 398 ); 399 } 400 } 401 402 return $comment; 403 } 404 405 /** 406 * Checks if a given request has access to read the comment. 407 * 408 * @since 4.7.0 409 * 410 * @param WP_REST_Request $request Full details about the request. 411 * @return true|WP_Error True if the request has read access for the item, error object otherwise. 412 */ 413 public function get_item_permissions_check( $request ) { 414 $comment = $this->get_comment( $request['id'] ); 415 if ( is_wp_error( $comment ) ) { 416 return $comment; 417 } 418 419 // Re-map edit context capabilities when requesting `note` type. 420 $edit_cap = 'note' === $comment->comment_type ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' ); 421 if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( ...$edit_cap ) ) { 422 return new WP_Error( 423 'rest_forbidden_context', 424 __( 'Sorry, you are not allowed to edit comments.' ), 425 array( 'status' => rest_authorization_required_code() ) 426 ); 427 } 428 429 $post = get_post( $comment->comment_post_ID ); 430 431 if ( ! $this->check_read_permission( $comment, $request ) ) { 432 return new WP_Error( 433 'rest_cannot_read', 434 __( 'Sorry, you are not allowed to read this comment.' ), 435 array( 'status' => rest_authorization_required_code() ) 436 ); 437 } 438 439 if ( $post && ! $this->check_read_post_permission( $post, $request ) ) { 440 return new WP_Error( 441 'rest_cannot_read_post', 442 __( 'Sorry, you are not allowed to read the post for this comment.' ), 443 array( 'status' => rest_authorization_required_code() ) 444 ); 445 } 446 447 return true; 448 } 449 450 /** 451 * Retrieves a comment. 452 * 453 * @since 4.7.0 454 * 455 * @param WP_REST_Request $request Full details about the request. 456 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. 457 */ 458 public function get_item( $request ) { 459 $comment = $this->get_comment( $request['id'] ); 460 if ( is_wp_error( $comment ) ) { 461 return $comment; 462 } 463 464 $data = $this->prepare_item_for_response( $comment, $request ); 465 $response = rest_ensure_response( $data ); 466 467 return $response; 468 } 469 470 /** 471 * Checks if a given request has access to create a comment. 472 * 473 * @since 4.7.0 474 * 475 * @param WP_REST_Request $request Full details about the request. 476 * @return true|WP_Error True if the request has access to create items, error object otherwise. 477 */ 478 public function create_item_permissions_check( $request ) { 479 $is_note = ! empty( $request['type'] ) && 'note' === $request['type']; 480 481 if ( ! is_user_logged_in() && $is_note ) { 482 return new WP_Error( 483 'rest_comment_login_required', 484 __( 'Sorry, you must be logged in to comment.' ), 485 array( 'status' => 401 ) 486 ); 487 } 488 489 if ( ! is_user_logged_in() ) { 490 if ( get_option( 'comment_registration' ) ) { 491 return new WP_Error( 492 'rest_comment_login_required', 493 __( 'Sorry, you must be logged in to comment.' ), 494 array( 'status' => 401 ) 495 ); 496 } 497 498 /** 499 * Filters whether comments can be created via the REST API without authentication. 500 * 501 * Enables creating comments for anonymous users. 502 * 503 * @since 4.7.0 504 * 505 * @param bool $allow_anonymous Whether to allow anonymous comments to 506 * be created. Default `false`. 507 * @param WP_REST_Request $request Request used to generate the 508 * response. 509 */ 510 $allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request ); 511 512 if ( ! $allow_anonymous ) { 513 return new WP_Error( 514 'rest_comment_login_required', 515 __( 'Sorry, you must be logged in to comment.' ), 516 array( 'status' => 401 ) 517 ); 518 } 519 } 520 521 // Limit who can set comment `author`, `author_ip` or `status` to anything other than the default. 522 if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) { 523 return new WP_Error( 524 'rest_comment_invalid_author', 525 /* translators: %s: Request parameter. */ 526 sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author' ), 527 array( 'status' => rest_authorization_required_code() ) 528 ); 529 } 530 531 if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) { 532 if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) { 533 return new WP_Error( 534 'rest_comment_invalid_author_ip', 535 /* translators: %s: Request parameter. */ 536 sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author_ip' ), 537 array( 'status' => rest_authorization_required_code() ) 538 ); 539 } 540 } 541 542 $edit_cap = $is_note ? array( 'edit_post', (int) $request['post'] ) : array( 'moderate_comments' ); 543 if ( isset( $request['status'] ) && ! current_user_can( ...$edit_cap ) ) { 544 return new WP_Error( 545 'rest_comment_invalid_status', 546 /* translators: %s: Request parameter. */ 547 sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'status' ), 548 array( 'status' => rest_authorization_required_code() ) 549 ); 550 } 551 552 if ( empty( $request['post'] ) ) { 553 return new WP_Error( 554 'rest_comment_invalid_post_id', 555 __( 'Sorry, you are not allowed to create this comment without a post.' ), 556 array( 'status' => 403 ) 557 ); 558 } 559 560 $post = get_post( (int) $request['post'] ); 561 562 if ( ! $post ) { 563 return new WP_Error( 564 'rest_comment_invalid_post_id', 565 __( 'Sorry, you are not allowed to create this comment without a post.' ), 566 array( 'status' => 403 ) 567 ); 568 } 569 570 if ( $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) { 571 return new WP_Error( 572 'rest_comment_not_supported_post_type', 573 __( 'Sorry, this post type does not support notes.' ), 574 array( 'status' => 403 ) 575 ); 576 } 577 578 if ( 'draft' === $post->post_status && ! $is_note ) { 579 return new WP_Error( 580 'rest_comment_draft_post', 581 __( 'Sorry, you are not allowed to create a comment on this post.' ), 582 array( 'status' => 403 ) 583 ); 584 } 585 586 if ( 'trash' === $post->post_status ) { 587 return new WP_Error( 588 'rest_comment_trash_post', 589 __( 'Sorry, you are not allowed to create a comment on this post.' ), 590 array( 'status' => 403 ) 591 ); 592 } 593 594 if ( ! $this->check_read_post_permission( $post, $request ) ) { 595 return new WP_Error( 596 'rest_cannot_read_post', 597 __( 'Sorry, you are not allowed to read the post for this comment.' ), 598 array( 'status' => rest_authorization_required_code() ) 599 ); 600 } 601 602 if ( ! comments_open( $post->ID ) && ! $is_note ) { 603 return new WP_Error( 604 'rest_comment_closed', 605 __( 'Sorry, comments are closed for this item.' ), 606 array( 'status' => 403 ) 607 ); 608 } 609 610 return true; 611 } 612 613 /** 614 * Creates a comment. 615 * 616 * @since 4.7.0 617 * 618 * @param WP_REST_Request $request Full details about the request. 619 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. 620 */ 621 public function create_item( $request ) { 622 if ( ! empty( $request['id'] ) ) { 623 return new WP_Error( 624 'rest_comment_exists', 625 __( 'Cannot create existing comment.' ), 626 array( 'status' => 400 ) 627 ); 628 } 629 630 // Do not allow comments to be created with a non-core type. 631 if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array( 'comment', 'note' ), true ) ) { 632 return new WP_Error( 633 'rest_invalid_comment_type', 634 __( 'Cannot create a comment with that type.' ), 635 array( 'status' => 400 ) 636 ); 637 } 638 639 $prepared_comment = $this->prepare_item_for_database( $request ); 640 if ( is_wp_error( $prepared_comment ) ) { 641 return $prepared_comment; 642 } 643 644 $prepared_comment['comment_type'] = $request['type']; 645 646 if ( ! isset( $prepared_comment['comment_content'] ) ) { 647 $prepared_comment['comment_content'] = ''; 648 } 649 650 // Include note metadata into check_is_comment_content_allowed. 651 if ( isset( $request['meta']['_wp_note_status'] ) ) { 652 $prepared_comment['meta']['_wp_note_status'] = $request['meta']['_wp_note_status']; 653 } 654 655 if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) { 656 return new WP_Error( 657 'rest_comment_content_invalid', 658 __( 'Invalid comment content.' ), 659 array( 'status' => 400 ) 660 ); 661 } 662 663 // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). 664 if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) { 665 $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true ); 666 } 667 668 // Set author data if the user's logged in. 669 $missing_author = empty( $prepared_comment['user_id'] ) 670 && empty( $prepared_comment['comment_author'] ) 671 && empty( $prepared_comment['comment_author_email'] ) 672 && empty( $prepared_comment['comment_author_url'] ); 673 674 if ( is_user_logged_in() && $missing_author ) { 675 $user = wp_get_current_user(); 676 677 $prepared_comment['user_id'] = $user->ID; 678 $prepared_comment['comment_author'] = $user->display_name; 679 $prepared_comment['comment_author_email'] = $user->user_email; 680 $prepared_comment['comment_author_url'] = $user->user_url; 681 } 682 683 // Honor the discussion setting that requires a name and email address of the comment author. 684 if ( get_option( 'require_name_email' ) ) { 685 if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) { 686 return new WP_Error( 687 'rest_comment_author_data_required', 688 __( 'Creating a comment requires valid author name and email values.' ), 689 array( 'status' => 400 ) 690 ); 691 } 692 } 693 694 if ( ! isset( $prepared_comment['comment_author_email'] ) ) { 695 $prepared_comment['comment_author_email'] = ''; 696 } 697 698 if ( ! isset( $prepared_comment['comment_author_url'] ) ) { 699 $prepared_comment['comment_author_url'] = ''; 700 } 701 702 if ( ! isset( $prepared_comment['comment_agent'] ) ) { 703 $prepared_comment['comment_agent'] = ''; 704 } 705 706 $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment ); 707 708 if ( is_wp_error( $check_comment_lengths ) ) { 709 $error_code = $check_comment_lengths->get_error_code(); 710 return new WP_Error( 711 $error_code, 712 __( 'Comment field exceeds maximum length allowed.' ), 713 array( 'status' => 400 ) 714 ); 715 } 716 717 $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true ); 718 719 if ( is_wp_error( $prepared_comment['comment_approved'] ) ) { 720 $error_code = $prepared_comment['comment_approved']->get_error_code(); 721 $error_message = $prepared_comment['comment_approved']->get_error_message(); 722 723 if ( 'comment_duplicate' === $error_code ) { 724 return new WP_Error( 725 $error_code, 726 $error_message, 727 array( 'status' => 409 ) 728 ); 729 } 730 731 if ( 'comment_flood' === $error_code ) { 732 return new WP_Error( 733 $error_code, 734 $error_message, 735 array( 'status' => 400 ) 736 ); 737 } 738 739 return $prepared_comment['comment_approved']; 740 } 741 742 /** 743 * Filters a comment before it is inserted via the REST API. 744 * 745 * Allows modification of the comment right before it is inserted via wp_insert_comment(). 746 * Returning a WP_Error value from the filter will short-circuit insertion and allow 747 * skipping further processing. 748 * 749 * @since 4.7.0 750 * @since 4.8.0 `$prepared_comment` can now be a WP_Error to short-circuit insertion. 751 * 752 * @param array|WP_Error $prepared_comment The prepared comment data for wp_insert_comment(). 753 * @param WP_REST_Request $request Request used to insert the comment. 754 */ 755 $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request ); 756 if ( is_wp_error( $prepared_comment ) ) { 757 return $prepared_comment; 758 } 759 760 $comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) ); 761 762 if ( ! $comment_id ) { 763 return new WP_Error( 764 'rest_comment_failed_create', 765 __( 'Creating comment failed.' ), 766 array( 'status' => 500 ) 767 ); 768 } 769 770 if ( isset( $request['status'] ) ) { 771 $this->handle_status_param( $request['status'], $comment_id ); 772 } 773 774 $comment = get_comment( $comment_id ); 775 776 /** 777 * Fires after a comment is created or updated via the REST API. 778 * 779 * @since 4.7.0 780 * 781 * @param WP_Comment $comment Inserted or updated comment object. 782 * @param WP_REST_Request $request Request object. 783 * @param bool $creating True when creating a comment, false 784 * when updating. 785 */ 786 do_action( 'rest_insert_comment', $comment, $request, true ); 787 788 $schema = $this->get_item_schema(); 789 790 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { 791 $meta_update = $this->meta->update_value( $request['meta'], $comment_id ); 792 793 if ( is_wp_error( $meta_update ) ) { 794 return $meta_update; 795 } 796 } 797 798 $fields_update = $this->update_additional_fields_for_object( $comment, $request ); 799 800 if ( is_wp_error( $fields_update ) ) { 801 return $fields_update; 802 } 803 804 $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; 805 $request->set_param( 'context', $context ); 806 807 /** 808 * Fires completely after a comment is created or updated via the REST API. 809 * 810 * @since 5.0.0 811 * 812 * @param WP_Comment $comment Inserted or updated comment object. 813 * @param WP_REST_Request $request Request object. 814 * @param bool $creating True when creating a comment, false 815 * when updating. 816 */ 817 do_action( 'rest_after_insert_comment', $comment, $request, true ); 818 819 $response = $this->prepare_item_for_response( $comment, $request ); 820 $response = rest_ensure_response( $response ); 821 822 $response->set_status( 201 ); 823 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) ); 824 825 return $response; 826 } 827 828 /** 829 * Checks if a given REST request has access to update a comment. 830 * 831 * @since 4.7.0 832 * 833 * @param WP_REST_Request $request Full details about the request. 834 * @return true|WP_Error True if the request has access to update the item, error object otherwise. 835 */ 836 public function update_item_permissions_check( $request ) { 837 $comment = $this->get_comment( $request['id'] ); 838 if ( is_wp_error( $comment ) ) { 839 return $comment; 840 } 841 842 if ( ! $this->check_edit_permission( $comment ) ) { 843 return new WP_Error( 844 'rest_cannot_edit', 845 __( 'Sorry, you are not allowed to edit this comment.' ), 846 array( 'status' => rest_authorization_required_code() ) 847 ); 848 } 849 850 return true; 851 } 852 853 /** 854 * Updates a comment. 855 * 856 * @since 4.7.0 857 * 858 * @param WP_REST_Request $request Full details about the request. 859 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. 860 */ 861 public function update_item( $request ) { 862 $comment = $this->get_comment( $request['id'] ); 863 if ( is_wp_error( $comment ) ) { 864 return $comment; 865 } 866 867 $id = $comment->comment_ID; 868 869 if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) { 870 return new WP_Error( 871 'rest_comment_invalid_type', 872 __( 'Sorry, you are not allowed to change the comment type.' ), 873 array( 'status' => 404 ) 874 ); 875 } 876 877 $prepared_args = $this->prepare_item_for_database( $request ); 878 879 if ( is_wp_error( $prepared_args ) ) { 880 return $prepared_args; 881 } 882 883 if ( ! empty( $prepared_args['comment_post_ID'] ) ) { 884 $post = get_post( $prepared_args['comment_post_ID'] ); 885 886 if ( empty( $post ) ) { 887 return new WP_Error( 888 'rest_comment_invalid_post_id', 889 __( 'Invalid post ID.' ), 890 array( 'status' => 403 ) 891 ); 892 } 893 } 894 895 if ( empty( $prepared_args ) && isset( $request['status'] ) ) { 896 // Only the comment status is being changed. 897 $change = $this->handle_status_param( $request['status'], $id ); 898 899 if ( ! $change ) { 900 return new WP_Error( 901 'rest_comment_failed_edit', 902 __( 'Updating comment status failed.' ), 903 array( 'status' => 500 ) 904 ); 905 } 906 } elseif ( ! empty( $prepared_args ) ) { 907 if ( is_wp_error( $prepared_args ) ) { 908 return $prepared_args; 909 } 910 if ( ! $this->check_is_comment_content_allowed( $prepared_args ) ) { 911 return new WP_Error( 912 'rest_comment_content_invalid', 913 __( 'Invalid comment content.' ), 914 array( 'status' => 400 ) 915 ); 916 } 917 918 $prepared_args['comment_ID'] = $id; 919 920 $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); 921 922 if ( is_wp_error( $check_comment_lengths ) ) { 923 $error_code = $check_comment_lengths->get_error_code(); 924 return new WP_Error( 925 $error_code, 926 __( 'Comment field exceeds maximum length allowed.' ), 927 array( 'status' => 400 ) 928 ); 929 } 930 931 $updated = wp_update_comment( wp_slash( (array) $prepared_args ), true ); 932 933 if ( is_wp_error( $updated ) ) { 934 return new WP_Error( 935 'rest_comment_failed_edit', 936 __( 'Updating comment failed.' ), 937 array( 'status' => 500 ) 938 ); 939 } 940 941 if ( isset( $request['status'] ) ) { 942 $this->handle_status_param( $request['status'], $id ); 943 } 944 } 945 946 $comment = get_comment( $id ); 947 948 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */ 949 do_action( 'rest_insert_comment', $comment, $request, false ); 950 951 $schema = $this->get_item_schema(); 952 953 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { 954 $meta_update = $this->meta->update_value( $request['meta'], $id ); 955 956 if ( is_wp_error( $meta_update ) ) { 957 return $meta_update; 958 } 959 } 960 961 $fields_update = $this->update_additional_fields_for_object( $comment, $request ); 962 963 if ( is_wp_error( $fields_update ) ) { 964 return $fields_update; 965 } 966 967 $request->set_param( 'context', 'edit' ); 968 969 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */ 970 do_action( 'rest_after_insert_comment', $comment, $request, false ); 971 972 $response = $this->prepare_item_for_response( $comment, $request ); 973 974 return rest_ensure_response( $response ); 975 } 976 977 /** 978 * Checks if a given request has access to delete a comment. 979 * 980 * @since 4.7.0 981 * 982 * @param WP_REST_Request $request Full details about the request. 983 * @return true|WP_Error True if the request has access to delete the item, error object otherwise. 984 */ 985 public function delete_item_permissions_check( $request ) { 986 $comment = $this->get_comment( $request['id'] ); 987 if ( is_wp_error( $comment ) ) { 988 return $comment; 989 } 990 991 if ( ! $this->check_edit_permission( $comment ) ) { 992 return new WP_Error( 993 'rest_cannot_delete', 994 __( 'Sorry, you are not allowed to delete this comment.' ), 995 array( 'status' => rest_authorization_required_code() ) 996 ); 997 } 998 return true; 999 } 1000 1001 /** 1002 * Deletes a comment. 1003 * 1004 * @since 4.7.0 1005 * 1006 * @param WP_REST_Request $request Full details about the request. 1007 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. 1008 */ 1009 public function delete_item( $request ) { 1010 $comment = $this->get_comment( $request['id'] ); 1011 if ( is_wp_error( $comment ) ) { 1012 return $comment; 1013 } 1014 1015 $force = isset( $request['force'] ) ? (bool) $request['force'] : false; 1016 1017 /** 1018 * Filters whether a comment can be trashed via the REST API. 1019 * 1020 * Return false to disable trash support for the comment. 1021 * 1022 * @since 4.7.0 1023 * 1024 * @param bool $supports_trash Whether the comment supports trashing. 1025 * @param WP_Comment $comment The comment object being considered for trashing support. 1026 */ 1027 $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment ); 1028 1029 $request->set_param( 'context', 'edit' ); 1030 1031 if ( $force ) { 1032 $previous = $this->prepare_item_for_response( $comment, $request ); 1033 $result = wp_delete_comment( $comment->comment_ID, true ); 1034 $response = new WP_REST_Response(); 1035 $response->set_data( 1036 array( 1037 'deleted' => true, 1038 'previous' => $previous->get_data(), 1039 ) 1040 ); 1041 } else { 1042 // If this type doesn't support trashing, error out. 1043 if ( ! $supports_trash ) { 1044 return new WP_Error( 1045 'rest_trash_not_supported', 1046 /* translators: %s: force=true */ 1047 sprintf( __( "The comment does not support trashing. Set '%s' to delete." ), 'force=true' ), 1048 array( 'status' => 501 ) 1049 ); 1050 } 1051 1052 if ( 'trash' === $comment->comment_approved ) { 1053 return new WP_Error( 1054 'rest_already_trashed', 1055 __( 'The comment has already been trashed.' ), 1056 array( 'status' => 410 ) 1057 ); 1058 } 1059 1060 $result = wp_trash_comment( $comment->comment_ID ); 1061 $comment = get_comment( $comment->comment_ID ); 1062 $response = $this->prepare_item_for_response( $comment, $request ); 1063 } 1064 1065 if ( ! $result ) { 1066 return new WP_Error( 1067 'rest_cannot_delete', 1068 __( 'The comment cannot be deleted.' ), 1069 array( 'status' => 500 ) 1070 ); 1071 } 1072 1073 /** 1074 * Fires after a comment is deleted via the REST API. 1075 * 1076 * @since 4.7.0 1077 * 1078 * @param WP_Comment $comment The deleted comment data. 1079 * @param WP_REST_Response $response The response returned from the API. 1080 * @param WP_REST_Request $request The request sent to the API. 1081 */ 1082 do_action( 'rest_delete_comment', $comment, $response, $request ); 1083 1084 return $response; 1085 } 1086 1087 /** 1088 * Prepares a single comment output for response. 1089 * 1090 * @since 4.7.0 1091 * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support. 1092 * 1093 * @param WP_Comment $item Comment object. 1094 * @param WP_REST_Request $request Request object. 1095 * @return WP_REST_Response Response object. 1096 */ 1097 public function prepare_item_for_response( $item, $request ) { 1098 // Restores the more descriptive, specific name for use within this method. 1099 $comment = $item; 1100 1101 // Don't prepare the response body for HEAD requests. 1102 if ( $request->is_method( 'HEAD' ) ) { 1103 /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */ 1104 return apply_filters( 'rest_prepare_comment', new WP_REST_Response( array() ), $comment, $request ); 1105 } 1106 1107 $fields = $this->get_fields_for_response( $request ); 1108 $data = array(); 1109 1110 if ( in_array( 'id', $fields, true ) ) { 1111 $data['id'] = (int) $comment->comment_ID; 1112 } 1113 1114 if ( in_array( 'post', $fields, true ) ) { 1115 $data['post'] = (int) $comment->comment_post_ID; 1116 } 1117 1118 if ( in_array( 'parent', $fields, true ) ) { 1119 $data['parent'] = (int) $comment->comment_parent; 1120 } 1121 1122 if ( in_array( 'author', $fields, true ) ) { 1123 $data['author'] = (int) $comment->user_id; 1124 } 1125 1126 if ( in_array( 'author_name', $fields, true ) ) { 1127 $data['author_name'] = $comment->comment_author; 1128 } 1129 1130 if ( in_array( 'author_email', $fields, true ) ) { 1131 $data['author_email'] = $comment->comment_author_email; 1132 } 1133 1134 if ( in_array( 'author_url', $fields, true ) ) { 1135 $data['author_url'] = $comment->comment_author_url; 1136 } 1137 1138 if ( in_array( 'author_ip', $fields, true ) ) { 1139 $data['author_ip'] = $comment->comment_author_IP; 1140 } 1141 1142 if ( in_array( 'author_user_agent', $fields, true ) ) { 1143 $data['author_user_agent'] = $comment->comment_agent; 1144 } 1145 1146 if ( in_array( 'date', $fields, true ) ) { 1147 $data['date'] = mysql_to_rfc3339( $comment->comment_date ); 1148 } 1149 1150 if ( in_array( 'date_gmt', $fields, true ) ) { 1151 $data['date_gmt'] = mysql_to_rfc3339( $comment->comment_date_gmt ); 1152 } 1153 1154 if ( in_array( 'content', $fields, true ) ) { 1155 $data['content'] = array( 1156 /** This filter is documented in wp-includes/comment-template.php */ 1157 'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment, array() ), 1158 'raw' => $comment->comment_content, 1159 ); 1160 } 1161 1162 if ( in_array( 'link', $fields, true ) ) { 1163 $data['link'] = get_comment_link( $comment ); 1164 } 1165 1166 if ( in_array( 'status', $fields, true ) ) { 1167 $data['status'] = $this->prepare_status_response( $comment->comment_approved ); 1168 } 1169 1170 if ( in_array( 'type', $fields, true ) ) { 1171 $data['type'] = get_comment_type( $comment->comment_ID ); 1172 } 1173 1174 if ( in_array( 'author_avatar_urls', $fields, true ) ) { 1175 $data['author_avatar_urls'] = rest_get_avatar_urls( $comment ); 1176 } 1177 1178 if ( in_array( 'meta', $fields, true ) ) { 1179 $data['meta'] = $this->meta->get_value( $comment->comment_ID, $request ); 1180 } 1181 1182 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 1183 $data = $this->add_additional_fields_to_object( $data, $request ); 1184 $data = $this->filter_response_by_context( $data, $context ); 1185 1186 // Wrap the data in a response object. 1187 $response = rest_ensure_response( $data ); 1188 1189 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 1190 $response->add_links( $this->prepare_links( $comment ) ); 1191 } 1192 1193 /** 1194 * Filters a comment returned from the REST API. 1195 * 1196 * Allows modification of the comment right before it is returned. 1197 * 1198 * @since 4.7.0 1199 * 1200 * @param WP_REST_Response $response The response object. 1201 * @param WP_Comment $comment The original comment object. 1202 * @param WP_REST_Request $request Request used to generate the response. 1203 */ 1204 return apply_filters( 'rest_prepare_comment', $response, $comment, $request ); 1205 } 1206 1207 /** 1208 * Prepares links for the request. 1209 * 1210 * @since 4.7.0 1211 * 1212 * @param WP_Comment $comment Comment object. 1213 * @return array Links for the given comment. 1214 */ 1215 protected function prepare_links( $comment ) { 1216 $links = array( 1217 'self' => array( 1218 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ), 1219 ), 1220 'collection' => array( 1221 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 1222 ), 1223 ); 1224 1225 if ( 0 !== (int) $comment->user_id ) { 1226 $links['author'] = array( 1227 'href' => rest_url( 'wp/v2/users/' . $comment->user_id ), 1228 'embeddable' => true, 1229 ); 1230 } 1231 1232 if ( 0 !== (int) $comment->comment_post_ID ) { 1233 $post = get_post( $comment->comment_post_ID ); 1234 $post_route = rest_get_route_for_post( $post ); 1235 1236 if ( ! empty( $post->ID ) && $post_route ) { 1237 $links['up'] = array( 1238 'href' => rest_url( $post_route ), 1239 'embeddable' => true, 1240 'post_type' => $post->post_type, 1241 ); 1242 } 1243 } 1244 1245 if ( 0 !== (int) $comment->comment_parent ) { 1246 $links['in-reply-to'] = array( 1247 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ), 1248 'embeddable' => true, 1249 ); 1250 } 1251 1252 // Only grab one comment to verify the comment has children. 1253 $comment_children = $comment->get_children( 1254 array( 1255 'count' => true, 1256 'orderby' => 'none', 1257 ) 1258 ); 1259 1260 if ( ! empty( $comment_children ) ) { 1261 $args = array( 1262 'parent' => $comment->comment_ID, 1263 ); 1264 1265 $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) ); 1266 1267 $links['children'] = array( 1268 'href' => $rest_url, 1269 'embeddable' => true, 1270 ); 1271 } 1272 1273 return $links; 1274 } 1275 1276 /** 1277 * Prepends internal property prefix to query parameters to match our response fields. 1278 * 1279 * @since 4.7.0 1280 * 1281 * @param string $query_param Query parameter. 1282 * @return string The normalized query parameter. 1283 */ 1284 protected function normalize_query_param( $query_param ) { 1285 $prefix = 'comment_'; 1286 1287 switch ( $query_param ) { 1288 case 'id': 1289 $normalized = $prefix . 'ID'; 1290 break; 1291 case 'post': 1292 $normalized = $prefix . 'post_ID'; 1293 break; 1294 case 'parent': 1295 $normalized = $prefix . 'parent'; 1296 break; 1297 case 'include': 1298 $normalized = 'comment__in'; 1299 break; 1300 default: 1301 $normalized = $prefix . $query_param; 1302 break; 1303 } 1304 1305 return $normalized; 1306 } 1307 1308 /** 1309 * Checks comment_approved to set comment status for single comment output. 1310 * 1311 * @since 4.7.0 1312 * 1313 * @param string $comment_approved Comment status. 1314 * @return string Comment status. 1315 */ 1316 protected function prepare_status_response( $comment_approved ) { 1317 1318 switch ( $comment_approved ) { 1319 case 'hold': 1320 case '0': 1321 $status = 'hold'; 1322 break; 1323 1324 case 'approve': 1325 case '1': 1326 $status = 'approved'; 1327 break; 1328 1329 case 'spam': 1330 case 'trash': 1331 default: 1332 $status = $comment_approved; 1333 break; 1334 } 1335 1336 return $status; 1337 } 1338 1339 /** 1340 * Prepares a single comment to be inserted into the database. 1341 * 1342 * @since 4.7.0 1343 * 1344 * @param WP_REST_Request $request Request object. 1345 * @return array|WP_Error Prepared comment, otherwise WP_Error object. 1346 */ 1347 protected function prepare_item_for_database( $request ) { 1348 $prepared_comment = array(); 1349 1350 /* 1351 * Allow the comment_content to be set via the 'content' or 1352 * the 'content.raw' properties of the Request object. 1353 */ 1354 if ( isset( $request['content'] ) && is_string( $request['content'] ) ) { 1355 $prepared_comment['comment_content'] = trim( $request['content'] ); 1356 } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) { 1357 $prepared_comment['comment_content'] = trim( $request['content']['raw'] ); 1358 } 1359 1360 if ( isset( $request['post'] ) ) { 1361 $prepared_comment['comment_post_ID'] = (int) $request['post']; 1362 } 1363 1364 if ( isset( $request['parent'] ) ) { 1365 $prepared_comment['comment_parent'] = $request['parent']; 1366 } 1367 1368 if ( isset( $request['author'] ) ) { 1369 $user = new WP_User( $request['author'] ); 1370 1371 if ( $user->exists() ) { 1372 $prepared_comment['user_id'] = $user->ID; 1373 $prepared_comment['comment_author'] = $user->display_name; 1374 $prepared_comment['comment_author_email'] = $user->user_email; 1375 $prepared_comment['comment_author_url'] = $user->user_url; 1376 } else { 1377 return new WP_Error( 1378 'rest_comment_author_invalid', 1379 __( 'Invalid comment author ID.' ), 1380 array( 'status' => 400 ) 1381 ); 1382 } 1383 } 1384 1385 if ( isset( $request['author_name'] ) ) { 1386 $prepared_comment['comment_author'] = $request['author_name']; 1387 } 1388 1389 if ( isset( $request['author_email'] ) ) { 1390 $prepared_comment['comment_author_email'] = $request['author_email']; 1391 } 1392 1393 if ( isset( $request['author_url'] ) ) { 1394 $prepared_comment['comment_author_url'] = $request['author_url']; 1395 } 1396 1397 if ( isset( $request['author_ip'] ) && current_user_can( 'moderate_comments' ) ) { 1398 $prepared_comment['comment_author_IP'] = $request['author_ip']; 1399 } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) { 1400 $prepared_comment['comment_author_IP'] = $_SERVER['REMOTE_ADDR']; 1401 } else { 1402 $prepared_comment['comment_author_IP'] = '127.0.0.1'; 1403 } 1404 1405 if ( ! empty( $request['author_user_agent'] ) ) { 1406 $prepared_comment['comment_agent'] = $request['author_user_agent']; 1407 } elseif ( $request->get_header( 'user_agent' ) ) { 1408 $prepared_comment['comment_agent'] = $request->get_header( 'user_agent' ); 1409 } 1410 1411 if ( ! empty( $request['date'] ) ) { 1412 $date_data = rest_get_date_with_gmt( $request['date'] ); 1413 1414 if ( ! empty( $date_data ) ) { 1415 list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data; 1416 } 1417 } elseif ( ! empty( $request['date_gmt'] ) ) { 1418 $date_data = rest_get_date_with_gmt( $request['date_gmt'], true ); 1419 1420 if ( ! empty( $date_data ) ) { 1421 list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data; 1422 } 1423 } 1424 1425 /** 1426 * Filters a comment added via the REST API after it is prepared for insertion into the database. 1427 * 1428 * Allows modification of the comment right after it is prepared for the database. 1429 * 1430 * @since 4.7.0 1431 * 1432 * @param array $prepared_comment The prepared comment data for `wp_insert_comment`. 1433 * @param WP_REST_Request $request The current request. 1434 */ 1435 return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request ); 1436 } 1437 1438 /** 1439 * Retrieves the comment's schema, conforming to JSON Schema. 1440 * 1441 * @since 4.7.0 1442 * 1443 * @return array 1444 */ 1445 public function get_item_schema() { 1446 if ( $this->schema ) { 1447 return $this->add_additional_fields_schema( $this->schema ); 1448 } 1449 1450 $schema = array( 1451 '$schema' => 'http://json-schema.org/draft-04/schema#', 1452 'title' => 'comment', 1453 'type' => 'object', 1454 'properties' => array( 1455 'id' => array( 1456 'description' => __( 'Unique identifier for the comment.' ), 1457 'type' => 'integer', 1458 'context' => array( 'view', 'edit', 'embed' ), 1459 'readonly' => true, 1460 ), 1461 'author' => array( 1462 'description' => __( 'The ID of the user object, if author was a user.' ), 1463 'type' => 'integer', 1464 'context' => array( 'view', 'edit', 'embed' ), 1465 ), 1466 'author_email' => array( 1467 'description' => __( 'Email address for the comment author.' ), 1468 'type' => 'string', 1469 'format' => 'email', 1470 'context' => array( 'edit' ), 1471 'arg_options' => array( 1472 'sanitize_callback' => array( $this, 'check_comment_author_email' ), 1473 'validate_callback' => null, // Skip built-in validation of 'email'. 1474 ), 1475 ), 1476 'author_ip' => array( 1477 'description' => __( 'IP address for the comment author.' ), 1478 'type' => 'string', 1479 'format' => 'ip', 1480 'context' => array( 'edit' ), 1481 ), 1482 'author_name' => array( 1483 'description' => __( 'Display name for the comment author.' ), 1484 'type' => 'string', 1485 'context' => array( 'view', 'edit', 'embed' ), 1486 'arg_options' => array( 1487 'sanitize_callback' => 'sanitize_text_field', 1488 ), 1489 ), 1490 'author_url' => array( 1491 'description' => __( 'URL for the comment author.' ), 1492 'type' => 'string', 1493 'format' => 'uri', 1494 'context' => array( 'view', 'edit', 'embed' ), 1495 ), 1496 'author_user_agent' => array( 1497 'description' => __( 'User agent for the comment author.' ), 1498 'type' => 'string', 1499 'context' => array( 'edit' ), 1500 'arg_options' => array( 1501 'sanitize_callback' => 'sanitize_text_field', 1502 ), 1503 ), 1504 'content' => array( 1505 'description' => __( 'The content for the comment.' ), 1506 'type' => 'object', 1507 'context' => array( 'view', 'edit', 'embed' ), 1508 'arg_options' => array( 1509 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 1510 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). 1511 ), 1512 'properties' => array( 1513 'raw' => array( 1514 'description' => __( 'Content for the comment, as it exists in the database.' ), 1515 'type' => 'string', 1516 'context' => array( 'edit' ), 1517 ), 1518 'rendered' => array( 1519 'description' => __( 'HTML content for the comment, transformed for display.' ), 1520 'type' => 'string', 1521 'context' => array( 'view', 'edit', 'embed' ), 1522 'readonly' => true, 1523 ), 1524 ), 1525 ), 1526 'date' => array( 1527 'description' => __( "The date the comment was published, in the site's timezone." ), 1528 'type' => 'string', 1529 'format' => 'date-time', 1530 'context' => array( 'view', 'edit', 'embed' ), 1531 ), 1532 'date_gmt' => array( 1533 'description' => __( 'The date the comment was published, as GMT.' ), 1534 'type' => 'string', 1535 'format' => 'date-time', 1536 'context' => array( 'view', 'edit' ), 1537 ), 1538 'link' => array( 1539 'description' => __( 'URL to the comment.' ), 1540 'type' => 'string', 1541 'format' => 'uri', 1542 'context' => array( 'view', 'edit', 'embed' ), 1543 'readonly' => true, 1544 ), 1545 'parent' => array( 1546 'description' => __( 'The ID for the parent of the comment.' ), 1547 'type' => 'integer', 1548 'context' => array( 'view', 'edit', 'embed' ), 1549 'default' => 0, 1550 ), 1551 'post' => array( 1552 'description' => __( 'The ID of the associated post object.' ), 1553 'type' => 'integer', 1554 'context' => array( 'view', 'edit' ), 1555 'default' => 0, 1556 ), 1557 'status' => array( 1558 'description' => __( 'State of the comment.' ), 1559 'type' => 'string', 1560 'context' => array( 'view', 'edit' ), 1561 'arg_options' => array( 1562 'sanitize_callback' => 'sanitize_key', 1563 ), 1564 ), 1565 'type' => array( 1566 'description' => __( 'Type of the comment.' ), 1567 'type' => 'string', 1568 'context' => array( 'view', 'edit', 'embed' ), 1569 'readonly' => true, 1570 'default' => 'comment', 1571 ), 1572 ), 1573 ); 1574 1575 if ( get_option( 'show_avatars' ) ) { 1576 $avatar_properties = array(); 1577 1578 $avatar_sizes = rest_get_avatar_sizes(); 1579 1580 foreach ( $avatar_sizes as $size ) { 1581 $avatar_properties[ $size ] = array( 1582 /* translators: %d: Avatar image size in pixels. */ 1583 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ), 1584 'type' => 'string', 1585 'format' => 'uri', 1586 'context' => array( 'embed', 'view', 'edit' ), 1587 ); 1588 } 1589 1590 $schema['properties']['author_avatar_urls'] = array( 1591 'description' => __( 'Avatar URLs for the comment author.' ), 1592 'type' => 'object', 1593 'context' => array( 'view', 'edit', 'embed' ), 1594 'readonly' => true, 1595 'properties' => $avatar_properties, 1596 ); 1597 } 1598 1599 $schema['properties']['meta'] = $this->meta->get_field_schema(); 1600 1601 $this->schema = $schema; 1602 1603 return $this->add_additional_fields_schema( $this->schema ); 1604 } 1605 1606 /** 1607 * Retrieves the query params for collections. 1608 * 1609 * @since 4.7.0 1610 * 1611 * @return array Comments collection parameters. 1612 */ 1613 public function get_collection_params() { 1614 $query_params = parent::get_collection_params(); 1615 1616 $query_params['context']['default'] = 'view'; 1617 1618 $query_params['after'] = array( 1619 'description' => __( 'Limit response to comments published after a given ISO8601 compliant date.' ), 1620 'type' => 'string', 1621 'format' => 'date-time', 1622 ); 1623 1624 $query_params['author'] = array( 1625 'description' => __( 'Limit result set to comments assigned to specific user IDs. Requires authorization.' ), 1626 'type' => 'array', 1627 'items' => array( 1628 'type' => 'integer', 1629 ), 1630 ); 1631 1632 $query_params['author_exclude'] = array( 1633 'description' => __( 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.' ), 1634 'type' => 'array', 1635 'items' => array( 1636 'type' => 'integer', 1637 ), 1638 ); 1639 1640 $query_params['author_email'] = array( 1641 'default' => null, 1642 'description' => __( 'Limit result set to that from a specific author email. Requires authorization.' ), 1643 'format' => 'email', 1644 'type' => 'string', 1645 ); 1646 1647 $query_params['before'] = array( 1648 'description' => __( 'Limit response to comments published before a given ISO8601 compliant date.' ), 1649 'type' => 'string', 1650 'format' => 'date-time', 1651 ); 1652 1653 $query_params['exclude'] = array( 1654 'description' => __( 'Ensure result set excludes specific IDs.' ), 1655 'type' => 'array', 1656 'items' => array( 1657 'type' => 'integer', 1658 ), 1659 'default' => array(), 1660 ); 1661 1662 $query_params['include'] = array( 1663 'description' => __( 'Limit result set to specific IDs.' ), 1664 'type' => 'array', 1665 'items' => array( 1666 'type' => 'integer', 1667 ), 1668 'default' => array(), 1669 ); 1670 1671 $query_params['offset'] = array( 1672 'description' => __( 'Offset the result set by a specific number of items.' ), 1673 'type' => 'integer', 1674 ); 1675 1676 $query_params['order'] = array( 1677 'description' => __( 'Order sort attribute ascending or descending.' ), 1678 'type' => 'string', 1679 'default' => 'desc', 1680 'enum' => array( 1681 'asc', 1682 'desc', 1683 ), 1684 ); 1685 1686 $query_params['orderby'] = array( 1687 'description' => __( 'Sort collection by comment attribute.' ), 1688 'type' => 'string', 1689 'default' => 'date_gmt', 1690 'enum' => array( 1691 'date', 1692 'date_gmt', 1693 'id', 1694 'include', 1695 'post', 1696 'parent', 1697 'type', 1698 ), 1699 ); 1700 1701 $query_params['parent'] = array( 1702 'default' => array(), 1703 'description' => __( 'Limit result set to comments of specific parent IDs.' ), 1704 'type' => 'array', 1705 'items' => array( 1706 'type' => 'integer', 1707 ), 1708 ); 1709 1710 $query_params['parent_exclude'] = array( 1711 'default' => array(), 1712 'description' => __( 'Ensure result set excludes specific parent IDs.' ), 1713 'type' => 'array', 1714 'items' => array( 1715 'type' => 'integer', 1716 ), 1717 ); 1718 1719 $query_params['post'] = array( 1720 'default' => array(), 1721 'description' => __( 'Limit result set to comments assigned to specific post IDs.' ), 1722 'type' => 'array', 1723 'items' => array( 1724 'type' => 'integer', 1725 ), 1726 ); 1727 1728 $query_params['status'] = array( 1729 'default' => 'approve', 1730 'description' => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ), 1731 'sanitize_callback' => 'sanitize_key', 1732 'type' => 'string', 1733 'validate_callback' => 'rest_validate_request_arg', 1734 ); 1735 1736 $query_params['type'] = array( 1737 'default' => 'comment', 1738 'description' => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ), 1739 'sanitize_callback' => 'sanitize_key', 1740 'type' => 'string', 1741 'validate_callback' => 'rest_validate_request_arg', 1742 ); 1743 1744 $query_params['password'] = array( 1745 'description' => __( 'The password for the post if it is password protected.' ), 1746 'type' => 'string', 1747 ); 1748 1749 /** 1750 * Filters REST API collection parameters for the comments controller. 1751 * 1752 * This filter registers the collection parameter, but does not map the 1753 * collection parameter to an internal WP_Comment_Query parameter. Use the 1754 * `rest_comment_query` filter to set WP_Comment_Query parameters. 1755 * 1756 * @since 4.7.0 1757 * 1758 * @param array $query_params JSON Schema-formatted collection parameters. 1759 */ 1760 return apply_filters( 'rest_comment_collection_params', $query_params ); 1761 } 1762 1763 /** 1764 * Sets the comment_status of a given comment object when creating or updating a comment. 1765 * 1766 * @since 4.7.0 1767 * 1768 * @param string|int $new_status New comment status. 1769 * @param int $comment_id Comment ID. 1770 * @return bool Whether the status was changed. 1771 */ 1772 protected function handle_status_param( $new_status, $comment_id ) { 1773 $old_status = wp_get_comment_status( $comment_id ); 1774 1775 if ( $new_status === $old_status ) { 1776 return false; 1777 } 1778 1779 switch ( $new_status ) { 1780 case 'approved': 1781 case 'approve': 1782 case '1': 1783 $changed = wp_set_comment_status( $comment_id, 'approve' ); 1784 break; 1785 case 'hold': 1786 case '0': 1787 $changed = wp_set_comment_status( $comment_id, 'hold' ); 1788 break; 1789 case 'spam': 1790 $changed = wp_spam_comment( $comment_id ); 1791 break; 1792 case 'unspam': 1793 $changed = wp_unspam_comment( $comment_id ); 1794 break; 1795 case 'trash': 1796 $changed = wp_trash_comment( $comment_id ); 1797 break; 1798 case 'untrash': 1799 $changed = wp_untrash_comment( $comment_id ); 1800 break; 1801 default: 1802 $changed = false; 1803 break; 1804 } 1805 1806 return $changed; 1807 } 1808 1809 /** 1810 * Checks if the post can be read. 1811 * 1812 * Correctly handles posts with the inherit status. 1813 * 1814 * @since 4.7.0 1815 * 1816 * @param WP_Post $post Post object. 1817 * @param WP_REST_Request $request Request data to check. 1818 * @return bool Whether post can be read. 1819 */ 1820 protected function check_read_post_permission( $post, $request ) { 1821 $post_type = get_post_type_object( $post->post_type ); 1822 1823 // Return false if custom post type doesn't exist 1824 if ( ! $post_type ) { 1825 return false; 1826 } 1827 1828 $posts_controller = $post_type->get_rest_controller(); 1829 1830 /* 1831 * Ensure the posts controller is specifically a WP_REST_Posts_Controller instance 1832 * before using methods specific to that controller. 1833 */ 1834 if ( ! $posts_controller instanceof WP_REST_Posts_Controller ) { 1835 $posts_controller = new WP_REST_Posts_Controller( $post->post_type ); 1836 } 1837 1838 $has_password_filter = false; 1839 1840 // Only check password if a specific post was queried for or a single comment 1841 $requested_post = ! empty( $request['post'] ) && ( ! is_array( $request['post'] ) || 1 === count( $request['post'] ) ); 1842 $requested_comment = ! empty( $request['id'] ); 1843 if ( ( $requested_post || $requested_comment ) && $posts_controller->can_access_password_content( $post, $request ) ) { 1844 add_filter( 'post_password_required', '__return_false' ); 1845 1846 $has_password_filter = true; 1847 } 1848 1849 if ( post_password_required( $post ) ) { 1850 $result = current_user_can( 'edit_post', $post->ID ); 1851 } else { 1852 $result = $posts_controller->check_read_permission( $post ); 1853 } 1854 1855 if ( $has_password_filter ) { 1856 remove_filter( 'post_password_required', '__return_false' ); 1857 } 1858 1859 return $result; 1860 } 1861 1862 /** 1863 * Checks if the comment can be read. 1864 * 1865 * @since 4.7.0 1866 * 1867 * @param WP_Comment $comment Comment object. 1868 * @param WP_REST_Request $request Request data to check. 1869 * @return bool Whether the comment can be read. 1870 */ 1871 protected function check_read_permission( $comment, $request ) { 1872 if ( ! empty( $comment->comment_post_ID ) ) { 1873 $post = get_post( $comment->comment_post_ID ); 1874 if ( $post ) { 1875 if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) { 1876 return true; 1877 } 1878 } 1879 } 1880 1881 if ( 0 === get_current_user_id() ) { 1882 return false; 1883 } 1884 1885 if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) { 1886 return false; 1887 } 1888 1889 if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) { 1890 return true; 1891 } 1892 1893 return current_user_can( 'edit_comment', $comment->comment_ID ); 1894 } 1895 1896 /** 1897 * Checks if a comment can be edited or deleted. 1898 * 1899 * @since 4.7.0 1900 * 1901 * @param WP_Comment $comment Comment object. 1902 * @return bool Whether the comment can be edited or deleted. 1903 */ 1904 protected function check_edit_permission( $comment ) { 1905 if ( 0 === (int) get_current_user_id() ) { 1906 return false; 1907 } 1908 1909 if ( current_user_can( 'moderate_comments' ) ) { 1910 return true; 1911 } 1912 1913 return current_user_can( 'edit_comment', $comment->comment_ID ); 1914 } 1915 1916 /** 1917 * Checks a comment author email for validity. 1918 * 1919 * Accepts either a valid email address or empty string as a valid comment 1920 * author email address. Setting the comment author email to an empty 1921 * string is allowed when a comment is being updated. 1922 * 1923 * @since 4.7.0 1924 * 1925 * @param string $value Author email value submitted. 1926 * @param WP_REST_Request $request Full details about the request. 1927 * @param string $param The parameter name. 1928 * @return string|WP_Error The sanitized email address, if valid, 1929 * otherwise an error. 1930 */ 1931 public function check_comment_author_email( $value, $request, $param ) { 1932 $email = (string) $value; 1933 if ( empty( $email ) ) { 1934 return $email; 1935 } 1936 1937 $check_email = rest_validate_request_arg( $email, $request, $param ); 1938 if ( is_wp_error( $check_email ) ) { 1939 return $check_email; 1940 } 1941 1942 return $email; 1943 } 1944 1945 /** 1946 * If empty comments are not allowed, checks if the provided comment content is not empty. 1947 * 1948 * @since 5.6.0 1949 * 1950 * @param array $prepared_comment The prepared comment data. 1951 * @return bool True if the content is allowed, false otherwise. 1952 */ 1953 protected function check_is_comment_content_allowed( $prepared_comment ) { 1954 if ( ! isset( $prepared_comment['comment_content'] ) ) { 1955 return true; 1956 } 1957 1958 $check = wp_parse_args( 1959 $prepared_comment, 1960 array( 1961 'comment_post_ID' => 0, 1962 'comment_author' => null, 1963 'comment_author_email' => null, 1964 'comment_author_url' => null, 1965 'comment_parent' => 0, 1966 'user_id' => 0, 1967 ) 1968 ); 1969 1970 /** This filter is documented in wp-includes/comment.php */ 1971 $allow_empty = apply_filters( 'allow_empty_comment', false, $check ); 1972 1973 if ( $allow_empty ) { 1974 return true; 1975 } 1976 1977 // Allow empty notes only when resolution metadata is valid. 1978 if ( 1979 isset( $check['comment_type'] ) && 1980 'note' === $check['comment_type'] && 1981 isset( $check['meta']['_wp_note_status'] ) && 1982 in_array( $check['meta']['_wp_note_status'], array( 'resolved', 'reopen' ), true ) 1983 ) { 1984 return true; 1985 } 1986 1987 /* 1988 * Do not allow a comment to be created with missing or empty 1989 * comment_content. See wp_handle_comment_submission(). 1990 */ 1991 return '' !== $check['comment_content']; 1992 } 1993 1994 /** 1995 * Check if post type supports notes. 1996 * 1997 * @param string $post_type Post type name. 1998 * @return bool True if post type supports notes, false otherwise. 1999 */ 2000 private function check_post_type_supports_notes( $post_type ) { 2001 $supports = get_all_post_type_supports( $post_type ); 2002 if ( ! isset( $supports['editor'] ) ) { 2003 return false; 2004 } 2005 if ( ! is_array( $supports['editor'] ) ) { 2006 return false; 2007 } 2008 foreach ( $supports['editor'] as $item ) { 2009 if ( ! empty( $item['notes'] ) ) { 2010 return true; 2011 } 2012 } 2013 return false; 2014 } 2015 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Oct 30 08:20:06 2025 | Cross-referenced by PHPXref |