PluginProbe
Gutenberg / 23.2.2
Gutenberg v23.2.2
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / compat / wordpress-6.9 / class-gutenberg-rest-comment-controller-6-9.php

class-gutenberg-rest-comment-controller-6-9.php in Gutenberg 23.2.2, at lib/compat/wordpress-6.9/class-gutenberg-rest-comment-controller-6-9.php

623 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A custom REST server for Gutenberg.
4 *
5 * @package gutenberg
6 * @since 6.9.0
7 */
8
9 // Create a new class that extends WP_REST_Comments_Controller
10 class Gutenberg_REST_Comment_Controller_6_9 extends WP_REST_Comments_Controller {
11
12 public function get_items_permissions_check( $request ) {
13 $is_note = 'note' === $request['type'];
14 $is_edit_context = 'edit' === $request['context'];
15
16 if ( ! empty( $request['post'] ) ) {
17 foreach ( (array) $request['post'] as $post_id ) {
18 $post = get_post( $post_id );
19
20 // Note: This is only relevant change for the backport.
21 if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
22 return new WP_Error(
23 'rest_comment_not_supported_post_type',
24 __( 'Sorry, this post type does not support notes.', 'gutenberg' ),
25 array( 'status' => 403 )
26 );
27 }
28
29 if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) {
30 return new WP_Error(
31 'rest_cannot_read_post',
32 __( 'Sorry, you are not allowed to read the post for this comment.', 'gutenberg' ),
33 array( 'status' => rest_authorization_required_code() )
34 );
35 } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
36 return new WP_Error(
37 'rest_cannot_read',
38 __( 'Sorry, you are not allowed to read comments without a post.', 'gutenberg' ),
39 array( 'status' => rest_authorization_required_code() )
40 );
41 }
42 }
43 }
44
45 // Re-map edit context capabilities when requesting `note` for a post.
46 // Note: This is only relevant change for the backport.
47 if ( $is_edit_context && $is_note && ! empty( $request['post'] ) ) {
48 foreach ( (array) $request['post'] as $post_id ) {
49 if ( ! current_user_can( 'edit_post', $post_id ) ) {
50 return new WP_Error(
51 'rest_forbidden_context',
52 __( 'Sorry, you are not allowed to edit comments.', 'gutenberg' ),
53 array( 'status' => rest_authorization_required_code() )
54 );
55 }
56 }
57 } elseif ( $is_edit_context && ! current_user_can( 'moderate_comments' ) ) {
58 return new WP_Error(
59 'rest_forbidden_context',
60 __( 'Sorry, you are not allowed to edit comments.', 'gutenberg' ),
61 array( 'status' => rest_authorization_required_code() )
62 );
63 }
64
65 if ( ! current_user_can( 'edit_posts' ) ) {
66 $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
67 $forbidden_params = array();
68
69 foreach ( $protected_params as $param ) {
70 if ( 'status' === $param ) {
71 if ( 'approve' !== $request[ $param ] ) {
72 $forbidden_params[] = $param;
73 }
74 } elseif ( 'type' === $param ) {
75 if ( 'comment' !== $request[ $param ] ) {
76 $forbidden_params[] = $param;
77 }
78 } elseif ( ! empty( $request[ $param ] ) ) {
79 $forbidden_params[] = $param;
80 }
81 }
82
83 if ( ! empty( $forbidden_params ) ) {
84 return new WP_Error(
85 'rest_forbidden_param',
86 /* translators: %s: List of forbidden parameters. */
87 sprintf( __( 'Query parameter not permitted: %s', 'gutenberg' ), implode( ', ', $forbidden_params ) ),
88 array( 'status' => rest_authorization_required_code() )
89 );
90 }
91 }
92
93 return true;
94 }
95
96 public function get_item_permissions_check( $request ) {
97 $comment = $this->get_comment( $request['id'] );
98 if ( is_wp_error( $comment ) ) {
99 return $comment;
100 }
101
102 // Re-map edit context capabilities when requesting `note` type.
103 // Note: This is only relevant change for the backport.
104 $edit_cap = 'note' === $comment->comment_type ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' );
105 if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( ...$edit_cap ) ) {
106 return new WP_Error(
107 'rest_forbidden_context',
108 __( 'Sorry, you are not allowed to edit comments.', 'gutenberg' ),
109 array( 'status' => rest_authorization_required_code() )
110 );
111 }
112
113 $post = get_post( $comment->comment_post_ID );
114
115 if ( ! $this->check_read_permission( $comment, $request ) ) {
116 return new WP_Error(
117 'rest_cannot_read',
118 __( 'Sorry, you are not allowed to read this comment.', 'gutenberg' ),
119 array( 'status' => rest_authorization_required_code() )
120 );
121 }
122
123 if ( $post && ! $this->check_read_post_permission( $post, $request ) ) {
124 return new WP_Error(
125 'rest_cannot_read_post',
126 __( 'Sorry, you are not allowed to read the post for this comment.', 'gutenberg' ),
127 array( 'status' => rest_authorization_required_code() )
128 );
129 }
130
131 return true;
132 }
133
134 public function create_item_permissions_check( $request ) {
135 $is_note = ! empty( $request['type'] ) && 'note' === $request['type'];
136
137 // Note: This is only relevant change for the backport.
138 if ( ! is_user_logged_in() && $is_note ) {
139 return new WP_Error(
140 'rest_comment_login_required',
141 __( 'Sorry, you must be logged in to comment.', 'gutenberg' ),
142 array( 'status' => 401 )
143 );
144 }
145
146 if ( ! is_user_logged_in() ) {
147 if ( get_option( 'comment_registration' ) ) {
148 return new WP_Error(
149 'rest_comment_login_required',
150 __( 'Sorry, you must be logged in to comment.', 'gutenberg' ),
151 array( 'status' => 401 )
152 );
153 }
154
155 /**
156 * Filters whether comments can be created via the REST API without authentication.
157 *
158 * Enables creating comments for anonymous users.
159 *
160 * @since 4.7.0
161 *
162 * @param bool $allow_anonymous Whether to allow anonymous comments to
163 * be created. Default `false`.
164 * @param WP_REST_Request $request Request used to generate the
165 * response.
166 */
167 $allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request );
168
169 if ( ! $allow_anonymous ) {
170 return new WP_Error(
171 'rest_comment_login_required',
172 __( 'Sorry, you must be logged in to comment.', 'gutenberg' ),
173 array( 'status' => 401 )
174 );
175 }
176 }
177
178 // Limit who can set comment `author`, `author_ip` or `status` to anything other than the default.
179 if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
180 return new WP_Error(
181 'rest_comment_invalid_author',
182 /* translators: %s: Request parameter. */
183 sprintf( __( "Sorry, you are not allowed to edit '%s' for comments.", 'gutenberg' ), 'author' ),
184 array( 'status' => rest_authorization_required_code() )
185 );
186 }
187
188 if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) {
189 if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) {
190 return new WP_Error(
191 'rest_comment_invalid_author_ip',
192 /* translators: %s: Request parameter. */
193 sprintf( __( "Sorry, you are not allowed to edit '%s' for comments.", 'gutenberg' ), 'author_ip' ),
194 array( 'status' => rest_authorization_required_code() )
195 );
196 }
197 }
198
199 // Note: This is only relevant change for the backport.
200 $edit_cap = $is_note ? array( 'edit_post', (int) $request['post'] ) : array( 'moderate_comments' );
201 if ( isset( $request['status'] ) && ! current_user_can( ...$edit_cap ) ) {
202 return new WP_Error(
203 'rest_comment_invalid_status',
204 /* translators: %s: Request parameter. */
205 sprintf( __( "Sorry, you are not allowed to edit '%s' for comments.", 'gutenberg' ), 'status' ),
206 array( 'status' => rest_authorization_required_code() )
207 );
208 }
209
210 if ( empty( $request['post'] ) ) {
211 return new WP_Error(
212 'rest_comment_invalid_post_id',
213 __( 'Sorry, you are not allowed to create this comment without a post.', 'gutenberg' ),
214 array( 'status' => 403 )
215 );
216 }
217
218 $post = get_post( (int) $request['post'] );
219
220 if ( ! $post ) {
221 return new WP_Error(
222 'rest_comment_invalid_post_id',
223 __( 'Sorry, you are not allowed to create this comment without a post.', 'gutenberg' ),
224 array( 'status' => 403 )
225 );
226 }
227
228 // Note: This is only relevant change for the backport.
229 if ( $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
230 return new WP_Error(
231 'rest_comment_not_supported_post_type',
232 __( 'Sorry, this post type does not support notes.', 'gutenberg' ),
233 array( 'status' => 403 )
234 );
235 }
236
237 // Note: This is only relevant change for the backport.
238 if ( 'draft' === $post->post_status && ! $is_note ) {
239 return new WP_Error(
240 'rest_comment_draft_post',
241 __( 'Sorry, you are not allowed to create a comment on this post.', 'gutenberg' ),
242 array( 'status' => 403 )
243 );
244 }
245
246 if ( 'trash' === $post->post_status ) {
247 return new WP_Error(
248 'rest_comment_trash_post',
249 __( 'Sorry, you are not allowed to create a comment on this post.', 'gutenberg' ),
250 array( 'status' => 403 )
251 );
252 }
253
254 if ( ! $this->check_read_post_permission( $post, $request ) ) {
255 return new WP_Error(
256 'rest_cannot_read_post',
257 __( 'Sorry, you are not allowed to read the post for this comment.', 'gutenberg' ),
258 array( 'status' => rest_authorization_required_code() )
259 );
260 }
261
262 // Note: This is only relevant change for the backport.
263 if ( ! comments_open( $post->ID ) && ! $is_note ) {
264 return new WP_Error(
265 'rest_comment_closed',
266 __( 'Sorry, comments are closed for this item.', 'gutenberg' ),
267 array( 'status' => 403 )
268 );
269 }
270
271 return true;
272 }
273
274 /**
275 * Creates a comment.
276 *
277 * @since 4.7.0
278 *
279 * @param WP_REST_Request $request Full details about the request.
280 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
281 */
282 public function create_item( $request ) {
283 // This code is copied exactly from (core file name) except for sectioned marked with the comment.
284 // '// Note: This is only relevant change for the backport.'
285 if ( ! empty( $request['id'] ) ) {
286 return new WP_Error(
287 'rest_comment_exists',
288 __( 'Cannot create existing comment.', 'gutenberg' ),
289 array( 'status' => 400 )
290 );
291 }
292
293 // Note: Removes non-default comment type check for the backport.
294 // Do not allow comments to be created with a non-core type.
295 if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array( 'comment', 'note' ), true ) ) {
296 return new WP_Error(
297 'rest_invalid_comment_type',
298 __( 'Cannot create a comment with that type.', 'gutenberg' ),
299 array( 'status' => 400 )
300 );
301 }
302
303 $prepared_comment = $this->prepare_item_for_database( $request );
304 if ( is_wp_error( $prepared_comment ) ) {
305 return $prepared_comment;
306 }
307
308 $prepared_comment['comment_type'] = $request['type'];
309
310 if ( ! isset( $prepared_comment['comment_content'] ) ) {
311 $prepared_comment['comment_content'] = '';
312 }
313
314 // Include note metadata into check_is_comment_content_allowed [backport].
315 if ( isset( $request['meta']['_wp_note_status'] ) ) {
316 $prepared_comment['meta']['_wp_note_status'] = $request['meta']['_wp_note_status'];
317 }
318
319 if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) {
320 return new WP_Error(
321 'rest_comment_content_invalid',
322 __( 'Invalid comment content.', 'gutenberg' ),
323 array( 'status' => 400 )
324 );
325 }
326
327 // Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
328 if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
329 $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
330 }
331
332 // Set author data if the user's logged in.
333 $missing_author = empty( $prepared_comment['user_id'] )
334 && empty( $prepared_comment['comment_author'] )
335 && empty( $prepared_comment['comment_author_email'] )
336 && empty( $prepared_comment['comment_author_url'] );
337
338 if ( is_user_logged_in() && $missing_author ) {
339 $user = wp_get_current_user();
340
341 $prepared_comment['user_id'] = $user->ID;
342 $prepared_comment['comment_author'] = $user->display_name;
343 $prepared_comment['comment_author_email'] = $user->user_email;
344 $prepared_comment['comment_author_url'] = $user->user_url;
345 }
346
347 // Honor the discussion setting that requires a name and email address of the comment author.
348 if ( get_option( 'require_name_email' ) ) {
349 if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) {
350 return new WP_Error(
351 'rest_comment_author_data_required',
352 __( 'Creating a comment requires valid author name and email values.', 'gutenberg' ),
353 array( 'status' => 400 )
354 );
355 }
356 }
357
358 if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
359 $prepared_comment['comment_author_email'] = '';
360 }
361
362 if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
363 $prepared_comment['comment_author_url'] = '';
364 }
365
366 if ( ! isset( $prepared_comment['comment_agent'] ) ) {
367 $prepared_comment['comment_agent'] = '';
368 }
369
370 $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment );
371
372 if ( is_wp_error( $check_comment_lengths ) ) {
373 $error_code = $check_comment_lengths->get_error_code();
374 return new WP_Error(
375 $error_code,
376 __( 'Comment field exceeds maximum length allowed.', 'gutenberg' ),
377 array( 'status' => 400 )
378 );
379 }
380
381 // Don't check for duplicates or flooding for notes.
382 $prepared_comment['comment_approved'] =
383 'note' === $prepared_comment['comment_type'] ?
384 '1' :
385 wp_allow_comment( $prepared_comment, true );
386
387 if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
388 $error_code = $prepared_comment['comment_approved']->get_error_code();
389 $error_message = $prepared_comment['comment_approved']->get_error_message();
390
391 if ( 'comment_duplicate' === $error_code ) {
392 return new WP_Error(
393 $error_code,
394 $error_message,
395 array( 'status' => 409 )
396 );
397 }
398
399 if ( 'comment_flood' === $error_code ) {
400 return new WP_Error(
401 $error_code,
402 $error_message,
403 array( 'status' => 400 )
404 );
405 }
406
407 return $prepared_comment['comment_approved'];
408 }
409
410 /**
411 * Filters a comment before it is inserted via the REST API.
412 *
413 * Allows modification of the comment right before it is inserted via wp_insert_comment().
414 * Returning a WP_Error value from the filter will short-circuit insertion and allow
415 * skipping further processing.
416 *
417 * @since 4.7.0
418 * @since 4.8.0 `$prepared_comment` can now be a WP_Error to short-circuit insertion.
419 *
420 * @param array|WP_Error $prepared_comment The prepared comment data for wp_insert_comment().
421 * @param WP_REST_Request $request Request used to insert the comment.
422 */
423 $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
424 if ( is_wp_error( $prepared_comment ) ) {
425 return $prepared_comment;
426 }
427
428 $comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) );
429
430 if ( ! $comment_id ) {
431 return new WP_Error(
432 'rest_comment_failed_create',
433 __( 'Creating comment failed.', 'gutenberg' ),
434 array( 'status' => 500 )
435 );
436 }
437
438 if ( isset( $request['status'] ) ) {
439 $this->handle_status_param( $request['status'], $comment_id );
440 }
441
442 $comment = get_comment( $comment_id );
443
444 /**
445 * Fires after a comment is created or updated via the REST API.
446 *
447 * @since 4.7.0
448 *
449 * @param WP_Comment $comment Inserted or updated comment object.
450 * @param WP_REST_Request $request Request object.
451 * @param bool $creating True when creating a comment, false
452 * when updating.
453 */
454 do_action( 'rest_insert_comment', $comment, $request, true );
455
456 $schema = $this->get_item_schema();
457
458 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
459 $meta_update = $this->meta->update_value( $request['meta'], $comment_id );
460
461 if ( is_wp_error( $meta_update ) ) {
462 return $meta_update;
463 }
464 }
465
466 $fields_update = $this->update_additional_fields_for_object( $comment, $request );
467
468 if ( is_wp_error( $fields_update ) ) {
469 return $fields_update;
470 }
471
472 $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
473 $request->set_param( 'context', $context );
474
475 /**
476 * Fires completely after a comment is created or updated via the REST API.
477 *
478 * @since 5.0.0
479 *
480 * @param WP_Comment $comment Inserted or updated comment object.
481 * @param WP_REST_Request $request Request object.
482 * @param bool $creating True when creating a comment, false
483 * when updating.
484 */
485 do_action( 'rest_after_insert_comment', $comment, $request, true );
486
487 $response = $this->prepare_item_for_response( $comment, $request );
488 $response = rest_ensure_response( $response );
489
490 $response->set_status( 201 );
491 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );
492
493 return $response;
494 }
495
496 /**
497 * Check if post type supports block comments.
498 *
499 * @param string $post_type Post type name.
500 * @return bool True if post type supports block comments, false otherwise.
501 */
502 private function check_post_type_supports_notes( $post_type ) {
503 $supports = get_all_post_type_supports( $post_type );
504 if ( ! isset( $supports['editor'] ) ) {
505 return false;
506 }
507 if ( ! is_array( $supports['editor'] ) ) {
508 return false;
509 }
510 foreach ( $supports['editor'] as $item ) {
511 if ( ! empty( $item['notes'] ) ) {
512 return true;
513 }
514 }
515 return false;
516 }
517
518 /**
519 * Prepares links for the request.
520 *
521 * @since 4.7.0
522 *
523 * @param WP_Comment $comment Comment object.
524 * @return array Links for the given comment.
525 */
526 protected function prepare_links( $comment ) {
527 $links = parent::prepare_links( $comment );
528
529 // Embedding children for notes requires `type` and `status` inheritance.
530 // Note: This is only relevant change for the backport.
531 if ( isset( $links['children'] ) && 'note' === $comment->comment_type ) {
532 $args = array(
533 'parent' => $comment->comment_ID,
534 'type' => $comment->comment_type,
535 'status' => 'all',
536 );
537
538 $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
539
540 $links['children'] = array(
541 'href' => $rest_url,
542 'embeddable' => true,
543 );
544 }
545
546 return $links;
547 }
548
549 /**
550 * Override the schema to change `type` property.
551 *
552 * @return array
553 */
554 public function get_item_schema() {
555 $schema = parent::get_item_schema();
556 $schema['properties']['type'] = array(
557 'description' => __( 'Type of the comment.', 'gutenberg' ),
558 'type' => 'string',
559 'context' => array( 'view', 'edit', 'embed' ),
560 'readonly' => true,
561 );
562
563 return $schema;
564 }
565
566 /**
567 * If empty comments are not allowed, checks if the provided comment content is not empty.
568 *
569 * @since 6.9.0
570 *
571 * @param array $prepared_comment The prepared comment data.
572 * @return bool True if the content is allowed, false otherwise.
573 */
574 protected function check_is_comment_content_allowed( $prepared_comment ) {
575 if ( ! isset( $prepared_comment['comment_content'] ) ) {
576 return true;
577 }
578
579 $check = wp_parse_args(
580 $prepared_comment,
581 array(
582 'comment_post_ID' => 0,
583 'comment_author' => null,
584 'comment_author_email' => null,
585 'comment_author_url' => null,
586 'comment_parent' => 0,
587 'user_id' => 0,
588 )
589 );
590
591 /** This filter is documented in wp-includes/comment.php */
592 $allow_empty = apply_filters( 'allow_empty_comment', false, $check );
593
594 if ( $allow_empty ) {
595 return true;
596 }
597
598 // Allow empty block comments only when resolution metadata is valid [backport].
599 if (
600 isset( $check['comment_type'] ) &&
601 'note' === $check['comment_type'] &&
602 isset( $check['meta']['_wp_note_status'] ) &&
603 in_array( $check['meta']['_wp_note_status'], array( 'resolved', 'reopen' ), true )
604 ) {
605 return true;
606 }
607
608 /*
609 * Do not allow a comment to be created with missing or empty
610 * comment_content. See wp_handle_comment_submission().
611 */
612 return '' !== $check['comment_content'];
613 }
614 }
615
616 add_action(
617 'rest_api_init',
618 function () {
619 $controller = new Gutenberg_REST_Comment_Controller_6_9();
620 $controller->register_routes();
621 }
622 );
623