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-wpcom-client.php +48 -7 12.5.2 → 16.3-beta View file →
@@ -16,8 +16,15 @@
16 16 * @package Automattic\Jetpack\Stats_Admin
17 17 */
18 18 class WPCOM_Client {
19 19 /**
20 + * Transient prefix for caching REST API responses.
21 + *
22 + * @var string
23 + */
24 + const CACHE_TRANSIENT_PREFIX = 'STATS_REST_RESP_';
25 +
26 + /**
20 27 * Query the WordPress.com REST API using the blog token cached.
21 28 *
22 29 * @param String $path The API endpoint relative path.
23 30 * @param String $version The API version.
@@ -29,12 +36,12 @@
29 36 * @return array|WP_Error $response Data.
30 37 */
31 38 public static function request_as_blog_cached( $path, $version = '1.1', $args = array(), $body = null, $base_api_path = 'rest', $use_cache = true, $cache_key = null ) {
32 39 // Only allow caching GET requests.
33 - $use_cache = $use_cache && ! ( isset( $args['method'] ) && strtoupper( $args['method'] ) !== 'GET' );
40 + $use_cache = $use_cache && ! ( isset( $args['method'] ) && strtoupper( $args['method'] ) !== 'GET' ) && ! static::should_bypass_cache();
34 41
35 42 // Arrays are serialized without considering the order of objects, but it's okay atm.
36 - $cache_key = $cache_key !== null ? $cache_key : 'STATS_REST_RESP_' . md5( implode( '|', array( $path, $version, wp_json_encode( $args ), wp_json_encode( $body ), $base_api_path ) ) );
43 + $cache_key ??= self::CACHE_TRANSIENT_PREFIX . md5( implode( '|', array( $path, $version, wp_json_encode( $args, JSON_UNESCAPED_SLASHES ), wp_json_encode( $body, JSON_UNESCAPED_SLASHES ), $base_api_path ) ) );
37 44
38 45 if ( $use_cache ) {
39 46 $response_body_content = get_transient( $cache_key );
40 47 if ( false !== $response_body_content ) {
@@ -47,12 +54,10 @@
47 54 if ( is_wp_error( $response_body ) ) {
48 55 return $response_body;
49 56 }
50 57
51 - if ( $use_cache ) {
52 - // Cache the successful JSON response for 5 minutes.
53 - set_transient( $cache_key, wp_json_encode( $response_body ), 5 * MINUTE_IN_SECONDS );
54 - }
58 + // Cache the response for 5 minutes.
59 + set_transient( $cache_key, wp_json_encode( $response_body, JSON_UNESCAPED_SLASHES ), 5 * MINUTE_IN_SECONDS );
55 60
56 61 return $response_body;
57 62 }
58 63
@@ -75,8 +80,21 @@
75 80 $base_api_path
76 81 );
77 82
78 83 if ( is_wp_error( $response ) ) {
84 + // `Client` fails before sending anything when the site holds no blog token, and that
85 + // error carries no status, which the REST API renders as a 500. Say what actually
86 + // happened so callers can tell an unconnected site from a broken one. Newer connection
87 + // packages name the reason (`no_possible_tokens`); older ones return `missing_token`.
88 + // `malformed_token` means the stored token has no secret half and cannot sign anything.
89 + if ( in_array( $response->get_error_code(), array( 'missing_token', 'no_possible_tokens', 'malformed_token' ), true ) ) {
90 + return new WP_Error(
91 + 'site_not_connected',
92 + __( 'This site is not connected to WordPress.com.', 'jetpack-stats-admin' ),
93 + array( 'status' => 400 )
94 + );
95 + }
96 +
79 97 return $response;
80 98 }
81 99
82 100 $response_code = wp_remote_retrieve_response_code( $response );
@@ -84,8 +102,17 @@
84 102 $response_body = json_decode( $response_body_content, true );
85 103
86 104 $error = static::get_wp_error( $response_body, (int) $response_code );
87 105 if ( is_wp_error( $error ) ) {
106 + // Unknown token keys and incorrect secrets also mean the site cannot authenticate.
107 + // Expose these rejections like a missing token so callers can identify the broken connection.
108 + if ( in_array( $error->get_error_code(), array( 'invalid_token', 'unknown_token', 'signature_mismatch' ), true ) ) {
109 + return new WP_Error(
110 + 'site_not_connected',
111 + __( 'This site is not connected to WordPress.com.', 'jetpack-stats-admin' ),
112 + array( 'status' => 400 )
113 + );
114 + }
88 115 return $error;
89 116 }
90 117
91 118 return $response_body;
@@ -110,9 +137,9 @@
110 137 // Sometimes the response code could be 200 but the response body still contains an error.
111 138 if ( $error_code !== null || $response_code !== 200 ) {
112 139 return new WP_Error(
113 140 $error_code,
114 - isset( $response_body['message'] ) ? $response_body['message'] : 'unknown remote error',
141 + $response_body['message'] ?? 'unknown remote error',
115 142 array( 'status' => $response_code )
116 143 );
117 144 }
118 145
@@ -117,6 +144,20 @@
117 144 }
118 145
119 146 // No error.
120 147 return null;
148 + }
149 +
150 + /**
151 + * Check if the cache should be bypassed.
152 + *
153 + * @return bool
154 + */
155 + protected static function should_bypass_cache() {
156 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
157 + return isset( $_GET['force_refresh'] ) || isset( $_GET['statsPurchaseSuccess'] ) ||
158 + // phpcs:ignore WordPress.Arrays.ArrayKeySpacingRestrictions.SpacesAroundArrayKeys, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
159 + ( isset( $_SERVER[ 'HTTP_REFERER' ] ) && false !== strpos( $_SERVER[ 'HTTP_REFERER' ], 'force_refresh' ) ) ||
160 + // phpcs:ignore WordPress.Arrays.ArrayKeySpacingRestrictions.SpacesAroundArrayKeys, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
161 + ( isset( $_SERVER[ 'HTTP_REFERER' ] ) && false !== strpos( $_SERVER[ 'HTTP_REFERER' ], 'statsPurchaseSuccess' ) );
121 162 }
122 163 }