Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 267
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_JSON_API_Update_Comment_Endpoint
0.00% covered (danger)
0.00%
0 / 150
0.00% covered (danger)
0.00%
0 / 5
6006
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 callback
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 new_comment
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
1406
 update_comment
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
650
 delete_comment
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Manage comments via the WordPress.com REST API.
4 *
5 * Endpoints;
6 * Create a comment on a post:                     /sites/%s/posts/%d/replies/new
7 * Create a comment as a reply to another comment: /sites/%s/comments/%d/replies/new
8 * Edit a comment:                                 /sites/%s/comments/%d
9 * Delete a comment:                               /sites/%s/comments/%d/delete
10 */
11
12use Automattic\Jetpack\Status;
13
14if ( ! defined( 'ABSPATH' ) ) {
15    exit( 0 );
16}
17
18new WPCOM_JSON_API_Update_Comment_Endpoint(
19    array(
20        'description'                          => 'Create a comment on a post.',
21        'group'                                => 'comments',
22        'stat'                                 => 'posts:1:replies:new',
23
24        'method'                               => 'POST',
25        'path'                                 => '/sites/%s/posts/%d/replies/new',
26        'path_labels'                          => array(
27            '$site'    => '(int|string) Site ID or domain',
28            '$post_ID' => '(int) The post ID',
29        ),
30
31        'request_format'                       => array(
32            // explicitly document all input.
33            'content' => '(HTML) The comment text.',
34        // @todo Should we open this up to unauthenticated requests too?
35        // 'author'    => '(author object) The author of the comment.',
36        ),
37
38        'pass_wpcom_user_details'              => true,
39
40        'allow_fallback_to_jetpack_blog_token' => true,
41
42        'example_request'                      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/843/replies/new/',
43        'example_request_data'                 => array(
44            'headers' => array(
45                'authorization' => 'Bearer YOUR_API_TOKEN',
46            ),
47            'body'    => array(
48                'content' => 'Your reply is very interesting. This is a reply.',
49            ),
50        ),
51    )
52);
53
54new WPCOM_JSON_API_Update_Comment_Endpoint(
55    array(
56        'description'                          => 'Create a comment as a reply to another comment.',
57        'group'                                => 'comments',
58        'stat'                                 => 'comments:1:replies:new',
59
60        'method'                               => 'POST',
61        'path'                                 => '/sites/%s/comments/%d/replies/new',
62        'path_labels'                          => array(
63            '$site'       => '(int|string) Site ID or domain',
64            '$comment_ID' => '(int) The comment ID',
65        ),
66
67        'request_format'                       => array(
68            'content' => '(HTML) The comment text.',
69        // @todo Should we open this up to unauthenticated requests too?
70        // 'author'    => '(author object) The author of the comment.',
71        ),
72
73        'pass_wpcom_user_details'              => true,
74
75        'allow_fallback_to_jetpack_blog_token' => true,
76
77        'example_request'                      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/29/replies/new',
78        'example_request_data'                 => array(
79            'headers' => array(
80                'authorization' => 'Bearer YOUR_API_TOKEN',
81            ),
82            'body'    => array(
83                'content' => 'This reply is very interesting. This is editing a comment reply via the API.',
84            ),
85        ),
86    )
87);
88
89new WPCOM_JSON_API_Update_Comment_Endpoint(
90    array(
91        'description'          => 'Edit a comment.',
92        'group'                => 'comments',
93        'stat'                 => 'comments:1:POST',
94
95        'method'               => 'POST',
96        'path'                 => '/sites/%s/comments/%d',
97        'path_labels'          => array(
98            '$site'       => '(int|string) Site ID or domain',
99            '$comment_ID' => '(int) The comment ID',
100        ),
101
102        'request_format'       => array(
103            'author'       => "(string) The comment author's name.",
104            'author_email' => "(string) The comment author's email.",
105            'author_url'   => "(string) The comment author's URL.",
106            'content'      => '(HTML) The comment text.',
107            'date'         => "(ISO 8601 datetime) The comment's creation time.",
108            'status'       => array(
109                'approved'   => 'Approve the comment.',
110                'unapproved' => 'Remove the comment from public view and send it to the moderation queue.',
111                'spam'       => 'Mark the comment as spam.',
112                'unspam'     => 'Unmark the comment as spam. Will attempt to set it to the previous status.',
113                'trash'      => 'Send a comment to the trash if trashing is enabled (see constant: EMPTY_TRASH_DAYS).',
114                'untrash'    => 'Untrash a comment. Only works when the comment is in the trash.',
115            ),
116        ),
117
118        'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/29',
119        'example_request_data' => array(
120            'headers' => array(
121                'authorization' => 'Bearer YOUR_API_TOKEN',
122            ),
123            'body'    => array(
124                'content' => 'This reply is now edited via the API.',
125                'status'  => 'approved',
126            ),
127        ),
128    )
129);
130
131new WPCOM_JSON_API_Update_Comment_Endpoint(
132    array(
133        'description'          => 'Delete a comment.',
134        'group'                => 'comments',
135        'stat'                 => 'comments:1:delete',
136
137        'method'               => 'POST',
138        'path'                 => '/sites/%s/comments/%d/delete',
139        'path_labels'          => array(
140            '$site'       => '(int|string) Site ID or domain',
141            '$comment_ID' => '(int) The comment ID',
142        ),
143
144        'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/$comment_ID/delete',
145        'example_request_data' => array(
146            'headers' => array(
147                'authorization' => 'Bearer YOUR_API_TOKEN',
148            ),
149        ),
150    )
151);
152
153/**
154 * Update comments endpoint class.
155 *
156 * @phan-constructor-used-for-side-effects
157 */
158class WPCOM_JSON_API_Update_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
159    /**
160     * WPCOM_JSON_API_Update_Comment_Endpoint constructor.
161     *
162     * @param array $args - Args.
163     */
164    public function __construct( $args ) {
165        parent::__construct( $args );
166        if ( $this->api->ends_with( $this->path, '/delete' ) ) {
167            $this->comment_object_format['status']['deleted'] = 'The comment has been deleted permanently.';
168        }
169    }
170
171    /**
172     * Update comment API callback.
173     *
174     * /sites/%s/posts/%d/replies/new    -> $blog_id, $post_id
175     * /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
176     * /sites/%s/comments/%d             -> $blog_id, $comment_id
177     * /sites/%s/comments/%d/delete      -> $blog_id, $comment_id
178     *
179     * @param string $path API path.
180     * @param int    $blog_id The blog ID.
181     * @param int    $object_id The object ID.
182     *
183     * @return bool|WP_Error|array
184     */
185    public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
186        if ( $this->api->ends_with( $path, '/new' ) ) {
187            $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ), false );
188        } else {
189            $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
190        }
191        if ( is_wp_error( $blog_id ) ) {
192            return $blog_id;
193        }
194
195        if ( $this->api->ends_with( $path, '/delete' ) ) {
196            return $this->delete_comment( $path, $blog_id, $object_id );
197        } elseif ( $this->api->ends_with( $path, '/new' ) ) {
198            if ( str_contains( $path, '/posts/' ) ) {
199                return $this->new_comment( $path, $blog_id, $object_id, 0 );
200            } else {
201                return $this->new_comment( $path, $blog_id, 0, $object_id );
202            }
203        }
204
205        return $this->update_comment( $path, $blog_id, $object_id );
206    }
207
208    /**
209     * Add a new comment to a post or as a reply to another comment.
210     *
211     * /sites/%s/posts/%d/replies/new    -> $blog_id, $post_id
212     * /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
213     *
214     * @param string $path API path.
215     * @param int    $blog_id The blog ID.
216     * @param int    $post_id The post ID.
217     * @param int    $comment_parent_id The comment parent ID.
218     *
219     * @return bool|WP_Error|array
220     */
221    public function new_comment( $path, $blog_id, $post_id, $comment_parent_id ) {
222        $comment_parent = null;
223        if ( ! $post_id ) {
224            $comment_parent = get_comment( $comment_parent_id );
225            if ( ! $comment_parent_id || ! $comment_parent || is_wp_error( $comment_parent ) ) {
226                return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
227            }
228
229            $post_id = $comment_parent->comment_post_ID;
230        }
231
232        $post = get_post( $post_id );
233        if ( ! $post || is_wp_error( $post ) ) {
234            return new WP_Error( 'unknown_post', 'Unknown post', 404 );
235        }
236
237        if (
238            ( new Status() )->is_private_site() &&
239            /**
240             * Filter allowing non-registered users on the site to comment.
241             *
242             * @module json-api
243             *
244             * @since 3.4.0
245             *
246             * @param bool is_user_member_of_blog() Is the user member of the site.
247             */
248            ! apply_filters( 'wpcom_json_api_user_is_member_of_blog', is_user_member_of_blog() ) &&
249            ! is_super_admin()
250        ) {
251            return new WP_Error( 'unauthorized', 'User cannot create comments', 403 );
252        }
253
254        if ( ! comments_open( $post->ID ) && ! current_user_can( 'edit_post', $post->ID ) ) {
255            return new WP_Error( 'unauthorized', 'Comments on this post are closed', 403 );
256        }
257
258        $can_view = $this->user_can_view_post( $post->ID );
259        if ( ! $can_view || is_wp_error( $can_view ) ) {
260            return $can_view;
261        }
262
263        $post_status = get_post_status_object( get_post_status( $post ) );
264        if ( ! $post_status->public && ! $post_status->private ) {
265            return new WP_Error( 'unauthorized', 'Comments on drafts are not allowed', 403 );
266        }
267
268        $args  = $this->query_args();
269        $input = $this->input();
270        if ( ! is_array( $input ) || ! $input || ! strlen( $input['content'] ) ) {
271            return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
272        }
273
274        $user = wp_get_current_user();
275        if ( ! $user || is_wp_error( $user ) || ! $user->ID ) {
276            $auth_required = false;
277            if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
278                $auth_required = true;
279            } elseif ( isset( $this->api->token_details['user'] ) ) {
280                $user = (object) $this->api->token_details['user'];
281                foreach ( array( 'display_name', 'user_email', 'user_url' ) as $user_datum ) {
282                    if ( ! isset( $user->$user_datum ) ) {
283                        $auth_required = true;
284                    }
285                }
286                if ( ! isset( $user->ID ) ) {
287                    $user->ID = 0;
288                }
289
290                $author = get_user_by( 'id', (int) $user->ID );
291                // If we have a user with an external ID saved, we can use it.
292                if (
293                    ! $auth_required
294                    && $user->ID
295                    && $author
296                ) {
297                    $user = $author;
298                }
299            } else {
300                $auth_required = true;
301            }
302
303            if ( $auth_required ) {
304                return new WP_Error( 'authorization_required', 'An active access token must be used to comment.', 403 );
305            }
306        }
307
308        $insert = array(
309            'comment_post_ID'      => $post->ID,
310            'user_ID'              => $user->ID,
311            'comment_author'       => $user->display_name,
312            'comment_author_email' => $user->user_email,
313            'comment_author_url'   => $user->user_url,
314            'comment_content'      => $input['content'],
315            'comment_parent'       => $comment_parent_id,
316            'comment_type'         => 'comment',
317        );
318
319        if ( $comment_parent_id ) {
320            if ( '0' === $comment_parent->comment_approved && current_user_can( 'edit_comment', $comment_parent->comment_ID ) ) {
321                wp_set_comment_status( $comment_parent->comment_ID, 'approve' );
322            }
323        }
324
325        $this->api->trap_wp_die( 'comment_failure' );
326        $comment_id = wp_new_comment( add_magic_quotes( $insert ) );
327        $this->api->trap_wp_die( null );
328
329        $return = $this->get_comment( $comment_id, $args['context'] );
330        if ( ! $return ) {
331            return new WP_Error( 400, __( 'Comment cache problem?', 'jetpack' ) );
332        }
333        if ( is_wp_error( $return ) ) {
334            return $return;
335        }
336
337        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
338        do_action( 'wpcom_json_api_objects', 'comments' );
339        return $return;
340    }
341
342    /**
343     * Update a comment.
344     *
345     * /sites/%s/comments/%d -> $blog_id, $comment_id
346     *
347     * @param string $path API path.
348     * @param int    $blog_id Blog ID.
349     * @param int    $comment_id Comment ID.
350     *
351     * @return bool|WP_Error|array
352     */
353    public function update_comment( $path, $blog_id, $comment_id ) {
354        $comment = get_comment( $comment_id );
355        if ( ! $comment || is_wp_error( $comment ) ) {
356            return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
357        }
358
359        if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) {
360            return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 );
361        }
362
363        $args  = $this->query_args();
364        $input = $this->input( false );
365        if ( ! is_array( $input ) || ! $input ) {
366            return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
367        }
368
369        $update = array();
370        foreach ( $input as $key => $value ) {
371            $update[ "comment_$key" ] = $value;
372        }
373
374        $comment_status = wp_get_comment_status( $comment->comment_ID );
375        if ( isset( $update['comment_status'] ) ) {
376            switch ( $update['comment_status'] ) {
377                case 'approved':
378                    if ( 'approve' !== $comment_status ) {
379                        wp_set_comment_status( $comment->comment_ID, 'approve' );
380                    }
381                    break;
382                case 'unapproved':
383                    if ( 'hold' !== $comment_status ) {
384                        wp_set_comment_status( $comment->comment_ID, 'hold' );
385                    }
386                    break;
387                case 'spam':
388                    if ( 'spam' !== $comment_status ) {
389                        wp_spam_comment( $comment->comment_ID );
390                    }
391                    break;
392                case 'unspam':
393                    if ( 'spam' === $comment_status ) {
394                        wp_unspam_comment( $comment->comment_ID );
395                    }
396                    break;
397                case 'trash':
398                    if ( ! EMPTY_TRASH_DAYS ) {
399                        return new WP_Error( 'trash_disabled', 'Cannot trash comment', 403 );
400                    }
401
402                    if ( 'trash' !== $comment_status ) {
403                        wp_trash_comment( $comment_id );
404                    }
405                    break;
406                case 'untrash':
407                    if ( 'trash' === $comment_status ) {
408                        wp_untrash_comment( $comment->comment_ID );
409                    }
410                    break;
411                default:
412                    $update['comment_approved'] = 1;
413                    break;
414            }
415            unset( $update['comment_status'] );
416        }
417
418        if ( ! empty( $update ) ) {
419            $update['comment_ID'] = $comment->comment_ID;
420            wp_update_comment( add_magic_quotes( $update ) );
421        }
422
423        $return = $this->get_comment( $comment->comment_ID, $args['context'] );
424        if ( ! $return || is_wp_error( $return ) ) {
425            return $return;
426        }
427
428        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
429        do_action( 'wpcom_json_api_objects', 'comments' );
430        return $return;
431    }
432
433    /**
434     * Delete a comment.
435     *
436     * /sites/%s/comments/%d/delete -> $blog_id, $comment_id
437     *
438     * @param string $path API path.
439     * @param int    $blog_id Blog ID.
440     * @param int    $comment_id Comment ID.
441     *
442     * @return bool|WP_Error|array
443     */
444    public function delete_comment( $path, $blog_id, $comment_id ) {
445        $comment = get_comment( $comment_id );
446        if ( ! $comment || is_wp_error( $comment ) ) {
447            return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
448        }
449
450        if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { // [sic] There is no delete_comment cap
451            return new WP_Error( 'unauthorized', 'User cannot delete comment', 403 );
452        }
453
454        $args   = $this->query_args();
455        $return = $this->get_comment( $comment->comment_ID, $args['context'] );
456        if ( ! $return || is_wp_error( $return ) ) {
457            return $return;
458        }
459
460        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
461        do_action( 'wpcom_json_api_objects', 'comments' );
462
463        wp_delete_comment( $comment->comment_ID );
464        $status = wp_get_comment_status( $comment->comment_ID );
465        if ( false === $status ) {
466            $return['status'] = 'deleted';
467            return $return;
468        }
469
470        return $this->get_comment( $comment->comment_ID, $args['context'] );
471    }
472}