Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.89% covered (warning)
63.89%
568 / 889
40.48% covered (danger)
40.48%
17 / 42
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Controller
63.89% covered (warning)
63.89%
568 / 889
40.48% covered (danger)
40.48%
17 / 42
668.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register_rest_routes
100.00% covered (success)
100.00%
373 / 373
100.00% covered (success)
100.00%
1 / 1
1
 can_user_view_general_stats_callback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 can_user_manage_stats_settings_callback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 can_user_view_wordads_stats_callback
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 get_stats_resource
98.00% covered (success)
98.00%
49 / 50
0.00% covered (danger)
0.00%
0 / 1
21
 get_single_post_likes
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
4.25
 get_single_resource_stats
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 get_single_post
21.43% covered (danger)
21.43%
3 / 14
0.00% covered (danger)
0.00%
0 / 1
7.37
 get_site_stats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_site_posts
72.73% covered (warning)
72.73%
16 / 22
0.00% covered (danger)
0.00%
0 / 1
4.32
 get_site_subscribers_counts
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 get_site_plan_usage
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 get_stats_settings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_stats_settings
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 get_stats_settings_keys
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_stats_settings_response
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 post_user_feedback
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 site_has_never_published_post
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 get_wordads_earnings
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 get_wordads_stats
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 get_email_stats_list
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 get_email_opens_stats_single
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 get_email_clicks_stats_single
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 get_email_stats_time_series
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 get_utm_stats_time_series
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 get_devices_stats_time_series
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 get_location_stats
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 update_notice_status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_notice_status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_referrer_spam_list
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 mark_referrer_spam
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 unmark_referrer_spam
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 update_dashboard_modules
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 get_dashboard_modules
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 update_dashboard_module_settings
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 get_dashboard_module_settings
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 run_commercial_classification
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 get_site_purchases
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 get_forbidden_error
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 filter_and_build_query_string
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
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
9namespace Automattic\Jetpack\Stats_Admin;
10
11use Automattic\Jetpack\Constants;
12use Automattic\Jetpack\Stats\Settings as Stats_Settings;
13use Automattic\Jetpack\Stats\WPCOM_Stats;
14use Jetpack_Options;
15use WP_Error;
16use WP_REST_Request;
17use 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 */
23class 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                return $result;
854            }
855        }
856
857        if ( isset( $params['wpcom_reader_views_enabled'] ) ) {
858            $result = Stats_Settings::update_reader_views( (bool) $params['wpcom_reader_views_enabled'] );
859            if ( is_wp_error( $result ) ) {
860                return $result;
861            }
862        }
863
864        return $this->get_stats_settings_response();
865    }
866
867    /**
868     * The Stats settings the Settings screen offers.
869     *
870     * @return string[]
871     */
872    private static function get_stats_settings_keys() {
873        // Nothing reads `do_not_track`, so the screen does not offer it.
874        return array_values( array_diff( Stats_Settings::KEYS, array( 'do_not_track' ) ) );
875    }
876
877    /**
878     * Build the settings response: the current values and the roles the toggles list.
879     *
880     * @return array
881     */
882    private function get_stats_settings_response() {
883        if ( ! function_exists( 'get_editable_roles' ) ) {
884            require_once ABSPATH . 'wp-admin/includes/user.php';
885        }
886
887        $roles = array();
888        foreach ( get_editable_roles() as $slug => $role ) {
889            $roles[] = array(
890                'slug' => $slug,
891                'name' => translate_user_role( $role['name'] ),
892            );
893        }
894
895        return array(
896            'settings' => array_merge(
897                Stats_Settings::get( self::get_stats_settings_keys() ),
898                array( 'wpcom_reader_views_enabled' => (bool) get_option( 'wpcom_reader_views_enabled', true ) )
899            ),
900            'roles'    => $roles,
901        );
902    }
903
904    /**
905     * Post user feedback for Jetpack Stats.
906     *
907     * @param WP_REST_Request $req The request object.
908     *
909     * @return array
910     */
911    public function post_user_feedback( $req ) {
912        $current_user  = wp_get_current_user();
913        $body_from_req = json_decode( $req->get_body(), true );
914        $body_data     = is_array( $body_from_req ) ? $body_from_req : array();
915        $user_email    = $current_user->user_email;
916
917        return WPCOM_Client::request_as_blog_cached(
918            sprintf(
919                '/sites/%d/jetpack-stats/user-feedback?%s',
920                Jetpack_Options::get_option( 'id' ),
921                $this->filter_and_build_query_string(
922                    $req->get_query_params()
923                )
924            ),
925            'v2',
926            array(
927                'timeout' => 5,
928                'method'  => 'POST',
929                'headers' => array( 'Content-Type' => 'application/json' ),
930            ),
931            wp_json_encode(
932                array_merge(
933                    $body_data,
934                    array(
935                        'user_email' => $user_email,
936                    )
937                ),
938                JSON_UNESCAPED_SLASHES
939            ),
940            'wpcom'
941        );
942    }
943
944    /**
945     * Whether site has never published post.
946     *
947     * @param WP_REST_Request $req The request object.
948     * @return array
949     */
950    public function site_has_never_published_post( $req ) {
951        return WPCOM_Client::request_as_blog_cached(
952            sprintf(
953                '/sites/%d/site-has-never-published-post?%s',
954                Jetpack_Options::get_option( 'id' ),
955                $this->filter_and_build_query_string(
956                    $req->get_params()
957                )
958            ),
959            'v2',
960            array( 'timeout' => 5 ),
961            null,
962            'wpcom'
963        );
964    }
965
966    /**
967     * Get detailed WordAds earnings information for the site.
968     *
969     * @param WP_REST_Request $req The request object.
970     * @return array
971     */
972    public function get_wordads_earnings( $req ) {
973        return WPCOM_Client::request_as_blog_cached(
974            sprintf(
975                '/sites/%d/wordads/earnings?%s',
976                Jetpack_Options::get_option( 'id' ),
977                $this->filter_and_build_query_string(
978                    $req->get_params()
979                )
980            ),
981            'v1.1',
982            array( 'timeout' => 5 )
983        );
984    }
985
986    /**
987     * Get WordAds stats for the site.
988     *
989     * @param WP_REST_Request $req The request object.
990     * @return array
991     */
992    public function get_wordads_stats( $req ) {
993        return WPCOM_Client::request_as_blog_cached(
994            sprintf(
995                '/sites/%d/wordads/stats?%s',
996                Jetpack_Options::get_option( 'id' ),
997                $this->filter_and_build_query_string(
998                    $req->get_params()
999                )
1000            ),
1001            'v1.1',
1002            array( 'timeout' => 5 )
1003        );
1004    }
1005
1006    /**
1007     * Get Email stats as a list.
1008     *
1009     * @param WP_REST_Request $req The request object.
1010     * @return array
1011     */
1012    public function get_email_stats_list( $req ) {
1013        switch ( $req->get_param( 'resource' ) ) {
1014            case 'summary':
1015                return WPCOM_Client::request_as_blog_cached(
1016                    sprintf(
1017                        '/sites/%d/stats/emails/%s?%s',
1018                        Jetpack_Options::get_option( 'id' ),
1019                        $req->get_param( 'resource' ),
1020                        $this->filter_and_build_query_string(
1021                            $req->get_params()
1022                        )
1023                    ),
1024                    'v1.1',
1025                    array( 'timeout' => 5 )
1026                );
1027            default:
1028                return $this->get_forbidden_error();
1029        }
1030    }
1031
1032    /**
1033     * Get Email opens stats for a single post.
1034     *
1035     * @param WP_REST_Request $req The request object.
1036     * @return array
1037     */
1038    public function get_email_opens_stats_single( $req ) {
1039        switch ( $req->get_param( 'resource' ) ) {
1040            case 'client':
1041            case 'device':
1042            case 'country':
1043            case 'rate':
1044                return WPCOM_Client::request_as_blog_cached(
1045                    sprintf(
1046                        '/sites/%d/stats/opens/emails/%d/%s?%s',
1047                        Jetpack_Options::get_option( 'id' ),
1048                        $req->get_param( 'post_id' ),
1049                        $req->get_param( 'resource' ),
1050                        $this->filter_and_build_query_string(
1051                            $req->get_params()
1052                        )
1053                    ),
1054                    'v1.1',
1055                    array( 'timeout' => 5 )
1056                );
1057            default:
1058                return $this->get_forbidden_error();
1059        }
1060    }
1061
1062    /**
1063     * Get Email clicks stats for a single post.
1064     *
1065     * @param WP_REST_Request $req The request object.
1066     * @return array
1067     */
1068    public function get_email_clicks_stats_single( $req ) {
1069        switch ( $req->get_param( 'resource' ) ) {
1070            case 'client':
1071            case 'device':
1072            case 'country':
1073            case 'rate':
1074            case 'link':
1075            case 'user-content-link':
1076                return WPCOM_Client::request_as_blog_cached(
1077                    sprintf(
1078                        '/sites/%d/stats/clicks/emails/%d/%s?%s',
1079                        Jetpack_Options::get_option( 'id' ),
1080                        $req->get_param( 'post_id' ),
1081                        $req->get_param( 'resource' ),
1082                        $this->filter_and_build_query_string(
1083                            $req->get_params()
1084                        )
1085                    ),
1086                    'v1.1',
1087                    array( 'timeout' => 5 )
1088                );
1089            default:
1090                return $this->get_forbidden_error();
1091        }
1092    }
1093
1094    /**
1095     * Get Email stats time series.
1096     *
1097     * @param WP_REST_Request $req The request object.
1098     * @return array
1099     */
1100    public function get_email_stats_time_series( $req ) {
1101        switch ( $req->get_param( 'resource' ) ) {
1102            case 'opens':
1103            case 'clicks':
1104                return WPCOM_Client::request_as_blog_cached(
1105                    sprintf(
1106                        '/sites/%d/stats/%s/emails/%d?%s',
1107                        Jetpack_Options::get_option( 'id' ),
1108                        $req->get_param( 'resource' ),
1109                        $req->get_param( 'post_id' ),
1110                        $this->filter_and_build_query_string(
1111                            $req->get_params()
1112                        )
1113                    ),
1114                    'v1.1',
1115                    array( 'timeout' => 5 )
1116                );
1117            default:
1118                return $this->get_forbidden_error();
1119        }
1120    }
1121
1122    /**
1123     * Get UTM stats time series.
1124     *
1125     * @param WP_REST_Request $req The request object.
1126     * @return array
1127     */
1128    public function get_utm_stats_time_series( $req ) {
1129        return WPCOM_Client::request_as_blog_cached(
1130            sprintf(
1131                '/sites/%d/stats/utm/%s?%s',
1132                Jetpack_Options::get_option( 'id' ),
1133                $req->get_param( 'utm_params' ),
1134                $this->filter_and_build_query_string(
1135                    $req->get_params()
1136                )
1137            ),
1138            'v1.1',
1139            array( 'timeout' => 10 )
1140        );
1141    }
1142
1143    /**
1144     * Get Devices stats time series.
1145     *
1146     * @param WP_REST_Request $req The request object.
1147     * @return array
1148     */
1149    public function get_devices_stats_time_series( $req ) {
1150        return WPCOM_Client::request_as_blog_cached(
1151            sprintf(
1152                '/sites/%d/stats/devices/%s?%s',
1153                Jetpack_Options::get_option( 'id' ),
1154                $req->get_param( 'device_property' ),
1155                $this->filter_and_build_query_string(
1156                    $req->get_params()
1157                )
1158            ),
1159            'v1.1',
1160            array( 'timeout' => 10 )
1161        );
1162    }
1163
1164    /**
1165     * Get Location stats.
1166     *
1167     * @param WP_REST_Request $req The request object.
1168     * @return array
1169     */
1170    public function get_location_stats( $req ) {
1171        $params   = $req->get_params();
1172        $geo_mode = $params['geo_mode'];
1173        unset( $params['geo_mode'] );
1174
1175        return $this->wpcom_stats->get_views_by_location( $geo_mode, $params );
1176    }
1177
1178    /**
1179     * Dismiss or delay stats notices.
1180     *
1181     * @param WP_REST_Request $req The request object.
1182     * @return array
1183     */
1184    public function update_notice_status( $req ) {
1185        return ( new Notices() )->update_notice( $req->get_param( 'id' ), $req->get_param( 'status' ), $req->get_param( 'postponed_for' ) );
1186    }
1187
1188    /**
1189     * Get stats notices.
1190     *
1191     * @param WP_REST_Request $req The request object.
1192     * @return array
1193     */
1194    public function get_notice_status( $req ) {
1195        return ( new Notices() )->get_notices_to_show( (bool) $req->get_param( 'include_details' ) );
1196    }
1197
1198    /**
1199     * Get the list of spam referrers.
1200     *
1201     * @return array
1202     */
1203    public function get_referrer_spam_list() {
1204        return WPCOM_Client::request_as_blog(
1205            sprintf(
1206                '/sites/%d/stats/referrers/spam',
1207                Jetpack_Options::get_option( 'id' )
1208            ),
1209            'v1.1',
1210            array(
1211                'timeout' => 5,
1212                'method'  => 'GET',
1213            )
1214        );
1215    }
1216
1217    /**
1218     * Mark a referrer as spam.
1219     *
1220     * @param WP_REST_Request $req The request object.
1221     * @return array
1222     */
1223    public function mark_referrer_spam( $req ) {
1224        return WPCOM_Client::request_as_blog(
1225            sprintf(
1226                '/sites/%d/stats/referrers/spam/new?%s',
1227                Jetpack_Options::get_option( 'id' ),
1228                $this->filter_and_build_query_string(
1229                    $req->get_query_params()
1230                )
1231            ),
1232            'v1.1',
1233            array(
1234                'timeout' => 5,
1235                'method'  => 'POST',
1236            )
1237        );
1238    }
1239
1240    /**
1241     * Unmark a referrer as spam.
1242     *
1243     * @param WP_REST_Request $req The request object.
1244     * @return array
1245     */
1246    public function unmark_referrer_spam( $req ) {
1247        return WPCOM_Client::request_as_blog(
1248            sprintf(
1249                '/sites/%d/stats/referrers/spam/delete?%s',
1250                Jetpack_Options::get_option( 'id' ),
1251                $this->filter_and_build_query_string(
1252                    $req->get_query_params()
1253                )
1254            ),
1255            'v1.1',
1256            array(
1257                'timeout' => 5,
1258                'method'  => 'POST',
1259            )
1260        );
1261    }
1262
1263    /**
1264     * Toggle modules on dashboard.
1265     *
1266     * @param WP_REST_Request $req The request object.
1267     * @return array
1268     */
1269    public function update_dashboard_modules( $req ) {
1270        // Clear dashboard modules cache.
1271        delete_transient( static::JETPACK_STATS_DASHBOARD_MODULES_CACHE_KEY );
1272        return WPCOM_Client::request_as_blog(
1273            sprintf(
1274                '/sites/%d/jetpack-stats-dashboard/modules?%s',
1275                Jetpack_Options::get_option( 'id' ),
1276                $this->filter_and_build_query_string(
1277                    $req->get_query_params()
1278                )
1279            ),
1280            'v2',
1281            array(
1282                'timeout' => 5,
1283                'method'  => 'POST',
1284                'headers' => array( 'Content-Type' => 'application/json' ),
1285            ),
1286            $req->get_body(),
1287            'wpcom'
1288        );
1289    }
1290
1291    /**
1292     * Get modules on dashboard.
1293     *
1294     * @param WP_REST_Request $req The request object.
1295     * @return array
1296     */
1297    public function get_dashboard_modules( $req ) {
1298        return WPCOM_Client::request_as_blog_cached(
1299            sprintf(
1300                '/sites/%d/jetpack-stats-dashboard/modules?%s',
1301                Jetpack_Options::get_option( 'id' ),
1302                $this->filter_and_build_query_string(
1303                    $req->get_query_params()
1304                )
1305            ),
1306            'v2',
1307            array(
1308                'timeout' => 5,
1309            ),
1310            null,
1311            'wpcom',
1312            true,
1313            static::JETPACK_STATS_DASHBOARD_MODULES_CACHE_KEY
1314        );
1315    }
1316
1317    /**
1318     * Update module settings on dashboard.
1319     *
1320     * @param WP_REST_Request $req The request object.
1321     * @return array
1322     */
1323    public function update_dashboard_module_settings( $req ) {
1324        // Clear dashboard modules cache.
1325        delete_transient( static::JETPACK_STATS_DASHBOARD_MODULE_SETTINGS_CACHE_KEY );
1326        return WPCOM_Client::request_as_blog(
1327            sprintf(
1328                '/sites/%d/jetpack-stats-dashboard/module-settings?%s',
1329                Jetpack_Options::get_option( 'id' ),
1330                $this->filter_and_build_query_string(
1331                    $req->get_query_params()
1332                )
1333            ),
1334            'v2',
1335            array(
1336                'timeout' => 5,
1337                'method'  => 'POST',
1338                'headers' => array( 'Content-Type' => 'application/json' ),
1339            ),
1340            $req->get_body(),
1341            'wpcom'
1342        );
1343    }
1344
1345    /**
1346     * Get module settings on dashboard.
1347     *
1348     * @param WP_REST_Request $req The request object.
1349     * @return array
1350     */
1351    public function get_dashboard_module_settings( $req ) {
1352        return WPCOM_Client::request_as_blog_cached(
1353            sprintf(
1354                '/sites/%d/jetpack-stats-dashboard/module-settings?%s',
1355                Jetpack_Options::get_option( 'id' ),
1356                $this->filter_and_build_query_string(
1357                    $req->get_query_params()
1358                )
1359            ),
1360            'v2',
1361            array(
1362                'timeout' => 5,
1363            ),
1364            null,
1365            'wpcom',
1366            true,
1367            static::JETPACK_STATS_DASHBOARD_MODULE_SETTINGS_CACHE_KEY
1368        );
1369    }
1370
1371    /**
1372     * Run commercial classification.
1373     *
1374     * @param WP_REST_Request $req The request object.
1375     * @return array
1376     */
1377    public function run_commercial_classification( $req ) {
1378        return WPCOM_Client::request_as_blog(
1379            sprintf(
1380                '/sites/%d/commercial-classification?%s',
1381                Jetpack_Options::get_option( 'id' ),
1382                $this->filter_and_build_query_string(
1383                    $req->get_query_params()
1384                )
1385            ),
1386            'v2',
1387            array(
1388                'timeout' => 5,
1389                'method'  => 'POST',
1390            ),
1391            null,
1392            'wpcom'
1393        );
1394    }
1395
1396    /**
1397     * Get purchases array; I don't see anything sensetive in there, so didn't sentinizie it.
1398     * Plus it is the same case as Jetpack.
1399     *
1400     * @param WP_REST_Request $req The request object.
1401     * @return array
1402     */
1403    public function get_site_purchases( $req ) {
1404        return WPCOM_Client::request_as_blog_cached(
1405            sprintf(
1406                '/upgrades?site=%d&%s',
1407                Jetpack_Options::get_option( 'id' ),
1408                $this->filter_and_build_query_string(
1409                    $req->get_query_params()
1410                )
1411            ),
1412            'v1.2',
1413            array( 'timeout' => 10 ),
1414            null,
1415            'rest',
1416            false
1417        );
1418    }
1419
1420    /**
1421     * Return a WP_Error object with a forbidden error.
1422     */
1423    protected function get_forbidden_error() {
1424        $error_msg = esc_html__(
1425            'You are not allowed to perform this action.',
1426            'jetpack-stats-admin'
1427        );
1428
1429        return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) );
1430    }
1431
1432    /**
1433     * Filter and build query string from all the requested params.
1434     *
1435     * @param array $params The params to filter.
1436     * @param array $keys_to_unset The keys to unset from the params array.
1437     * @return string The filtered and built query string.
1438     */
1439    protected function filter_and_build_query_string( $params, $keys_to_unset = array() ) {
1440        if ( isset( $params['rest_route'] ) ) {
1441            unset( $params['rest_route'] );
1442        }
1443        if ( ! empty( $keys_to_unset ) && is_array( $keys_to_unset ) ) {
1444            foreach ( $keys_to_unset as $key ) {
1445                if ( isset( $params[ $key ] ) ) {
1446                    unset( $params[ $key ] );
1447                }
1448            }
1449        }
1450        return http_build_query( $params );
1451    }
1452}