PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-stats-admin/src/class-rest-controller.php +332 -10 13.6.2 → 16.3-beta View file →
@@ -8,8 +8,9 @@
8 8
9 9 namespace Automattic\Jetpack\Stats_Admin;
10 10
11 11 use Automattic\Jetpack\Constants;
12 +use Automattic\Jetpack\Stats\Settings as Stats_Settings;
12 13 use Automattic\Jetpack\Stats\WPCOM_Stats;
13 14 use Jetpack_Options;
14 15 use WP_Error;
15 16 use WP_REST_Request;
@@ -44,8 +45,21 @@
44 45 $this->wpcom_stats = new WPCOM_Stats();
45 46 }
46 47
47 48 /**
49 + * Registers the REST routes on the `rest_api_init` hook.
50 + *
51 + * Instantiated here, rather than eagerly, so the controller class only loads
52 + * on requests that reach `rest_api_init`. Static so the callback can be
53 + * unregistered.
54 + *
55 + * @access public
56 + */
57 + public static function register() {
58 + ( new self() )->register_rest_routes();
59 + }
60 +
61 + /**
48 62 * Registers the REST routes for Odyssey Stats.
49 63 *
50 64 * Odyssey Stats is built from `wp-calypso`, which leverages the `public-api.wordpress.com` API.
51 65 * The current Site ID is added as part of the route, so that the front end doesn't have to handle the differences.
@@ -152,8 +166,58 @@
152 166 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
153 167 )
154 168 );
155 169
170 + // Stats settings.
171 + register_rest_route(
172 + static::$namespace,
173 + sprintf( '/sites/%d/jetpack-stats/settings', Jetpack_Options::get_option( 'id' ) ),
174 + array(
175 + array(
176 + 'methods' => WP_REST_Server::READABLE,
177 + 'callback' => array( $this, 'get_stats_settings' ),
178 + 'permission_callback' => array( $this, 'can_user_manage_stats_settings_callback' ),
179 + ),
180 + array(
181 + 'methods' => WP_REST_Server::EDITABLE,
182 + 'callback' => array( $this, 'update_stats_settings' ),
183 + 'permission_callback' => array( $this, 'can_user_manage_stats_settings_callback' ),
184 + 'args' => array(
185 + 'admin_bar' => array(
186 + 'description' => 'Show a chart of the last 48 hours of views in the admin bar',
187 + 'type' => 'boolean',
188 + ),
189 + 'roles' => array(
190 + 'description' => 'Roles that can view Stats. `administrator` is always kept.',
191 + 'type' => 'array',
192 + 'items' => array( 'type' => 'string' ),
193 + 'minItems' => 1,
194 + ),
195 + 'count_roles' => array(
196 + 'description' => 'Roles whose logged-in page views are counted',
197 + 'type' => 'array',
198 + 'items' => array( 'type' => 'string' ),
199 + ),
200 + 'wpcom_reader_views_enabled' => array(
201 + 'description' => 'Show post views in the WordPress.com Reader',
202 + 'type' => 'boolean',
203 + ),
204 + ),
205 + ),
206 + )
207 + );
208 +
209 + // User feedback endpoint.
210 + register_rest_route(
211 + static::$namespace,
212 + sprintf( '/sites/%d/jetpack-stats/user-feedback', Jetpack_Options::get_option( 'id' ) ),
213 + array(
214 + 'methods' => WP_REST_Server::CREATABLE,
215 + 'callback' => array( $this, 'post_user_feedback' ),
216 + 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
217 + )
218 + );
219 +
156 220 // WordAds Earnings.
157 221 register_rest_route(
158 222 static::$namespace,
159 223 sprintf( '/sites/%d/wordads/earnings', Jetpack_Options::get_option( 'id' ) ),
@@ -196,9 +260,9 @@
196 260 'description' => 'Status of the notice',
197 261 ),
198 262 'postponed_for' => array(
199 263 'type' => 'number',
200 - 'default' => null,
264 + 'default' => 0,
201 265 'description' => 'Postponed for (in seconds)',
202 266 'minimum' => 0,
203 267 ),
204 268 ),
@@ -225,9 +289,10 @@
225 289 'description' => 'Status of the notice',
226 290 ),
227 291 'postponed_for' => array(
228 292 'type' => 'number',
229 - 'default' => null,
293 + // Forwarded to WPCOM as-is, whose schema rejects the null an omitted param would carry.
294 + 'default' => 0,
230 295 'description' => 'Postponed for (in seconds)',
231 296 'minimum' => 0,
232 297 ),
233 298 ),
@@ -241,11 +306,29 @@
241 306 array(
242 307 'methods' => WP_REST_Server::READABLE,
243 308 'callback' => array( $this, 'get_notice_status' ),
244 309 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
310 + 'args' => array(
311 + 'include_details' => array(
312 + 'type' => 'boolean',
313 + 'default' => false,
314 + 'description' => 'Return a detail record per notice instead of a flat boolean map',
315 + ),
316 + ),
245 317 )
246 318 );
247 319
320 + // Get referrer spam list.
321 + register_rest_route(
322 + static::$namespace,
323 + sprintf( '/sites/%d/stats/referrers/spam', Jetpack_Options::get_option( 'id' ) ),
324 + array(
325 + 'methods' => WP_REST_Server::READABLE,
326 + 'callback' => array( $this, 'get_referrer_spam_list' ),
327 + 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
328 + )
329 + );
330 +
248 331 // Mark referrer spam.
249 332 register_rest_route(
250 333 static::$namespace,
251 334 sprintf( '/sites/%d/stats/referrers/spam/new', Jetpack_Options::get_option( 'id' ) ),
@@ -402,8 +485,30 @@
402 485 'callback' => array( $this, 'run_commercial_classification' ),
403 486 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
404 487 )
405 488 );
489 +
490 + // Purchases endpoint.
491 + register_rest_route(
492 + static::$namespace,
493 + sprintf( '/sites/%d/purchases', Jetpack_Options::get_option( 'id' ) ),
494 + array(
495 + 'methods' => WP_REST_Server::READABLE,
496 + 'callback' => array( $this, 'get_site_purchases' ),
497 + 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
498 + )
499 + );
500 +
501 + // Get Location stats.
502 + register_rest_route(
503 + static::$namespace,
504 + sprintf( '/sites/%d/stats/location-views/(?P<geo_mode>country|region|city)', Jetpack_Options::get_option( 'id' ) ),
505 + array(
506 + 'methods' => WP_REST_Server::READABLE,
507 + 'callback' => array( $this, 'get_location_stats' ),
508 + 'permission_callback' => array( $this, 'can_user_view_general_stats_callback' ),
509 + )
510 + );
406 511 }
407 512
408 513 /**
409 514 * Only administrators or users with capability `view_stats` can access the API.
@@ -418,8 +523,21 @@
418 523 return $this->get_forbidden_error();
419 524 }
420 525
421 526 /**
527 + * Only administrators can read or change the Stats settings, because `roles` decides who else can view Stats.
528 + *
529 + * @return bool|WP_Error
530 + */
531 + public function can_user_manage_stats_settings_callback() {
532 + if ( current_user_can( 'manage_options' ) ) {
533 + return true;
534 + }
535 +
536 + return $this->get_forbidden_error();
537 + }
538 +
539 + /**
422 540 * Only administrators or users with capability `activate_wordads` can access the API.
423 541 */
424 542 public function can_user_view_wordads_stats_callback() {
425 543 // phpcs:ignore WordPress.WP.Capabilities.Unknown
@@ -461,8 +579,11 @@
461 579
462 580 case 'top-posts':
463 581 return $this->wpcom_stats->get_top_posts( $req->get_params() );
464 582
583 + case 'archives':
584 + return $this->wpcom_stats->get_archives( $req->get_params() );
585 +
465 586 case 'publicize':
466 587 return $this->wpcom_stats->get_publicize_followers( $req->get_params() );
467 588
468 589 case 'followers':
@@ -536,9 +657,9 @@
536 657
537 658 if ( 200 !== $response_code ) {
538 659 return new WP_Error(
539 660 isset( $response_body['error'] ) ? 'remote-error-' . $response_body['error'] : 'remote-error',
540 - isset( $response_body['message'] ) ? $response_body['message'] : 'unknown remote error',
661 + $response_body['message'] ?? 'unknown remote error',
541 662 array( 'status' => $response_code )
542 663 );
543 664 }
544 665
@@ -581,13 +702,21 @@
581 702 if ( is_wp_error( $post ) || empty( $post ) ) {
582 703 return $post;
583 704 }
584 705
585 - // It shouldn't be a problem because only title and ID are exposed.
706 + // The endpoint should be as compatible as possible with `/sites/$site_id/posts/$post_id`.
707 + // The reason we are not forwarding the request is that `/sites/$site_id/posts/$post_id` might require user tokens for private posts/sites, which is not possible for users without a WordPress.com account.
708 + // 'like_count' is not included in the response because it's available through another endpoint `/sites/$site_id/posts/$post_id/likes`.
586 709 return array(
587 - 'ID' => $post->ID,
588 - 'title' => $post->post_title,
589 - 'URL' => get_permalink( $post->ID ),
710 + 'ID' => $post->ID,
711 + 'site_ID' => Jetpack_Options::get_option( 'id' ),
712 + 'title' => $post->post_title,
713 + 'URL' => get_permalink( $post->ID ),
714 + 'type' => $post->post_type,
715 + 'status' => $post->post_status,
716 + 'discussion' => array( 'comment_count' => intval( $post->comment_count ) ),
717 + 'date' => $post->post_date,
718 + 'post_thumbnail' => array( 'URL' => get_the_post_thumbnail_url( $post->ID ) ),
590 719 );
591 720 }
592 721
593 722 /**
@@ -629,9 +758,9 @@
629 758
630 759 if ( 200 !== $response_code ) {
631 760 return new WP_Error(
632 761 isset( $response_body['error'] ) ? 'remote-error-' . $response_body['error'] : 'remote-error',
633 - isset( $response_body['message'] ) ? $response_body['message'] : 'unknown remote error',
762 + $response_body['message'] ?? 'unknown remote error',
634 763 array( 'status' => $response_code )
635 764 );
636 765 }
637 766
@@ -679,8 +808,143 @@
679 808 ),
680 809 'v2',
681 810 array( 'timeout' => 5 ),
682 811 null,
812 + 'wpcom',
813 + false
814 + );
815 + }
816 +
817 + /**
818 + * Get the Stats settings and the site's roles.
819 + *
820 + * @return array
821 + */
822 + public function get_stats_settings() {
823 + return $this->get_stats_settings_response();
824 + }
825 +
826 + /**
827 + * Save the Stats settings in the request.
828 + *
829 + * @param WP_REST_Request $req The request object.
830 + *
831 + * @return array|WP_Error The settings after the save, or why the values were refused.
832 + */
833 + public function update_stats_settings( $req ) {
834 + $params = $req->get_params();
835 + $keys = self::get_stats_settings_keys();
836 +
837 + $stats_values = array_intersect_key( $params, array_flip( $keys ) );
838 + if ( empty( $stats_values ) && ! isset( $params['wpcom_reader_views_enabled'] ) ) {
839 + return new WP_Error(
840 + 'jetpack_stats_missing_setting_field',
841 + sprintf(
842 + /* translators: %s: comma-separated list of the settings that can be changed. */
843 + __( 'Provide at least one of: %s.', 'jetpack-stats-admin' ),
844 + implode( ', ', array_merge( $keys, array( 'wpcom_reader_views_enabled' ) ) )
845 + ),
846 + array( 'status' => 400 )
847 + );
848 + }
849 +
850 + if ( ! empty( $stats_values ) ) {
851 + $result = Stats_Settings::update( $stats_values, $keys );
852 + if ( is_wp_error( $result ) ) {
853 + $result->add_data( array( 'status' => 400 ) );
854 + return $result;
855 + }
856 + }
857 +
858 + if ( isset( $params['wpcom_reader_views_enabled'] ) ) {
859 + $reader_views = (int) $params['wpcom_reader_views_enabled'];
860 + update_option( 'wpcom_reader_views_enabled', $reader_views );
861 + // update_option() also returns false for an unchanged value, so read the option back.
862 + if ( (int) get_option( 'wpcom_reader_views_enabled', 1 ) !== $reader_views ) {
863 + return new WP_Error(
864 + 'jetpack_stats_save_failed',
865 + __( 'The Stats settings could not be saved.', 'jetpack-stats-admin' ),
866 + array( 'status' => 400 )
867 + );
868 + }
869 + }
870 +
871 + return $this->get_stats_settings_response();
872 + }
873 +
874 + /**
875 + * The Stats settings the Settings screen offers.
876 + *
877 + * @return string[]
878 + */
879 + private static function get_stats_settings_keys() {
880 + // Nothing reads `do_not_track`, so the screen does not offer it.
881 + return array_values( array_diff( Stats_Settings::KEYS, array( 'do_not_track' ) ) );
882 + }
883 +
884 + /**
885 + * Build the settings response: the current values and the roles the toggles list.
886 + *
887 + * @return array
888 + */
889 + private function get_stats_settings_response() {
890 + if ( ! function_exists( 'get_editable_roles' ) ) {
891 + require_once ABSPATH . 'wp-admin/includes/user.php';
892 + }
893 +
894 + $roles = array();
895 + foreach ( get_editable_roles() as $slug => $role ) {
896 + $roles[] = array(
897 + 'slug' => $slug,
898 + 'name' => translate_user_role( $role['name'] ),
899 + );
900 + }
901 +
902 + return array(
903 + 'settings' => array_merge(
904 + Stats_Settings::get( self::get_stats_settings_keys() ),
905 + array( 'wpcom_reader_views_enabled' => (bool) get_option( 'wpcom_reader_views_enabled', true ) )
906 + ),
907 + 'roles' => $roles,
908 + );
909 + }
910 +
911 + /**
912 + * Post user feedback for Jetpack Stats.
913 + *
914 + * @param WP_REST_Request $req The request object.
915 + *
916 + * @return array
917 + */
918 + public function post_user_feedback( $req ) {
919 + $current_user = wp_get_current_user();
920 + $body_from_req = json_decode( $req->get_body(), true );
921 + $body_data = is_array( $body_from_req ) ? $body_from_req : array();
922 + $user_email = $current_user->user_email;
923 +
924 + return WPCOM_Client::request_as_blog_cached(
925 + sprintf(
926 + '/sites/%d/jetpack-stats/user-feedback?%s',
927 + Jetpack_Options::get_option( 'id' ),
928 + $this->filter_and_build_query_string(
929 + $req->get_query_params()
930 + )
931 + ),
932 + 'v2',
933 + array(
934 + 'timeout' => 5,
935 + 'method' => 'POST',
936 + 'headers' => array( 'Content-Type' => 'application/json' ),
937 + ),
938 + wp_json_encode(
939 + array_merge(
940 + $body_data,
941 + array(
942 + 'user_email' => $user_email,
943 + )
944 + ),
945 + JSON_UNESCAPED_SLASHES
946 + ),
683 947 'wpcom'
684 948 );
685 949 }
686 950
@@ -904,8 +1168,22 @@
904 1168 );
905 1169 }
906 1170
907 1171 /**
1172 + * Get Location stats.
1173 + *
1174 + * @param WP_REST_Request $req The request object.
1175 + * @return array
1176 + */
1177 + public function get_location_stats( $req ) {
1178 + $params = $req->get_params();
1179 + $geo_mode = $params['geo_mode'];
1180 + unset( $params['geo_mode'] );
1181 +
1182 + return $this->wpcom_stats->get_views_by_location( $geo_mode, $params );
1183 + }
1184 +
1185 + /**
908 1186 * Dismiss or delay stats notices.
909 1187 *
910 1188 * @param WP_REST_Request $req The request object.
911 1189 * @return array
@@ -916,15 +1194,35 @@
916 1194
917 1195 /**
918 1196 * Get stats notices.
919 1197 *
1198 + * @param WP_REST_Request $req The request object.
920 1199 * @return array
921 1200 */
922 - public function get_notice_status() {
923 - return ( new Notices() )->get_notices_to_show();
1201 + public function get_notice_status( $req ) {
1202 + return ( new Notices() )->get_notices_to_show( (bool) $req->get_param( 'include_details' ) );
924 1203 }
925 1204
926 1205 /**
1206 + * Get the list of spam referrers.
1207 + *
1208 + * @return array
1209 + */
1210 + public function get_referrer_spam_list() {
1211 + return WPCOM_Client::request_as_blog(
1212 + sprintf(
1213 + '/sites/%d/stats/referrers/spam',
1214 + Jetpack_Options::get_option( 'id' )
1215 + ),
1216 + 'v1.1',
1217 + array(
1218 + 'timeout' => 5,
1219 + 'method' => 'GET',
1220 + )
1221 + );
1222 + }
1223 +
1224 + /**
927 1225 * Mark a referrer as spam.
928 1226 *
929 1227 * @param WP_REST_Request $req The request object.
930 1228 * @return array
@@ -1098,8 +1396,32 @@
1098 1396 'method' => 'POST',
1099 1397 ),
1100 1398 null,
1101 1399 'wpcom'
1400 + );
1401 + }
1402 +
1403 + /**
1404 + * Get purchases array; I don't see anything sensetive in there, so didn't sentinizie it.
1405 + * Plus it is the same case as Jetpack.
1406 + *
1407 + * @param WP_REST_Request $req The request object.
1408 + * @return array
1409 + */
1410 + public function get_site_purchases( $req ) {
1411 + return WPCOM_Client::request_as_blog_cached(
1412 + sprintf(
1413 + '/upgrades?site=%d&%s',
1414 + Jetpack_Options::get_option( 'id' ),
1415 + $this->filter_and_build_query_string(
1416 + $req->get_query_params()
1417 + )
1418 + ),
1419 + 'v1.2',
1420 + array( 'timeout' => 10 ),
1421 + null,
1422 + 'rest',
1423 + false
1102 1424 );
1103 1425 }
1104 1426
1105 1427 /**