Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 187
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 2
WPCOM_JSON_API_List_Comments_Walker
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 2
56
0.00% covered (danger)
0.00%
0 / 1
 start_el
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 display_element
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
WPCOM_JSON_API_List_Comments_Endpoint
0.00% covered (danger)
0.00%
0 / 144
0.00% covered (danger)
0.00%
0 / 2
2352
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
2
 callback
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 1
2256
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3if ( ! defined( 'ABSPATH' ) ) {
4    exit( 0 );
5}
6
7/**
8 * Comments Walker Class.
9 */
10class WPCOM_JSON_API_List_Comments_Walker extends Walker {
11
12    /**
13     * Tree type.
14     *
15     * @var string
16     */
17    public $tree_type = 'comment';
18
19    /**
20     * Database fields.
21     *
22     * @var array
23     */
24    public $db_fields = array(
25        'parent' => 'comment_parent',
26        'id'     => 'comment_ID',
27    );
28
29    /**
30     * Start the element output.
31     *
32     * @param array  $output - the output.
33     * @param object $object - the object.
34     * @param int    $depth - depth.
35     * @param array  $args - the arguments.
36     * @param int    $current_object_id - the object ID.
37     */
38    public function start_el( &$output, $object, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
39        if ( ! is_array( $output ) ) {
40            $output = array();
41        }
42
43        $output[] = $object->comment_ID;
44    }
45
46    /**
47     * Taken from WordPress's Walker_Comment::display_element()
48     *
49     * This function is designed to enhance Walker::display_element() to
50     * display children of higher nesting levels than selected inline on
51     * the highest depth level displayed. This prevents them being orphaned
52     * at the end of the comment list.
53     *
54     * Example: max_depth = 2, with 5 levels of nested content.
55     * 1
56     *  1.1
57     *    1.1.1
58     *    1.1.1.1
59     *    1.1.1.1.1
60     *    1.1.2
61     *    1.1.2.1
62     * 2
63     *  2.2
64     *
65     * @see Walker_Comment::display_element()
66     * @see Walker::display_element()
67     * @see wp_list_comments()
68     *
69     * @param object $element — Data object.
70     * @param array  $children_elements - List of elements to continue traversing (passed by reference).
71     * @param int    $max_depth — Max depth to traverse.
72     * @param int    $depth — Depth of current element.
73     * @param array  $args — An array of arguments.
74     * @param string $output — Used to append additional content (passed by reference).
75     */
76    public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
77
78        if ( ! $element ) {
79            return;
80        }
81
82        $id_field = $this->db_fields['id'];
83        $id       = $element->$id_field;
84
85        parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
86
87        // If we're at the max depth, and the current element still has children, loop over those and display them at this level
88        // This is to prevent them being orphaned to the end of the list.
89        if ( $max_depth <= $depth + 1 && isset( $children_elements[ $id ] ) ) {
90            foreach ( $children_elements[ $id ] as $child ) {
91                $this->display_element( $child, $children_elements, $max_depth, $depth, $args, $output );
92            }
93
94            unset( $children_elements[ $id ] );
95        }
96    }
97}
98
99new WPCOM_JSON_API_List_Comments_Endpoint(
100    array(
101        'description'                          => 'Get a list of recent comments.',
102        'group'                                => 'comments',
103        'stat'                                 => 'comments',
104
105        'method'                               => 'GET',
106        'path'                                 => '/sites/%s/comments/',
107        'path_labels'                          => array(
108            '$site' => '(int|string) Site ID or domain',
109        ),
110
111        'allow_fallback_to_jetpack_blog_token' => true,
112
113        'example_request'                      => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/?number=2',
114    )
115);
116
117new WPCOM_JSON_API_List_Comments_Endpoint(
118    array(
119        'description'                          => 'Get a list of recent comments on a post.',
120        'group'                                => 'comments',
121        'stat'                                 => 'posts:1:replies',
122
123        'method'                               => 'GET',
124        'path'                                 => '/sites/%s/posts/%d/replies/',
125        'path_labels'                          => array(
126            '$site'    => '(int|string) Site ID or domain',
127            '$post_ID' => '(int) The post ID',
128        ),
129
130        'allow_fallback_to_jetpack_blog_token' => true,
131
132        'example_request'                      => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7/replies/?number=2',
133    )
134);
135
136/**
137 * List comment endpoint.
138 *
139 * /sites/%s/comments/            -> $blog_id
140 * /sites/%s/posts/%d/replies/    -> $blog_id, $post_id
141 * /sites/%s/comments/%d/replies/ -> $blog_id, $comment_id
142 *
143 * @todo permissions
144 *
145 * @phan-constructor-used-for-side-effects
146 */
147class WPCOM_JSON_API_List_Comments_Endpoint extends WPCOM_JSON_API_Comment_Endpoint { // phpcs:ignore
148
149    /**
150     * The response format.
151     *
152     * @var array
153     */
154    public $response_format = array(
155        'found'    => '(int) The total number of comments found that match the request (ignoring limits, offsets, and pagination).',
156        'site_ID'  => '(int) The site ID',
157        'comments' => '(array:comment) An array of comment objects.',
158    );
159
160    /**
161     * Constructor function.
162     *
163     * @param array $args - the arguments.
164     */
165    public function __construct( $args ) {
166        parent::__construct( $args );
167        $this->query = array_merge(
168            $this->query,
169            array(
170                'number'            => '(int=20) The number of comments to return.  Limit: 100. When using hierarchical=1, number refers to the number of top-level comments returned.',
171                'offset'            => '(int=0) 0-indexed offset. Not available if using hierarchical=1.',
172                'page'              => '(int) Return the Nth 1-indexed page of comments.  Takes precedence over the <code>offset</code> parameter. When using hierarchical=1, pagination is a bit different.  See the note on the number parameter.',
173                'order'             => array(
174                    'DESC' => 'Return comments in descending order from newest to oldest.',
175                    'ASC'  => 'Return comments in ascending order from oldest to newest.',
176                ),
177                'hierarchical'      => array(
178                    'false' => '',
179                    'true'  => '(BETA) Order the comment list hierarchically.',
180                ),
181                'after'             => '(ISO 8601 datetime) Return comments dated on or after the specified datetime. Not available if using hierarchical=1.',
182                'before'            => '(ISO 8601 datetime) Return comments dated on or before the specified datetime. Not available if using hierarchical=1.',
183                'type'              => array(
184                    'any'       => 'Return all comments regardless of type.',
185                    'comment'   => 'Return only regular comments.',
186                    'trackback' => 'Return only trackbacks.',
187                    'pingback'  => 'Return only pingbacks.',
188                    'pings'     => 'Return both trackbacks and pingbacks.',
189                ),
190                'status'            => array(
191                    'approved'   => 'Return only approved comments.',
192                    'unapproved' => 'Return only comments in the moderation queue.',
193                    'spam'       => 'Return only comments marked as spam.',
194                    'trash'      => 'Return only comments in the trash.',
195                    'all'        => 'Return comments of all statuses.',
196                ),
197                'author_wpcom_data' => array(
198                    'false' => 'Do not add wpcom_id and wpcom_login fields to comment author responses (default)',
199                    'true'  => 'Add wpcom_id and wpcom_login fields to comment author responses',
200                ),
201            )
202        );
203    }
204
205    /**
206     * The callback.
207     *
208     * @param string $path - the path.
209     * @param int    $blog_id - the blog ID.
210     * @param int    $object_id - the object ID.
211     */
212    public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
213        $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
214        if ( is_wp_error( $blog_id ) ) {
215            return $blog_id;
216        }
217
218        $args = $this->query_args();
219
220        if ( $args['number'] < 1 ) {
221            $args['number'] = 20;
222        } elseif ( 100 < $args['number'] ) {
223            return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
224        }
225
226        if ( str_contains( $path, '/posts/' ) ) {
227            // We're looking for comments of a particular post.
228            $post_id    = $object_id;
229            $comment_id = 0;
230        } else {
231            // We're looking for comments for the whole blog, or replies to a single comment.
232            $comment_id = $object_id;
233            $post_id    = 0;
234        }
235
236        // We can't efficiently get the number of replies to a single comment.
237        $count = false;
238        $found = -1;
239
240        if ( ! $comment_id ) {
241            // We can get comment counts for the whole site or for a single post, but only for certain queries.
242            if ( 'any' === $args['type'] && ! isset( $args['after'] ) && ! isset( $args['before'] ) ) {
243                $count = $this->api->wp_count_comments( $post_id );
244            }
245        }
246
247        switch ( $args['status'] ) {
248            case 'approved':
249                $status = 'approve';
250                if ( $count ) {
251                    $found = $count->approved;
252                }
253                break;
254            default:
255                if ( ! current_user_can( 'edit_posts' ) ) {
256                    return new WP_Error( 'unauthorized', 'User cannot read non-approved comments', 403 );
257                }
258                if ( 'unapproved' === $args['status'] ) {
259                    $status       = 'hold';
260                    $count_status = 'moderated';
261                } elseif ( 'all' === $args['status'] ) {
262                    $status       = 'all';
263                    $count_status = 'total_comments';
264                } else {
265                    $status       = $args['status'];
266                    $count_status = $args['status'];
267                }
268                if ( $count ) {
269                    $found = $count->$count_status;
270                }
271        }
272
273        /** This filter is documented in class.json-api.php */
274        $exclude = apply_filters(
275            'jetpack_api_exclude_comment_types',
276            array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
277        );
278
279        $query = array(
280            'order'        => $args['order'],
281            'type'         => 'any' === $args['type'] ? false : $args['type'],
282            'status'       => $status,
283            'type__not_in' => $exclude,
284        );
285
286        if ( isset( $args['page'] ) ) {
287            if ( $args['page'] < 1 ) {
288                $args['page'] = 1;
289            }
290        } elseif ( $args['offset'] < 0 ) {
291            $args['offset'] = 0;
292        }
293
294        if ( ! $args['hierarchical'] ) {
295            $query['number'] = $args['number'];
296
297            if ( isset( $args['page'] ) ) {
298                $query['offset'] = ( $args['page'] - 1 ) * $args['number'];
299            } else {
300                $query['offset'] = $args['offset'];
301            }
302
303            $is_before = isset( $args['before_gmt'] );
304            $is_after  = isset( $args['after_gmt'] );
305
306            if ( $is_before || $is_after ) {
307                $query['date_query'] = array(
308                    'column'    => 'comment_date_gmt',
309                    'inclusive' => true,
310                );
311
312                if ( $is_before ) {
313                    $query['date_query']['before'] = $args['before_gmt'];
314                }
315
316                if ( $is_after ) {
317                    $query['date_query']['after'] = $args['after_gmt'];
318                }
319            }
320        }
321        if ( $args['hierarchical'] && $found > 5000 ) {
322            // Massive comment thread found; don't pre-load comment metadata to reduce memory used.
323            $query['update_comment_meta_cache'] = false;
324        }
325
326        if ( $post_id ) {
327            $post = get_post( $post_id );
328            if ( ! $post || is_wp_error( $post ) ) {
329                return new WP_Error( 'unknown_post', 'Unknown post', 404 );
330            }
331            $query['post_id'] = $post->ID;
332            if ( $this->api->ends_with( $this->path, '/replies' ) ) {
333                $query['parent'] = 0;
334            }
335        } elseif ( $comment_id ) {
336            $comment = get_comment( $comment_id );
337            if ( ! $comment || is_wp_error( $comment ) ) {
338                return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
339            }
340            $query['parent'] = $comment_id;
341        }
342
343        $comments = get_comments( $query );
344
345        if ( $args['hierarchical'] ) {
346            $walker      = new WPCOM_JSON_API_List_Comments_Walker();
347            $comment_ids = $walker->paged_walk( $comments, get_option( 'thread_comments_depth', -1 ), isset( $args['page'] ) ? $args['page'] : 1, $args['number'] );
348            if ( ! empty( $comment_ids ) ) {
349                $comments = array_map( 'get_comment', $comment_ids );
350            }
351        }
352
353        $return = array();
354
355        foreach ( array_keys( $this->response_format ) as $key ) {
356            switch ( $key ) {
357                case 'found':
358                    $return[ $key ] = (int) $found;
359                    break;
360                case 'site_ID':
361                    $return[ $key ] = (int) $blog_id;
362                    break;
363                case 'comments':
364                    $return_comments = array();
365                    if ( ! empty( $comments ) ) {
366                        foreach ( $comments as $comment ) {
367                            $the_comment = $this->get_comment( $comment->comment_ID, $args['context'] );
368                            if ( $the_comment && ! is_wp_error( $the_comment ) ) {
369                                $return_comments[] = $the_comment;
370                            }
371                        }
372                    }
373
374                    if ( $return_comments ) {
375                        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
376                        do_action( 'wpcom_json_api_objects', 'comments', count( $return_comments ) );
377                    }
378
379                    $return[ $key ] = $return_comments;
380                    break;
381            }
382        }
383
384        return $return;
385    }
386}