PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-stats-admin / src / class-rest-controller.php

class-rest-controller.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-stats-admin/src/class-rest-controller.php

1,460 lines 40.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The Stats Rest Controller class.
4 * Registers the REST routes for Odyssey Stats.
5 *
6 * @package automattic/jetpack-stats-admin
7 */
8
9 namespace Automattic\Jetpack\Stats_Admin;
10
11 use Automattic\Jetpack\Constants;
12 use Automattic\Jetpack\Stats\Settings as Stats_Settings;
13 use Automattic\Jetpack\Stats\WPCOM_Stats;
14 use Jetpack_Options;
15 use WP_Error;
16 use WP_REST_Request;
17 use WP_REST_Server;
18
19 /**
20 * Registers the REST routes for Stats.
21 * It bascially forwards the requests to the WordPress.com REST API.
22 */
23 class REST_Controller {
24 const JETPACK_STATS_DASHBOARD_MODULES_CACHE_KEY = 'jetpack_stats_dashboard_modules_cache_key';
25 const JETPACK_STATS_DASHBOARD_MODULE_SETTINGS_CACHE_KEY = 'jetpack_stats_dashboard_module_settings_cache_key';
26
27 /**
28 * Namespace for the REST API.
29 *
30 * @var string
31 */
32 public static $namespace = 'jetpack/v4/stats-app';
33
34 /**
35 * Hold an instance of WPCOM_Stats.
36 *
37 * @var WPCOM_Stats
38 */
39 protected $wpcom_stats;
40
41 /**
42 * Constructor
43 */
44 public function __construct() {
45 $this->wpcom_stats = new WPCOM_Stats();
46 }
47
48 /**
49 * Registers the REST routes on the `rest_api_init` hook.
50 *
51 * Instantiated here, rather than eagerly, so the controller class only loads
52 * on requests that reach `rest_api_init`. Static so the callback can be
53 * unregistered.
54 *
55 * @access public
56 */
57 public static function register() {
58 ( new self() )->register_rest_routes();
59 }
60
61 /**
62 * Registers the REST routes for Odyssey Stats.
63 *
64 * Odyssey Stats is built from `wp-calypso`, which leverages the `public-api.wordpress.com` API.
65 * The current Site ID is added as part of the route, so that the front end doesn't have to handle the differences.
66 *
67 * @access public
68 * @static
69 */
70 public function register_rest_routes() {
71 // Stats for single resource type.
72 register_rest_route(
73 static::$namespace,
74 sprintf( '/sites/%d/stats/(?P<resource>[\-\w]+)/(?P<resource_id>[\d]+)', Jetpack_Options::get_option( 'id' ) ),
75 array(
76 'methods' => WP_REST_Server::READABLE,
77 'callback' => array( $this, 'get_single_resource_stats' ),
78 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
79 )
80 );
81
82 // Stats for a resource type.
83 register_rest_route(
84 static::$namespace,
85 sprintf( '/sites/%d/stats/(?P<resource>[\-\w]+)', Jetpack_Options::get_option( 'id' ) ),
86 array(
87 'methods' => WP_REST_Server::READABLE,
88 'callback' => array( $this, 'get_stats_resource' ),
89 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
90 )
91 );
92
93 // Single post info.
94 register_rest_route(
95 static::$namespace,
96 sprintf( '/sites/%d/posts/(?P<resource_id>[\d]+)', Jetpack_Options::get_option( 'id' ) ),
97 array(
98 'methods' => WP_REST_Server::READABLE,
99 'callback' => array( $this, 'get_single_post' ),
100 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
101 )
102 );
103
104 // Single post likes.
105 register_rest_route(
106 static::$namespace,
107 sprintf( '/sites/%d/posts/(?P<resource_id>[\d]+)/likes', Jetpack_Options::get_option( 'id' ) ),
108 array(
109 'methods' => WP_REST_Server::READABLE,
110 'callback' => array( $this, 'get_single_post_likes' ),
111 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
112 )
113 );
114
115 // General stats for the site.
116 register_rest_route(
117 static::$namespace,
118 sprintf( '/sites/%d/stats', Jetpack_Options::get_option( 'id' ) ),
119 array(
120 'methods' => WP_REST_Server::READABLE,
121 'callback' => array( $this, 'get_site_stats' ),
122 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
123 )
124 );
125
126 // Whether site has never published post / page.
127 register_rest_route(
128 static::$namespace,
129 sprintf( '/sites/%d/site-has-never-published-post', Jetpack_Options::get_option( 'id' ) ),
130 array(
131 'methods' => WP_REST_Server::READABLE,
132 'callback' => array( $this, 'site_has_never_published_post' ),
133 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
134 )
135 );
136
137 // List posts.
138 register_rest_route(
139 static::$namespace,
140 sprintf( '/sites/%d/posts', Jetpack_Options::get_option( 'id' ) ),
141 array(
142 'methods' => WP_REST_Server::READABLE,
143 'callback' => array( $this, 'get_site_posts' ),
144 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
145 )
146 );
147
148 // Subscribers counts.
149 register_rest_route(
150 static::$namespace,
151 sprintf( '/sites/%d/subscribers/counts', Jetpack_Options::get_option( 'id' ) ),
152 array(
153 'methods' => WP_REST_Server::READABLE,
154 'callback' => array( $this, 'get_site_subscribers_counts' ),
155 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
156 )
157 );
158
159 // Stats Plan Usage.
160 register_rest_route(
161 static::$namespace,
162 sprintf( '/sites/%d/jetpack-stats/usage', Jetpack_Options::get_option( 'id' ) ),
163 array(
164 'methods' => WP_REST_Server::READABLE,
165 'callback' => array( $this, 'get_site_plan_usage' ),
166 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
167 )
168 );
169
170 // Stats settings.
171 register_rest_route(
172 static::$namespace,
173 sprintf( '/sites/%d/jetpack-stats/settings', Jetpack_Options::get_option( 'id' ) ),
174 array(
175 array(
176 'methods' => WP_REST_Server::READABLE,
177 'callback' => array( $this, 'get_stats_settings' ),
178 'permission_callback' => array( $this, 'can_user_manage_stats_settings_callback' ),
179 ),
180 array(
181 'methods' => WP_REST_Server::EDITABLE,
182 'callback' => array( $this, 'update_stats_settings' ),
183 'permission_callback' => array( $this, 'can_user_manage_stats_settings_callback' ),
184 'args' => array(
185 'admin_bar' => array(
186 'description' => 'Show a chart of the last 48 hours of views in the admin bar',
187 'type' => 'boolean',
188 ),
189 'roles' => array(
190 'description' => 'Roles that can view Stats. `administrator` is always kept.',
191 'type' => 'array',
192 'items' => array( 'type' => 'string' ),
193 'minItems' => 1,
194 ),
195 'count_roles' => array(
196 'description' => 'Roles whose logged-in page views are counted',
197 'type' => 'array',
198 'items' => array( 'type' => 'string' ),
199 ),
200 'wpcom_reader_views_enabled' => array(
201 'description' => 'Show post views in the WordPress.com Reader',
202 'type' => 'boolean',
203 ),
204 ),
205 ),
206 )
207 );
208
209 // User feedback endpoint.
210 register_rest_route(
211 static::$namespace,
212 sprintf( '/sites/%d/jetpack-stats/user-feedback', Jetpack_Options::get_option( 'id' ) ),
213 array(
214 'methods' => WP_REST_Server::CREATABLE,
215 'callback' => array( $this, 'post_user_feedback' ),
216 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
217 )
218 );
219
220 // WordAds Earnings.
221 register_rest_route(
222 static::$namespace,
223 sprintf( '/sites/%d/wordads/earnings', Jetpack_Options::get_option( 'id' ) ),
224 array(
225 'methods' => WP_REST_Server::READABLE,
226 'callback' => array( $this, 'get_wordads_earnings' ),
227 'permission_callback' => array( $this, 'can_user_view_wordads_stats_callback' ),
228 )
229 );
230
231 // WordAds Stats.
232 register_rest_route(
233 static::$namespace,
234 sprintf( '/sites/%d/wordads/stats', Jetpack_Options::get_option( 'id' ) ),
235 array(
236 'methods' => WP_REST_Server::READABLE,
237 'callback' => array( $this, 'get_wordads_stats' ),
238 'permission_callback' => array( $this, 'can_user_view_wordads_stats_callback' ),
239 )
240 );
241
242 // Legacy: Update Stats notices.
243 // TODO: remove this in the next release.
244 register_rest_route(
245 static::$namespace,
246 '/stats/notices',
247 array(
248 'methods' => WP_REST_Server::EDITABLE,
249 'callback' => array( $this, 'update_notice_status' ),
250 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
251 'args' => array(
252 'id' => array(
253 'required' => true,
254 'type' => 'string',
255 'description' => 'ID of the notice',
256 ),
257 'status' => array(
258 'required' => true,
259 'type' => 'string',
260 'description' => 'Status of the notice',
261 ),
262 'postponed_for' => array(
263 'type' => 'number',
264 'default' => 0,
265 'description' => 'Postponed for (in seconds)',
266 'minimum' => 0,
267 ),
268 ),
269 )
270 );
271
272 // Update Stats notices.
273 register_rest_route(
274 static::$namespace,
275 sprintf( '/sites/%d/jetpack-stats-dashboard/notices', Jetpack_Options::get_option( 'id' ) ),
276 array(
277 'methods' => WP_REST_Server::EDITABLE,
278 'callback' => array( $this, 'update_notice_status' ),
279 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
280 'args' => array(
281 'id' => array(
282 'required' => true,
283 'type' => 'string',
284 'description' => 'ID of the notice',
285 ),
286 'status' => array(
287 'required' => true,
288 'type' => 'string',
289 'description' => 'Status of the notice',
290 ),
291 'postponed_for' => array(
292 'type' => 'number',
293 // Forwarded to WPCOM as-is, whose schema rejects the null an omitted param would carry.
294 'default' => 0,
295 'description' => 'Postponed for (in seconds)',
296 'minimum' => 0,
297 ),
298 ),
299 )
300 );
301
302 // Get Stats notices.
303 register_rest_route(
304 static::$namespace,
305 sprintf( '/sites/%d/jetpack-stats-dashboard/notices', Jetpack_Options::get_option( 'id' ) ),
306 array(
307 'methods' => WP_REST_Server::READABLE,
308 'callback' => array( $this, 'get_notice_status' ),
309 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
310 'args' => array(
311 'include_details' => array(
312 'type' => 'boolean',
313 'default' => false,
314 'description' => 'Return a detail record per notice instead of a flat boolean map',
315 ),
316 ),
317 )
318 );
319
320 // Get referrer spam list.
321 register_rest_route(
322 static::$namespace,
323 sprintf( '/sites/%d/stats/referrers/spam', Jetpack_Options::get_option( 'id' ) ),
324 array(
325 'methods' => WP_REST_Server::READABLE,
326 'callback' => array( $this, 'get_referrer_spam_list' ),
327 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
328 )
329 );
330
331 // Mark referrer spam.
332 register_rest_route(
333 static::$namespace,
334 sprintf( '/sites/%d/stats/referrers/spam/new', Jetpack_Options::get_option( 'id' ) ),
335 array(
336 'methods' => WP_REST_Server::EDITABLE,
337 'callback' => array( $this, 'mark_referrer_spam' ),
338 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
339 'args' => array(
340 'domain' => array(
341 'required' => true,
342 'type' => 'string',
343 'description' => 'Domain of the referrer',
344 ),
345 ),
346 )
347 );
348
349 // Unmark referrer spam.
350 register_rest_route(
351 static::$namespace,
352 sprintf( '/sites/%d/stats/referrers/spam/delete', Jetpack_Options::get_option( 'id' ) ),
353 array(
354 'methods' => WP_REST_Server::EDITABLE,
355 'callback' => array( $this, 'unmark_referrer_spam' ),
356 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
357 'args' => array(
358 'domain' => array(
359 'required' => true,
360 'type' => 'string',
361 'description' => 'Domain of the referrer',
362 ),
363 ),
364 )
365 );
366
367 // Update dashboard modules.
368 register_rest_route(
369 static::$namespace,
370 sprintf( '/sites/%d/jetpack-stats-dashboard/modules', Jetpack_Options::get_option( 'id' ) ),
371 array(
372 'methods' => WP_REST_Server::EDITABLE,
373 'callback' => array( $this, 'update_dashboard_modules' ),
374 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
375 )
376 );
377
378 // Get dashboard modules.
379 register_rest_route(
380 static::$namespace,
381 sprintf( '/sites/%d/jetpack-stats-dashboard/modules', Jetpack_Options::get_option( 'id' ) ),
382 array(
383 'methods' => WP_REST_Server::READABLE,
384 'callback' => array( $this, 'get_dashboard_modules' ),
385 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
386 )
387 );
388
389 // Update dashboard module settings.
390 register_rest_route(
391 static::$namespace,
392 sprintf( '/sites/%d/jetpack-stats-dashboard/module-settings', Jetpack_Options::get_option( 'id' ) ),
393 array(
394 'methods' => WP_REST_Server::EDITABLE,
395 'callback' => array( $this, 'update_dashboard_module_settings' ),
396 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
397 )
398 );
399
400 // Get dashboard module settings.
401 register_rest_route(
402 static::$namespace,
403 sprintf( '/sites/%d/jetpack-stats-dashboard/module-settings', Jetpack_Options::get_option( 'id' ) ),
404 array(
405 'methods' => WP_REST_Server::READABLE,
406 'callback' => array( $this, 'get_dashboard_module_settings' ),
407 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
408 )
409 );
410
411 // Get email stats as a list.
412 register_rest_route(
413 static::$namespace,
414 sprintf( '/sites/%d/stats/emails/(?P<resource>[\-\w\d]+)', Jetpack_Options::get_option( 'id' ) ),
415 array(
416 'methods' => WP_REST_Server::READABLE,
417 'callback' => array( $this, 'get_email_stats_list' ),
418 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
419 )
420 );
421
422 // Get Email opens stats for a single post.
423 register_rest_route(
424 static::$namespace,
425 sprintf( '/sites/%d/stats/opens/emails/(?P<post_id>[\d]+)/(?P<resource>[\-\w]+)', Jetpack_Options::get_option( 'id' ) ),
426 array(
427 'methods' => WP_REST_Server::READABLE,
428 'callback' => array( $this, 'get_email_opens_stats_single' ),
429 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
430 )
431 );
432
433 // Get Email clicks stats for a single post.
434 register_rest_route(
435 static::$namespace,
436 sprintf( '/sites/%d/stats/clicks/emails/(?P<post_id>[\d]+)/(?P<resource>[\-\w]+)', Jetpack_Options::get_option( 'id' ) ),
437 array(
438 'methods' => WP_REST_Server::READABLE,
439 'callback' => array( $this, 'get_email_clicks_stats_single' ),
440 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
441 )
442 );
443
444 // Get Email stats time series.
445 register_rest_route(
446 static::$namespace,
447 sprintf( '/sites/%d/stats/(?P<resource>[\-\w]+)/emails/(?P<post_id>[\d]+)', Jetpack_Options::get_option( 'id' ) ),
448 array(
449 'methods' => WP_REST_Server::READABLE,
450 'callback' => array( $this, 'get_email_stats_time_series' ),
451 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
452 )
453 );
454
455 // Get UTM stats time series.
456 register_rest_route(
457 static::$namespace,
458 // /stats/utm/utm_campaign,utm_source,utm_medium
459 sprintf( '/sites/%d/stats/utm/(?P<utm_params>[_,\-\w]+)', Jetpack_Options::get_option( 'id' ) ),
460 array(
461 'methods' => WP_REST_Server::READABLE,
462 'callback' => array( $this, 'get_utm_stats_time_series' ),
463 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
464 )
465 );
466
467 // Get Devices stats time series.
468 register_rest_route(
469 static::$namespace,
470 // /stats/devices/screensize
471 sprintf( '/sites/%d/stats/devices/(?P<device_property>[\w]+)', Jetpack_Options::get_option( 'id' ) ),
472 array(
473 'methods' => WP_REST_Server::READABLE,
474 'callback' => array( $this, 'get_devices_stats_time_series' ),
475 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
476 )
477 );
478
479 // Rerun commercial classificiation.
480 register_rest_route(
481 static::$namespace,
482 sprintf( '/sites/%d/commercial-classification', Jetpack_Options::get_option( 'id' ) ),
483 array(
484 'methods' => WP_REST_Server::EDITABLE,
485 'callback' => array( $this, 'run_commercial_classification' ),
486 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
487 )
488 );
489
490 // Purchases endpoint.
491 register_rest_route(
492 static::$namespace,
493 sprintf( '/sites/%d/purchases', Jetpack_Options::get_option( 'id' ) ),
494 array(
495 'methods' => WP_REST_Server::READABLE,
496 'callback' => array( $this, 'get_site_purchases' ),
497 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
498 )
499 );
500
501 // Get Location stats.
502 register_rest_route(
503 static::$namespace,
504 sprintf( '/sites/%d/stats/location-views/(?P<geo_mode>country|region|city)', Jetpack_Options::get_option( 'id' ) ),
505 array(
506 'methods' => WP_REST_Server::READABLE,
507 'callback' => array( $this, 'get_location_stats' ),
508 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
509 )
510 );
511 }
512
513 /**
514 * Only administrators or users with capability `view_stats` can access the API.
515 *
516 * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
517 */
518 public function can_user_view_general_stats_callback() {
519 if ( current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ) ) {
520 return true;
521 }
522
523 return $this->get_forbidden_error();
524 }
525
526 /**
527 * Only administrators can read or change the Stats settings, because `roles` decides who else can view Stats.
528 *
529 * @return bool|WP_Error
530 */
531 public function can_user_manage_stats_settings_callback() {
532 if ( current_user_can( 'manage_options' ) ) {
533 return true;
534 }
535
536 return $this->get_forbidden_error();
537 }
538
539 /**
540 * Only administrators or users with capability `activate_wordads` can access the API.
541 */
542 public function can_user_view_wordads_stats_callback() {
543 // phpcs:ignore WordPress.WP.Capabilities.Unknown
544 if ( current_user_can( 'manage_options' ) || current_user_can( 'activate_wordads' ) ) {
545 return true;
546 }
547
548 return $this->get_forbidden_error();
549 }
550
551 /**
552 * Stats resource endpoint.
553 *
554 * @param WP_REST_Request $req The request object.
555 * @return array
556 */
557 public function get_stats_resource( $req ) {
558 switch ( $req->get_param( 'resource' ) ) {
559 case 'file-downloads':
560 return $this->wpcom_stats->get_file_downloads( $req->get_params() );
561
562 case 'video-plays':
563 return $this->wpcom_stats->get_video_plays( $req->get_params() );
564
565 case 'clicks':
566 return $this->wpcom_stats->get_clicks( $req->get_params() );
567
568 case 'search-terms':
569 return $this->wpcom_stats->get_search_terms( $req->get_params() );
570
571 case 'top-authors':
572 return $this->wpcom_stats->get_top_authors( $req->get_params() );
573
574 case 'country-views':
575 return $this->wpcom_stats->get_views_by_country( $req->get_params() );
576
577 case 'referrers':
578 return $this->wpcom_stats->get_referrers( $req->get_params() );
579
580 case 'top-posts':
581 return $this->wpcom_stats->get_top_posts( $req->get_params() );
582
583 case 'archives':
584 return $this->wpcom_stats->get_archives( $req->get_params() );
585
586 case 'publicize':
587 return $this->wpcom_stats->get_publicize_followers( $req->get_params() );
588
589 case 'followers':
590 return $this->wpcom_stats->get_followers( $req->get_params() );
591
592 case 'tags':
593 return $this->wpcom_stats->get_tags( $req->get_params() );
594
595 case 'visits':
596 return $this->wpcom_stats->get_visits( $req->get_params() );
597
598 case 'comments':
599 return $this->wpcom_stats->get_top_comments( $req->get_params() );
600
601 case 'comment-followers':
602 return $this->wpcom_stats->get_comment_followers( $req->get_params() );
603
604 case 'streak':
605 return $this->wpcom_stats->get_streak( $req->get_params() );
606
607 case 'insights':
608 return $this->wpcom_stats->get_insights( $req->get_params() );
609
610 case 'highlights':
611 return $this->wpcom_stats->get_highlights( $req->get_params() );
612
613 case 'subscribers':
614 return WPCOM_Client::request_as_blog_cached(
615 sprintf(
616 '/sites/%d/stats/subscribers?%s',
617 Jetpack_Options::get_option( 'id' ),
618 $this->filter_and_build_query_string(
619 $req->get_query_params()
620 )
621 ),
622 'v1.1',
623 array( 'timeout' => 5 )
624 );
625
626 default:
627 return $this->get_forbidden_error();
628 }
629 }
630
631 /**
632 * Return likes of a single post.
633 *
634 * @param WP_REST_Request $req The request object.
635 */
636 public function get_single_post_likes( $req ) {
637 $response = wp_remote_get(
638 sprintf(
639 '%s/rest/v1.2/sites/%d/posts/%d/likes?%s',
640 Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
641 Jetpack_Options::get_option( 'id' ),
642 $req->get_param( 'resource_id' ),
643 $this->filter_and_build_query_string(
644 $req->get_params(),
645 array( 'resource_id' )
646 )
647 ),
648 array( 'timeout' => 5 )
649 );
650
651 $response_code = wp_remote_retrieve_response_code( $response );
652 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
653
654 if ( is_wp_error( $response ) ) {
655 return $response;
656 }
657
658 if ( 200 !== $response_code ) {
659 return new WP_Error(
660 isset( $response_body['error'] ) ? 'remote-error-' . $response_body['error'] : 'remote-error',
661 $response_body['message'] ?? 'unknown remote error',
662 array( 'status' => $response_code )
663 );
664 }
665
666 return $response_body;
667 }
668
669 /**
670 * Site Stats Resource endpoint.
671 *
672 * @param WP_REST_Request $req The request object.
673 * @return array
674 */
675 public function get_single_resource_stats( $req ) {
676 switch ( $req->get_param( 'resource' ) ) {
677 case 'post':
678 return $this->wpcom_stats->get_post_views(
679 intval( $req->get_param( 'resource_id' ) ),
680 $req->get_params()
681 );
682
683 case 'video':
684 return $this->wpcom_stats->get_video_details(
685 intval( $req->get_param( 'resource_id' ) ),
686 $req->get_params()
687 );
688
689 default:
690 return $this->get_forbidden_error();
691 }
692 }
693
694 /**
695 * Get brief information for a single post.
696 *
697 * @param WP_REST_Request $req The request object.
698 * @return array
699 */
700 public function get_single_post( $req ) {
701 $post = get_post( intval( $req->get_param( 'resource_id' ) ), 'OBJECT', 'display' );
702 if ( is_wp_error( $post ) || empty( $post ) ) {
703 return $post;
704 }
705
706 // The endpoint should be as compatible as possible with `/sites/$site_id/posts/$post_id`.
707 // The reason we are not forwarding the request is that `/sites/$site_id/posts/$post_id` might require user tokens for private posts/sites, which is not possible for users without a WordPress.com account.
708 // 'like_count' is not included in the response because it's available through another endpoint `/sites/$site_id/posts/$post_id/likes`.
709 return array(
710 'ID' => $post->ID,
711 'site_ID' => Jetpack_Options::get_option( 'id' ),
712 'title' => $post->post_title,
713 'URL' => get_permalink( $post->ID ),
714 'type' => $post->post_type,
715 'status' => $post->post_status,
716 'discussion' => array( 'comment_count' => intval( $post->comment_count ) ),
717 'date' => $post->post_date,
718 'post_thumbnail' => array( 'URL' => get_the_post_thumbnail_url( $post->ID ) ),
719 );
720 }
721
722 /**
723 * Get site stats.
724 *
725 * @param WP_REST_Request $req The request object.
726 * @return array
727 */
728 public function get_site_stats( $req ) {
729 return $this->wpcom_stats->get_stats( $req->get_params() );
730 }
731
732 /**
733 * List posts for the site.
734 *
735 * @param WP_REST_Request $req The request object.
736 * @return array
737 */
738 public function get_site_posts( $req ) {
739 // Force wpcom response.
740 $params = array_merge( array( 'force' => 'wpcom' ), $req->get_params() );
741 $response = wp_remote_get(
742 sprintf(
743 '%s/rest/v1.1/sites/%d/posts?%s',
744 Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
745 Jetpack_Options::get_option( 'id' ),
746 $req->get_param( 'resource_id' ),
747 $this->filter_and_build_query_string( $params, array( 'resource_id' ) )
748 ),
749 array( 'timeout' => 5 )
750 );
751
752 $response_code = wp_remote_retrieve_response_code( $response );
753 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
754
755 if ( is_wp_error( $response ) ) {
756 return $response;
757 }
758
759 if ( 200 !== $response_code ) {
760 return new WP_Error(
761 isset( $response_body['error'] ) ? 'remote-error-' . $response_body['error'] : 'remote-error',
762 $response_body['message'] ?? 'unknown remote error',
763 array( 'status' => $response_code )
764 );
765 }
766
767 return $response_body;
768 }
769
770 /**
771 * Get site subscribers counts.
772 *
773 * @param WP_REST_Request $req The request object.
774 *
775 * @return array
776 */
777 public function get_site_subscribers_counts( $req ) {
778 return WPCOM_Client::request_as_blog_cached(
779 sprintf(
780 '/sites/%d/subscribers/counts?%s',
781 Jetpack_Options::get_option( 'id' ),
782 $this->filter_and_build_query_string(
783 $req->get_query_params()
784 )
785 ),
786 'v2',
787 array( 'timeout' => 5 ),
788 null,
789 'wpcom'
790 );
791 }
792
793 /**
794 * Get site plan usage.
795 *
796 * @param WP_REST_Request $req The request object.
797 *
798 * @return array
799 */
800 public function get_site_plan_usage( $req ) {
801 return WPCOM_Client::request_as_blog_cached(
802 sprintf(
803 '/sites/%d/jetpack-stats/usage?%s',
804 Jetpack_Options::get_option( 'id' ),
805 $this->filter_and_build_query_string(
806 $req->get_query_params()
807 )
808 ),
809 'v2',
810 array( 'timeout' => 5 ),
811 null,
812 'wpcom',
813 false
814 );
815 }
816
817 /**
818 * Get the Stats settings and the site's roles.
819 *
820 * @return array
821 */
822 public function get_stats_settings() {
823 return $this->get_stats_settings_response();
824 }
825
826 /**
827 * Save the Stats settings in the request.
828 *
829 * @param WP_REST_Request $req The request object.
830 *
831 * @return array|WP_Error The settings after the save, or why the values were refused.
832 */
833 public function update_stats_settings( $req ) {
834 $params = $req->get_params();
835 $keys = self::get_stats_settings_keys();
836
837 $stats_values = array_intersect_key( $params, array_flip( $keys ) );
838 if ( empty( $stats_values ) && ! isset( $params['wpcom_reader_views_enabled'] ) ) {
839 return new WP_Error(
840 'jetpack_stats_missing_setting_field',
841 sprintf(
842 /* translators: %s: comma-separated list of the settings that can be changed. */
843 __( 'Provide at least one of: %s.', 'jetpack-stats-admin' ),
844 implode( ', ', array_merge( $keys, array( 'wpcom_reader_views_enabled' ) ) )
845 ),
846 array( 'status' => 400 )
847 );
848 }
849
850 if ( ! empty( $stats_values ) ) {
851 $result = Stats_Settings::update( $stats_values, $keys );
852 if ( is_wp_error( $result ) ) {
853 $result->add_data( array( 'status' => 400 ) );
854 return $result;
855 }
856 }
857
858 if ( isset( $params['wpcom_reader_views_enabled'] ) ) {
859 $reader_views = (int) $params['wpcom_reader_views_enabled'];
860 update_option( 'wpcom_reader_views_enabled', $reader_views );
861 // update_option() also returns false for an unchanged value, so read the option back.
862 if ( (int) get_option( 'wpcom_reader_views_enabled', 1 ) !== $reader_views ) {
863 return new WP_Error(
864 'jetpack_stats_save_failed',
865 __( 'The Stats settings could not be saved.', 'jetpack-stats-admin' ),
866 array( 'status' => 400 )
867 );
868 }
869 }
870
871 return $this->get_stats_settings_response();
872 }
873
874 /**
875 * The Stats settings the Settings screen offers.
876 *
877 * @return string[]
878 */
879 private static function get_stats_settings_keys() {
880 // Nothing reads `do_not_track`, so the screen does not offer it.
881 return array_values( array_diff( Stats_Settings::KEYS, array( 'do_not_track' ) ) );
882 }
883
884 /**
885 * Build the settings response: the current values and the roles the toggles list.
886 *
887 * @return array
888 */
889 private function get_stats_settings_response() {
890 if ( ! function_exists( 'get_editable_roles' ) ) {
891 require_once ABSPATH . 'wp-admin/includes/user.php';
892 }
893
894 $roles = array();
895 foreach ( get_editable_roles() as $slug => $role ) {
896 $roles[] = array(
897 'slug' => $slug,
898 'name' => translate_user_role( $role['name'] ),
899 );
900 }
901
902 return array(
903 'settings' => array_merge(
904 Stats_Settings::get( self::get_stats_settings_keys() ),
905 array( 'wpcom_reader_views_enabled' => (bool) get_option( 'wpcom_reader_views_enabled', true ) )
906 ),
907 'roles' => $roles,
908 );
909 }
910
911 /**
912 * Post user feedback for Jetpack Stats.
913 *
914 * @param WP_REST_Request $req The request object.
915 *
916 * @return array
917 */
918 public function post_user_feedback( $req ) {
919 $current_user = wp_get_current_user();
920 $body_from_req = json_decode( $req->get_body(), true );
921 $body_data = is_array( $body_from_req ) ? $body_from_req : array();
922 $user_email = $current_user->user_email;
923
924 return WPCOM_Client::request_as_blog_cached(
925 sprintf(
926 '/sites/%d/jetpack-stats/user-feedback?%s',
927 Jetpack_Options::get_option( 'id' ),
928 $this->filter_and_build_query_string(
929 $req->get_query_params()
930 )
931 ),
932 'v2',
933 array(
934 'timeout' => 5,
935 'method' => 'POST',
936 'headers' => array( 'Content-Type' => 'application/json' ),
937 ),
938 wp_json_encode(
939 array_merge(
940 $body_data,
941 array(
942 'user_email' => $user_email,
943 )
944 ),
945 JSON_UNESCAPED_SLASHES
946 ),
947 'wpcom'
948 );
949 }
950
951 /**
952 * Whether site has never published post.
953 *
954 * @param WP_REST_Request $req The request object.
955 * @return array
956 */
957 public function site_has_never_published_post( $req ) {
958 return WPCOM_Client::request_as_blog_cached(
959 sprintf(
960 '/sites/%d/site-has-never-published-post?%s',
961 Jetpack_Options::get_option( 'id' ),
962 $this->filter_and_build_query_string(
963 $req->get_params()
964 )
965 ),
966 'v2',
967 array( 'timeout' => 5 ),
968 null,
969 'wpcom'
970 );
971 }
972
973 /**
974 * Get detailed WordAds earnings information for the site.
975 *
976 * @param WP_REST_Request $req The request object.
977 * @return array
978 */
979 public function get_wordads_earnings( $req ) {
980 return WPCOM_Client::request_as_blog_cached(
981 sprintf(
982 '/sites/%d/wordads/earnings?%s',
983 Jetpack_Options::get_option( 'id' ),
984 $this->filter_and_build_query_string(
985 $req->get_params()
986 )
987 ),
988 'v1.1',
989 array( 'timeout' => 5 )
990 );
991 }
992
993 /**
994 * Get WordAds stats for the site.
995 *
996 * @param WP_REST_Request $req The request object.
997 * @return array
998 */
999 public function get_wordads_stats( $req ) {
1000 return WPCOM_Client::request_as_blog_cached(
1001 sprintf(
1002 '/sites/%d/wordads/stats?%s',
1003 Jetpack_Options::get_option( 'id' ),
1004 $this->filter_and_build_query_string(
1005 $req->get_params()
1006 )
1007 ),
1008 'v1.1',
1009 array( 'timeout' => 5 )
1010 );
1011 }
1012
1013 /**
1014 * Get Email stats as a list.
1015 *
1016 * @param WP_REST_Request $req The request object.
1017 * @return array
1018 */
1019 public function get_email_stats_list( $req ) {
1020 switch ( $req->get_param( 'resource' ) ) {
1021 case 'summary':
1022 return WPCOM_Client::request_as_blog_cached(
1023 sprintf(
1024 '/sites/%d/stats/emails/%s?%s',
1025 Jetpack_Options::get_option( 'id' ),
1026 $req->get_param( 'resource' ),
1027 $this->filter_and_build_query_string(
1028 $req->get_params()
1029 )
1030 ),
1031 'v1.1',
1032 array( 'timeout' => 5 )
1033 );
1034 default:
1035 return $this->get_forbidden_error();
1036 }
1037 }
1038
1039 /**
1040 * Get Email opens stats for a single post.
1041 *
1042 * @param WP_REST_Request $req The request object.
1043 * @return array
1044 */
1045 public function get_email_opens_stats_single( $req ) {
1046 switch ( $req->get_param( 'resource' ) ) {
1047 case 'client':
1048 case 'device':
1049 case 'country':
1050 case 'rate':
1051 return WPCOM_Client::request_as_blog_cached(
1052 sprintf(
1053 '/sites/%d/stats/opens/emails/%d/%s?%s',
1054 Jetpack_Options::get_option( 'id' ),
1055 $req->get_param( 'post_id' ),
1056 $req->get_param( 'resource' ),
1057 $this->filter_and_build_query_string(
1058 $req->get_params()
1059 )
1060 ),
1061 'v1.1',
1062 array( 'timeout' => 5 )
1063 );
1064 default:
1065 return $this->get_forbidden_error();
1066 }
1067 }
1068
1069 /**
1070 * Get Email clicks stats for a single post.
1071 *
1072 * @param WP_REST_Request $req The request object.
1073 * @return array
1074 */
1075 public function get_email_clicks_stats_single( $req ) {
1076 switch ( $req->get_param( 'resource' ) ) {
1077 case 'client':
1078 case 'device':
1079 case 'country':
1080 case 'rate':
1081 case 'link':
1082 case 'user-content-link':
1083 return WPCOM_Client::request_as_blog_cached(
1084 sprintf(
1085 '/sites/%d/stats/clicks/emails/%d/%s?%s',
1086 Jetpack_Options::get_option( 'id' ),
1087 $req->get_param( 'post_id' ),
1088 $req->get_param( 'resource' ),
1089 $this->filter_and_build_query_string(
1090 $req->get_params()
1091 )
1092 ),
1093 'v1.1',
1094 array( 'timeout' => 5 )
1095 );
1096 default:
1097 return $this->get_forbidden_error();
1098 }
1099 }
1100
1101 /**
1102 * Get Email stats time series.
1103 *
1104 * @param WP_REST_Request $req The request object.
1105 * @return array
1106 */
1107 public function get_email_stats_time_series( $req ) {
1108 switch ( $req->get_param( 'resource' ) ) {
1109 case 'opens':
1110 case 'clicks':
1111 return WPCOM_Client::request_as_blog_cached(
1112 sprintf(
1113 '/sites/%d/stats/%s/emails/%d?%s',
1114 Jetpack_Options::get_option( 'id' ),
1115 $req->get_param( 'resource' ),
1116 $req->get_param( 'post_id' ),
1117 $this->filter_and_build_query_string(
1118 $req->get_params()
1119 )
1120 ),
1121 'v1.1',
1122 array( 'timeout' => 5 )
1123 );
1124 default:
1125 return $this->get_forbidden_error();
1126 }
1127 }
1128
1129 /**
1130 * Get UTM stats time series.
1131 *
1132 * @param WP_REST_Request $req The request object.
1133 * @return array
1134 */
1135 public function get_utm_stats_time_series( $req ) {
1136 return WPCOM_Client::request_as_blog_cached(
1137 sprintf(
1138 '/sites/%d/stats/utm/%s?%s',
1139 Jetpack_Options::get_option( 'id' ),
1140 $req->get_param( 'utm_params' ),
1141 $this->filter_and_build_query_string(
1142 $req->get_params()
1143 )
1144 ),
1145 'v1.1',
1146 array( 'timeout' => 10 )
1147 );
1148 }
1149
1150 /**
1151 * Get Devices stats time series.
1152 *
1153 * @param WP_REST_Request $req The request object.
1154 * @return array
1155 */
1156 public function get_devices_stats_time_series( $req ) {
1157 return WPCOM_Client::request_as_blog_cached(
1158 sprintf(
1159 '/sites/%d/stats/devices/%s?%s',
1160 Jetpack_Options::get_option( 'id' ),
1161 $req->get_param( 'device_property' ),
1162 $this->filter_and_build_query_string(
1163 $req->get_params()
1164 )
1165 ),
1166 'v1.1',
1167 array( 'timeout' => 10 )
1168 );
1169 }
1170
1171 /**
1172 * Get Location stats.
1173 *
1174 * @param WP_REST_Request $req The request object.
1175 * @return array
1176 */
1177 public function get_location_stats( $req ) {
1178 $params = $req->get_params();
1179 $geo_mode = $params['geo_mode'];
1180 unset( $params['geo_mode'] );
1181
1182 return $this->wpcom_stats->get_views_by_location( $geo_mode, $params );
1183 }
1184
1185 /**
1186 * Dismiss or delay stats notices.
1187 *
1188 * @param WP_REST_Request $req The request object.
1189 * @return array
1190 */
1191 public function update_notice_status( $req ) {
1192 return ( new Notices() )->update_notice( $req->get_param( 'id' ), $req->get_param( 'status' ), $req->get_param( 'postponed_for' ) );
1193 }
1194
1195 /**
1196 * Get stats notices.
1197 *
1198 * @param WP_REST_Request $req The request object.
1199 * @return array
1200 */
1201 public function get_notice_status( $req ) {
1202 return ( new Notices() )->get_notices_to_show( (bool) $req->get_param( 'include_details' ) );
1203 }
1204
1205 /**
1206 * Get the list of spam referrers.
1207 *
1208 * @return array
1209 */
1210 public function get_referrer_spam_list() {
1211 return WPCOM_Client::request_as_blog(
1212 sprintf(
1213 '/sites/%d/stats/referrers/spam',
1214 Jetpack_Options::get_option( 'id' )
1215 ),
1216 'v1.1',
1217 array(
1218 'timeout' => 5,
1219 'method' => 'GET',
1220 )
1221 );
1222 }
1223
1224 /**
1225 * Mark a referrer as spam.
1226 *
1227 * @param WP_REST_Request $req The request object.
1228 * @return array
1229 */
1230 public function mark_referrer_spam( $req ) {
1231 return WPCOM_Client::request_as_blog(
1232 sprintf(
1233 '/sites/%d/stats/referrers/spam/new?%s',
1234 Jetpack_Options::get_option( 'id' ),
1235 $this->filter_and_build_query_string(
1236 $req->get_query_params()
1237 )
1238 ),
1239 'v1.1',
1240 array(
1241 'timeout' => 5,
1242 'method' => 'POST',
1243 )
1244 );
1245 }
1246
1247 /**
1248 * Unmark a referrer as spam.
1249 *
1250 * @param WP_REST_Request $req The request object.
1251 * @return array
1252 */
1253 public function unmark_referrer_spam( $req ) {
1254 return WPCOM_Client::request_as_blog(
1255 sprintf(
1256 '/sites/%d/stats/referrers/spam/delete?%s',
1257 Jetpack_Options::get_option( 'id' ),
1258 $this->filter_and_build_query_string(
1259 $req->get_query_params()
1260 )
1261 ),
1262 'v1.1',
1263 array(
1264 'timeout' => 5,
1265 'method' => 'POST',
1266 )
1267 );
1268 }
1269
1270 /**
1271 * Toggle modules on dashboard.
1272 *
1273 * @param WP_REST_Request $req The request object.
1274 * @return array
1275 */
1276 public function update_dashboard_modules( $req ) {
1277 // Clear dashboard modules cache.
1278 delete_transient( static::JETPACK_STATS_DASHBOARD_MODULES_CACHE_KEY );
1279 return WPCOM_Client::request_as_blog(
1280 sprintf(
1281 '/sites/%d/jetpack-stats-dashboard/modules?%s',
1282 Jetpack_Options::get_option( 'id' ),
1283 $this->filter_and_build_query_string(
1284 $req->get_query_params()
1285 )
1286 ),
1287 'v2',
1288 array(
1289 'timeout' => 5,
1290 'method' => 'POST',
1291 'headers' => array( 'Content-Type' => 'application/json' ),
1292 ),
1293 $req->get_body(),
1294 'wpcom'
1295 );
1296 }
1297
1298 /**
1299 * Get modules on dashboard.
1300 *
1301 * @param WP_REST_Request $req The request object.
1302 * @return array
1303 */
1304 public function get_dashboard_modules( $req ) {
1305 return WPCOM_Client::request_as_blog_cached(
1306 sprintf(
1307 '/sites/%d/jetpack-stats-dashboard/modules?%s',
1308 Jetpack_Options::get_option( 'id' ),
1309 $this->filter_and_build_query_string(
1310 $req->get_query_params()
1311 )
1312 ),
1313 'v2',
1314 array(
1315 'timeout' => 5,
1316 ),
1317 null,
1318 'wpcom',
1319 true,
1320 static::JETPACK_STATS_DASHBOARD_MODULES_CACHE_KEY
1321 );
1322 }
1323
1324 /**
1325 * Update module settings on dashboard.
1326 *
1327 * @param WP_REST_Request $req The request object.
1328 * @return array
1329 */
1330 public function update_dashboard_module_settings( $req ) {
1331 // Clear dashboard modules cache.
1332 delete_transient( static::JETPACK_STATS_DASHBOARD_MODULE_SETTINGS_CACHE_KEY );
1333 return WPCOM_Client::request_as_blog(
1334 sprintf(
1335 '/sites/%d/jetpack-stats-dashboard/module-settings?%s',
1336 Jetpack_Options::get_option( 'id' ),
1337 $this->filter_and_build_query_string(
1338 $req->get_query_params()
1339 )
1340 ),
1341 'v2',
1342 array(
1343 'timeout' => 5,
1344 'method' => 'POST',
1345 'headers' => array( 'Content-Type' => 'application/json' ),
1346 ),
1347 $req->get_body(),
1348 'wpcom'
1349 );
1350 }
1351
1352 /**
1353 * Get module settings on dashboard.
1354 *
1355 * @param WP_REST_Request $req The request object.
1356 * @return array
1357 */
1358 public function get_dashboard_module_settings( $req ) {
1359 return WPCOM_Client::request_as_blog_cached(
1360 sprintf(
1361 '/sites/%d/jetpack-stats-dashboard/module-settings?%s',
1362 Jetpack_Options::get_option( 'id' ),
1363 $this->filter_and_build_query_string(
1364 $req->get_query_params()
1365 )
1366 ),
1367 'v2',
1368 array(
1369 'timeout' => 5,
1370 ),
1371 null,
1372 'wpcom',
1373 true,
1374 static::JETPACK_STATS_DASHBOARD_MODULE_SETTINGS_CACHE_KEY
1375 );
1376 }
1377
1378 /**
1379 * Run commercial classification.
1380 *
1381 * @param WP_REST_Request $req The request object.
1382 * @return array
1383 */
1384 public function run_commercial_classification( $req ) {
1385 return WPCOM_Client::request_as_blog(
1386 sprintf(
1387 '/sites/%d/commercial-classification?%s',
1388 Jetpack_Options::get_option( 'id' ),
1389 $this->filter_and_build_query_string(
1390 $req->get_query_params()
1391 )
1392 ),
1393 'v2',
1394 array(
1395 'timeout' => 5,
1396 'method' => 'POST',
1397 ),
1398 null,
1399 'wpcom'
1400 );
1401 }
1402
1403 /**
1404 * Get purchases array; I don't see anything sensetive in there, so didn't sentinizie it.
1405 * Plus it is the same case as Jetpack.
1406 *
1407 * @param WP_REST_Request $req The request object.
1408 * @return array
1409 */
1410 public function get_site_purchases( $req ) {
1411 return WPCOM_Client::request_as_blog_cached(
1412 sprintf(
1413 '/upgrades?site=%d&%s',
1414 Jetpack_Options::get_option( 'id' ),
1415 $this->filter_and_build_query_string(
1416 $req->get_query_params()
1417 )
1418 ),
1419 'v1.2',
1420 array( 'timeout' => 10 ),
1421 null,
1422 'rest',
1423 false
1424 );
1425 }
1426
1427 /**
1428 * Return a WP_Error object with a forbidden error.
1429 */
1430 protected function get_forbidden_error() {
1431 $error_msg = esc_html__(
1432 'You are not allowed to perform this action.',
1433 'jetpack-stats-admin'
1434 );
1435
1436 return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) );
1437 }
1438
1439 /**
1440 * Filter and build query string from all the requested params.
1441 *
1442 * @param array $params The params to filter.
1443 * @param array $keys_to_unset The keys to unset from the params array.
1444 * @return string The filtered and built query string.
1445 */
1446 protected function filter_and_build_query_string( $params, $keys_to_unset = array() ) {
1447 if ( isset( $params['rest_route'] ) ) {
1448 unset( $params['rest_route'] );
1449 }
1450 if ( ! empty( $keys_to_unset ) && is_array( $keys_to_unset ) ) {
1451 foreach ( $keys_to_unset as $key ) {
1452 if ( isset( $params[ $key ] ) ) {
1453 unset( $params[ $key ] );
1454 }
1455 }
1456 }
1457 return http_build_query( $params );
1458 }
1459 }
1460