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