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