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