PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
← All changes | _inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php +584 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,584 @@
1 +<?php
2 +/**
3 + * Blogging prompts endpoint for wpcom/v3.
4 + *
5 + * @package automattic/jetpack
6 + */
7 +
8 +use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
9 +
10 +if ( ! defined( 'ABSPATH' ) ) {
11 + exit( 0 );
12 +}
13 +
14 +/**
15 + * REST API endpoint wpcom/v3/sites/%s/blogging-prompts.
16 + */
17 +class 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 +
584 +wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V3_Endpoint_Blogging_Prompts' );