Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.78% covered (warning)
69.78%
194 / 278
33.33% covered (danger)
33.33%
5 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_REST_API_V3_Endpoint_Blogging_Prompts
70.55% covered (warning)
70.55%
194 / 275
33.33% covered (danger)
33.33%
5 / 15
172.97
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 register_routes
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
1
 get_items
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
8.45
 get_item
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
6.80
 modify_query
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 map_date_query
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 filter_sql
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
8
 prepare_item_for_response
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
240
 is_in_bloganuary
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_bloganuary_id
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 prepare_date_response
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 get_collection_params
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
4
 get_item_schema
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
1 / 1
1
 permissions_check
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
8.05
 build_answering_users_sample
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Blogging prompts endpoint for wpcom/v3.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
9
10if ( ! defined( 'ABSPATH' ) ) {
11    exit( 0 );
12}
13
14/**
15 * REST API endpoint wpcom/v3/sites/%s/blogging-prompts.
16 */
17class WPCOM_REST_API_V3_Endpoint_Blogging_Prompts extends WP_REST_Posts_Controller {
18
19    use WPCOM_REST_API_Proxy_Request;
20
21    const TEMPLATE_BLOG_ID = 205876834;
22
23    /**
24     * Whether the endpoint is running on wpcom, or not.
25     *
26     * @var bool
27     */
28    public $is_wpcom;
29
30    /**
31     * Day of the year, from 1 to 366, and 0 representing no query.
32     *
33     * Used with yearless dates like `--12-20`, to get prompts by month and day, regardless of year.
34     *
35     * @var integer
36     */
37    public $day_of_year_query = 0;
38
39    /**
40     * A year used to force one prompt per day for a specific year.
41     *
42     * @var integer
43     */
44    public $force_year = 0;
45
46    /**
47     * Constructor.
48     */
49    public function __construct() {
50        $this->post_type                       = 'post';
51        $this->base_api_path                   = 'wpcom';
52        $this->version                         = 'v3';
53        $this->namespace                       = $this->base_api_path . '/' . $this->version;
54        $this->rest_base                       = 'blogging-prompts';
55        $this->wpcom_is_wpcom_only_endpoint    = true;
56        $this->wpcom_is_site_specific_endpoint = true;
57        $this->is_wpcom                        = defined( 'IS_WPCOM' ) && IS_WPCOM;
58
59        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
60    }
61
62    /**
63     * Registers the routes for blogging prompts.
64     *
65     * @see register_rest_route()
66     */
67    public function register_routes() {
68        register_rest_route(
69            $this->namespace,
70            '/' . $this->rest_base,
71            array(
72                array(
73                    'methods'             => WP_REST_Server::READABLE,
74                    'callback'            => array( $this, 'get_items' ),
75                    'permission_callback' => array( $this, 'permissions_check' ),
76                    'args'                => $this->get_collection_params(),
77                ),
78                'schema' => array( $this, 'get_item_schema' ),
79            )
80        );
81
82        register_rest_route(
83            $this->namespace,
84            '/' . $this->rest_base . '/(?P<id>[\d]+)',
85            array(
86                'args'   => array(
87                    'id' => array(
88                        'description' => __( 'Unique identifier for the prompt.', 'jetpack' ),
89                        'type'        => 'integer',
90                    ),
91                ),
92                array(
93                    'methods'             => WP_REST_Server::READABLE,
94                    'callback'            => array( $this, 'get_item' ),
95                    'permission_callback' => array( $this, 'permissions_check' ),
96                ),
97                'schema' => array( $this, 'get_item_schema' ),
98            )
99        );
100    }
101
102    /**
103     * Retrieves a collection of blogging prompts.
104     *
105     * @param WP_REST_Request $request Full details about the request.
106     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
107     */
108    public function get_items( $request ) {
109        if ( ! $this->is_wpcom ) {
110            return $this->proxy_request_to_wpcom( $request, '', 'user', true );
111        }
112
113        if ( $request->get_param( 'force_year' ) ) {
114            $this->force_year = $request->get_param( 'force_year' );
115        }
116
117        switch_to_blog( self::TEMPLATE_BLOG_ID );
118        add_action( 'pre_get_posts', array( $this, 'modify_query' ) );
119        add_filter( 'posts_clauses', array( $this, 'filter_sql' ), 10, 2 );
120        $items = parent::get_items( $request );
121        remove_filter( 'posts_clauses', array( $this, 'filter_sql' ) );
122        remove_action( 'pre_get_posts', array( $this, 'modify_query' ) );
123        restore_current_blog();
124
125        // Reset so a later query in the same request can never inherit this state.
126        $this->day_of_year_query = 0;
127
128        return $items;
129    }
130
131    /**
132     * Retrieves a single blogging prompt.
133     *
134     * @param WP_REST_Request $request Full details about the request.
135     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
136     */
137    public function get_item( $request ) {
138        if ( ! $this->is_wpcom ) {
139            return $this->proxy_request_to_wpcom( $request, $request->get_param( 'id' ), 'user', true );
140        }
141
142        if ( $request->get_param( 'force_year' ) ) {
143            $this->force_year = $request->get_param( 'force_year' );
144        }
145
146        switch_to_blog( self::TEMPLATE_BLOG_ID );
147        $item = parent::get_item( $request );
148        restore_current_blog();
149
150        return $item;
151    }
152
153    /**
154     * Modify the posts query using the {@see 'pre_get_posts'} hook.
155     *
156     * @param WP_Query $wp_query The WP_Query instance (passed by reference).
157     */
158    public function modify_query( &$wp_query ) {
159        // parent::get_items() renders each prompt while this hook is still attached,
160        // and rendering can spawn nested WP_Querys (e.g. Gutenberg's wp_global_styles
161        // lookup), so only ever touch the prompts query itself.
162        if ( $this->post_type !== $wp_query->get( 'post_type' ) ) {
163            return;
164        }
165
166        $date_query = $wp_query->get( 'date_query' );
167
168        if ( is_array( $date_query ) ) {
169            $wp_query->set( 'date_query', array_map( array( $this, 'map_date_query' ), $date_query ) );
170            // Mark the query so filter_sql() only modifies this one.
171            $wp_query->set( 'jetpack_blogging_prompts', true );
172        }
173    }
174
175    /**
176     * Modify date_query items when querying prompts.
177     *
178     * @link https://developer.wordpress.org/reference/classes/WP_Query/#date-parameters
179     *
180     * @param  array|string|null $date_query Date query.
181     * @return array|string|null             Modified date query.
182     */
183    public function map_date_query( $date_query ) {
184        if ( isset( $date_query['after'] ) ) {
185            // `after` date queries should include posts on the specified date, so force `inclusive` queries.
186            $date_query['inclusive'] = true;
187
188            // If using a "year-less" date, e.g. `--03-16`, override the date_query, and prepare to modify sql manually.
189            // `after` should be a date string when making API requests, rather than an array.
190            if ( is_string( $date_query['after'] ) && str_starts_with( $date_query['after'], '-' ) ) {
191                $date = date_create_from_format( '--m-d', $date_query['after'] );
192
193                if ( false !== $date ) {
194                    // PHP day of the year starts with 0; normalize to match SQL DAYOFTHEYEAR which starts with 1.
195                    $this->day_of_year_query = $date->format( 'z' ) + 1;
196
197                    // Unset the date query, since we'll by modifying the SQL manually.
198                    return null;
199                }
200            }
201        }
202
203        return $date_query;
204    }
205
206    /**
207     * Modify post sql for custom date ordering using the {@see 'posts_clauses'} hook.
208     *
209     * @param array         $clauses SQL clauses for the current query.
210     * @param WP_Query|null $query   The WP_Query instance being filtered.
211     * @return array                 Modified SQL clauses.
212     */
213    public function filter_sql( $clauses, $query = null ) {
214        global $wpdb;
215        if ( ! $query instanceof WP_Query || ! $query->get( 'jetpack_blogging_prompts' ) ) {
216            return $clauses;
217        }
218        if ( $this->day_of_year_query > 0 ) {
219            $day  = $this->day_of_year_query;
220            $year = $this->force_year ? $this->force_year : wp_date( 'Y' );
221
222            // Grab the current sort order, `ASC` or `DESC`, so we can reuse it.
223            $exploded = explode( ' ', $clauses['orderby'] );
224            $order    = end( $exploded );
225
226            // Calculate the day of year for each prompt, from 1 to 366, but use the current year so that prompts published
227            // during leap years have the correct day for non-leap years.
228            $fields = $clauses['fields'] . $wpdb->prepare( ", DAYOFYEAR(CONCAT(%d, DATE_FORMAT({$wpdb->posts}.post_date, '-%%m-%%d'))) AS day_of_year", $year );
229
230            // When it's not a leap year, exclude posts used for Feb 29th. DAYOFYEAR will return null for Feb 29th on non-leap years.
231            $where = $clauses['where'] . $wpdb->prepare( " AND DAYOFYEAR(CONCAT(%d, DATE_FORMAT({$wpdb->posts}.post_date, '-%%m-%%d'))) IS NOT NULL", $year );
232
233            // Order posts regardless of year: get a list of posts for each day,
234            // starting with the query date through the end of the year, then from the start of the year through the day before.
235            $orderby = $wpdb->prepare(
236                'CASE ' .
237                    'WHEN day_of_year < %d ' .
238                    // Push posts from the beginning of the year until the day before to the end.
239                    'THEN day_of_year + 366 ' .
240                    // Otherwise order posts from the query date through the end of the year.
241                    'ELSE day_of_year ' .
242                'END' .
243                // Sort posts for the same day by year, in asc or desc order.
244                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- order string cannot be escaped.
245                ", YEAR({$wpdb->posts}.post_date) " . ( 'DESC' === $order ? 'DESC' : 'ASC' ),
246                $day
247            );
248
249            if ( $this->force_year ) {
250                // If we're forcing the year, group by day of year, so that we only get one prompt per day.
251                $clauses['groupby'] = 'day_of_year';
252
253                // Ensure we get either to newest or oldest prompt for each day of the year, depending on the sort order.
254                // GROUP BY runs and collects the prompts for each day of the year before ORDER BY is run, so we first need to use MAX/MIN on post_date
255                // to find the most recent/oldest prompt for each day and join the results to the main query.
256                $clauses['join'] = $wpdb->prepare(
257                    'INNER JOIN (' .
258                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL function cannot be escaped.
259                        'SELECT ' . ( 'DESC' === $order ? 'MAX' : 'MIN' ) . "({$wpdb->posts}.post_date) AS post_date, DAYOFYEAR(CONCAT(%d, DATE_FORMAT(post_date, '-%%m-%%d'))) AS day_of_year " .
260                        "FROM {$wpdb->posts} " .
261                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- reuses unmodified existing clause.
262                        "WHERE 1=1 {$clauses['where']} " .
263                        'GROUP BY day_of_year' .
264                    ") AS newest_prompts ON {$wpdb->posts}.post_date = newest_prompts.post_date",
265                    $year
266                );
267            }
268
269            $clauses['fields']  = $fields;
270            $clauses['where']   = $where;
271            $clauses['orderby'] = $orderby;
272        }
273
274        return $clauses;
275    }
276
277    /**
278     * Prepares a single blogging prompt output for response.
279     *
280     * @param WP_Post         $prompt  Post object.
281     * @param WP_REST_Request $request Request object.
282     * @return WP_REST_Response        Response object.
283     */
284    public function prepare_item_for_response( $prompt, $request ) {
285        require_once WP_CONTENT_DIR . '/lib/blogging-prompts/answers.php';
286        require_once WP_CONTENT_DIR . '/lib/blogging-prompts/utils.php';
287
288        $fields = $this->get_fields_for_response( $request );
289
290        // Base fields for every post.
291        $data = array();
292
293        if ( rest_is_field_included( 'id', $fields ) ) {
294            $data['id'] = $prompt->ID;
295        }
296
297        if ( rest_is_field_included( 'date', $fields ) ) {
298            $data['date'] = $this->prepare_date_response( $prompt->post_date_gmt );
299        }
300
301        if ( rest_is_field_included( 'label', $fields ) ) {
302            if ( $this->is_in_bloganuary( $prompt->post_date_gmt ) ) {
303                $data['label'] = __( 'Bloganuary writing prompt', 'jetpack' );
304            } else {
305                $data['label'] = __( 'Daily writing prompt', 'jetpack' );
306            }
307        }
308
309        if ( rest_is_field_included( 'text', $fields ) ) {
310            $text = \BloggingPrompts\prompt_without_blocks( $prompt->post_content );
311            // Allow translating a variable, since this text is imported from bloggingpromptstemplates.wordpress.com for translation.
312            $translated_text = __( $text, 'jetpack' ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
313            $data['text']    = wp_kses( $translated_text, wp_kses_allowed_html( 'post' ) );
314        }
315
316        if ( rest_is_field_included( 'attribution', $fields ) ) {
317            $data['attribution'] = esc_html( get_post_meta( $prompt->ID, 'blogging_prompts_attribution', true ) );
318        }
319
320        // Will always be false when requesting as blog.
321        if ( rest_is_field_included( 'answered', $fields ) ) {
322            $data['answered'] = (bool) \A8C\BloggingPrompts\Answers::is_answered_by_user( $prompt->ID, get_current_user_id() );
323        }
324
325        if ( rest_is_field_included( 'answered_users_count', $fields ) ) {
326            $data['answered_users_count'] = (int) \A8C\BloggingPrompts\Answers::get_count( $prompt->ID );
327        }
328
329        if ( rest_is_field_included( 'answered_users_sample', $fields ) ) {
330            $data['answered_users_sample'] = $this->build_answering_users_sample( $prompt->ID );
331        }
332
333        if ( rest_is_field_included( 'answered_link', $fields ) ) {
334            if ( $this->is_in_bloganuary( $prompt->post_date_gmt ) ) {
335                $bloganuary_id         = $this->get_bloganuary_id( $prompt->post_date_gmt );
336                $data['answered_link'] = esc_url( "https://wordpress.com/tag/{$bloganuary_id}" );
337            } else {
338                $data['answered_link'] = esc_url( "https://wordpress.com/tag/dailyprompt-{$prompt->ID}" );
339            }
340        }
341
342        if ( rest_is_field_included( 'answered_link_text', $fields ) ) {
343            $data['answered_link_text'] = __( 'View all responses', 'jetpack' );
344        }
345
346        if ( $this->is_in_bloganuary( $prompt->post_date_gmt ) && rest_is_field_included( 'bloganuary_id', $fields ) ) {
347            $data['bloganuary_id'] = $this->get_bloganuary_id( $prompt->post_date_gmt );
348        }
349
350        return $data;
351    }
352
353    /**
354     * Return true if the post is in "Bloganuary"
355     *
356     * @param string $post_date_gmt Unused - Post date in GMT.
357     * @return bool Always returns false as Bloganuary is disabled.
358     */
359    protected function is_in_bloganuary( $post_date_gmt ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
360
361        /*
362        Disable for January 2025 and beyond (see https://wp.me/p5uIfZ-gxX).
363            Previously, this method would check if the post was published in January:
364            - Extract month from post_date_gmt -- $post_month = gmdate( 'm', strtotime( $post_date_gmt ) );
365            - Return true if month was '01' -- return $post_month === '01';
366        */
367        return false;
368    }
369
370    /**
371     * Return the bloganuary id of the form `bloganuary-yyyy-dd`
372     *
373     * @param string $post_date_gmt Post date in GMT.
374     * @return string Bloganuary id.
375     */
376    protected function get_bloganuary_id( $post_date_gmt ) {
377        $post_year_day = gmdate( 'Y-d', strtotime( $post_date_gmt ) );
378        if ( $this->force_year ) {
379            $post_year_day = $this->force_year . '-' . gmdate( 'd', strtotime( $post_date_gmt ) );
380        }
381        return 'bloganuary-' . $post_year_day;
382    }
383
384    /**
385     * Format a date for a blogging prompt, omiting the time.
386     *
387     * @param string $date_gmt Publish datetime of the prompt in GMT, i.e. 0000-00-00 00:00:00.
388     * @param string $date     Publish datetime of the prompt, i.e. 0000-00-00 00:00:00.
389     * @return string Publish date of the prompt in YYYY-MM-DD format.
390     */
391    public function prepare_date_response( $date_gmt, $date = null ) {
392        $post_date = $date ? $date : $date_gmt;
393        $date_obj  = date_create( $post_date );
394
395        if ( $this->force_year ) {
396            $date_obj->setDate( $this->force_year, (int) $date_obj->format( 'n' ), (int) $date_obj->format( 'j' ) );
397
398            // If ascending by day of year, go to the next year when we pass the last day of the year.
399            if ( $date_obj->format( 'm-d' ) === '12-31' ) {
400                $this->force_year += 1;
401            }
402        }
403
404        return false !== $date_obj ? $date_obj->format( 'Y-m-d' ) : substr( $post_date, 0, 10 );
405    }
406
407    /**
408     * Retrieves the query params for blogging prompt collections.
409     *
410     * @return array Query parameters for the collection.
411     */
412    public function get_collection_params() {
413        $parent_args = parent::get_collection_params();
414
415        $args = array(
416            // Modify date args so that will except a YYYY-MM-DD without a time.
417            'after'      => array(
418                'description'       => __( 'Show prompts following a given date.', 'jetpack' ),
419                'type'              => 'string',
420                'validate_callback' => function ( $param ) {
421                    // Allow month and day without year, e.g. `--02-28`
422                    if ( str_starts_with( $param, '-' ) ) {
423                        return false !== date_create_from_format( '--m-d', $param );
424                    }
425
426                    return false !== date_create( $param );
427                },
428            ),
429            'before'     => array(
430                'description'       => __( 'Show prompts before a given date.', 'jetpack' ),
431                'type'              => 'string',
432                'validate_callback' => function ( $param ) {
433                    return false !== date_create( $param );
434                },
435            ),
436            'force_year' => array(
437                'description'       => __( 'Force the returned prompts to be for a specific year. Returns only one prompt for each day.', 'jetpack' ),
438                'type'              => 'integer',
439                'validate_callback' => function ( $param ) {
440                    return is_numeric( $param ) && intval( $param ) > 0 && intval( $param ) < 9999;
441                },
442            ),
443        );
444
445        $args['exclude']          = $parent_args['exclude'];
446        $args['include']          = $parent_args['include'];
447        $args['page']             = $parent_args['page'];
448        $args['per_page']         = $parent_args['per_page'];
449        $args['order']            = $parent_args['order'];
450        $args['order']['default'] = 'asc';
451        $args['orderby']          = $parent_args['orderby'];
452        $args['search']           = $parent_args['search'];
453
454        return $args;
455    }
456
457    /**
458     * Retrieves the blogging prompt's schema, conforming to JSON Schema.
459     *
460     * @return array Item schema data.
461     */
462    public function get_item_schema() {
463        return array(
464            '$schema'    => 'http://json-schema.org/draft-04/schema#',
465            'title'      => 'blogging-prompt',
466            'type'       => 'object',
467            'properties' => array(
468                'id'                    => array(
469                    'description' => __( 'Unique identifier for the post.', 'jetpack' ),
470                    'type'        => 'integer',
471                ),
472                'date'                  => array(
473                    'description' => __( "The date the post was published, in the site's timezone.", 'jetpack' ),
474                    'type'        => 'string',
475                ),
476                'label'                 => array(
477                    'description' => __( 'Label for the prompt.', 'jetpack' ),
478                    'type'        => 'string',
479                ),
480                'text'                  => array(
481                    'description' => __( 'The text of the prompt. May include html tags like <em>.', 'jetpack' ),
482                    'type'        => 'string',
483                ),
484                'attribution'           => array(
485                    'description' => __( 'Source of the prompt, if known.', 'jetpack' ),
486                    'type'        => 'string',
487                ),
488                'answered'              => array(
489                    'description' => __( 'Whether the user has answered the prompt.', 'jetpack' ),
490                    'type'        => 'boolean',
491                ),
492                'answered_users_count'  => array(
493                    'description' => __( 'Number of users who have answered the prompt.', 'jetpack' ),
494                    'type'        => 'integer',
495                ),
496                'answered_users_sample' => array(
497                    'description' => __( 'Sample of users who have answered the prompt.', 'jetpack' ),
498                    'type'        => 'array',
499                    'items'       => array(
500                        'type'       => 'object',
501                        'properties' => array(
502                            'avatar' => array(
503                                'description' => __( "Gravatar URL for the user's avatar image.", 'jetpack' ),
504                                'type'        => 'string',
505                                'format'      => 'uri',
506                            ),
507                        ),
508                    ),
509                ),
510                'answered_link'         => array(
511                    'description' => __( 'Link to answers for the prompt.', 'jetpack' ),
512                    'type'        => 'string',
513                    'format'      => 'uri',
514                ),
515                'answered_link_text'    => array(
516                    'description' => __( 'Text for the link to answers for the prompt.', 'jetpack' ),
517                    'type'        => 'string',
518                ),
519                'bloganuary_id'         => array(
520                    'description' => __( 'Id used by the bloganuary promotion', 'jetpack' ),
521                    'type'        => 'string',
522                ),
523            ),
524        );
525    }
526
527    /**
528     * Checks if a given request has access to read blogging prompts for a site.
529     *
530     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
531     */
532    public function permissions_check() {
533        if ( current_user_can( 'edit_posts' ) ) {
534            return true;
535        }
536
537        // Allow "as blog" requests to wpcom so users without accounts can insert the Writing prompt block in the editor.
538        if ( $this->is_wpcom && is_jetpack_site( get_current_blog_id() ) ) {
539            if ( ! class_exists( 'WPCOM_REST_API_V2_Endpoint_Jetpack_Auth' ) ) {
540                require_once dirname( __DIR__ ) . '/rest-api-plugins/endpoints/jetpack-auth.php';
541            }
542
543            $jp_auth_endpoint = new WPCOM_REST_API_V2_Endpoint_Jetpack_Auth();
544            if ( true === $jp_auth_endpoint->is_jetpack_authorized_for_site() ) {
545                return true;
546            }
547        }
548
549        return new WP_Error(
550            'rest_cannot_read_prompts',
551            __( 'Sorry, you are not allowed to access blogging prompts on this site.', 'jetpack' ),
552            array( 'status' => rest_authorization_required_code() )
553        );
554    }
555
556    /**
557     * Creates a sample of users who have answered a blogging prompt.
558     *
559     * @param int $prompt_id Prompt ID.
560     * @return array List of users, including a gravatar url for each user.
561     */
562    protected function build_answering_users_sample( $prompt_id ) {
563        $results = \A8C\BloggingPrompts\Answers::get_sample_users_by( $prompt_id );
564
565        if ( ! $results ) {
566            return array();
567        }
568
569        $users = array();
570
571        foreach ( $results as $user ) {
572            $url = wpcom_get_avatar_url( $user->user_id, 96, 'identicon', false );
573            if ( has_gravatar( $user->user_id ) && ! empty( $url[0] ) && ! is_wp_error( $url[0] ) ) {
574                $users[] = array(
575                    'avatar' => (string) esc_url_raw( htmlspecialchars_decode( $url[0], ENT_COMPAT ) ),
576                );
577            }
578        }
579
580        return array_slice( $users, 0, 3 );
581    }
582}
583
584wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V3_Endpoint_Blogging_Prompts' );