Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.13% covered (warning)
64.13%
574 / 895
40.48% covered (danger)
40.48%
17 / 42
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Controller
64.13% covered (warning)
64.13%
574 / 895
40.48% covered (danger)
40.48%
17 / 42
657.15
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%
27 / 27
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                $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}