PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.1-a.5
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 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-error-handler.php
jetpack / jetpack_vendor / automattic / jetpack-connection / src Last commit date
abilities 3 months ago connectors 2 months ago health 2 months ago identity-crisis 2 months ago sso 2 months ago traits 9 months ago webhooks 9 months ago class-authorize-json-api.php 2 months ago class-client.php 8 months ago class-connection-assets.php 1 year ago class-connection-notice.php 8 months ago class-error-handler.php 1 month ago class-external-storage.php 4 months ago class-heartbeat.php 1 month ago class-initial-state.php 1 month ago class-manager.php 1 month ago class-nonce-handler.php 9 months ago class-package-version-tracker.php 2 months ago class-package-version.php 1 month ago class-partner-coupon.php 3 months ago class-partner.php 2 years ago class-plugin-storage.php 8 months ago class-plugin.php 9 months ago class-rest-authentication.php 9 months ago class-rest-connector.php 1 month ago class-secrets.php 9 months ago class-server-sandbox.php 3 months ago class-site-health.php 3 months ago class-terms-of-service.php 2 years ago class-tokens-locks.php 9 months ago class-tokens.php 9 months ago class-tracking.php 3 months ago class-urls.php 6 months ago class-user-account-status.php 9 months ago class-users-connection-admin.php 3 months ago class-utils.php 2 years ago class-webhooks.php 2 months ago class-xmlrpc-async-call.php 2 years ago class-xmlrpc-connector.php 9 months ago interface-manager.php 4 years ago interface-storage-provider.php 6 months ago
class-error-handler.php
1220 lines
1 <?php
2 /**
3 * The Jetpack Connection error class file.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8 namespace Automattic\Jetpack\Connection;
9
10 /**
11 * The Jetpack Connection Errors that handles errors
12 *
13 * This class handles the following workflow for incoming XML-RPC and REST API requests:
14 *
15 * 1. An incoming XML-RPC or REST API request with an invalid signature triggers an error
16 * 2. Applies a gate to only process each error code once an hour to avoid overflow
17 * 3. It stores the error on the database, but we don't know yet if this is a valid error, because
18 * we can't confirm it came from WP.com.
19 * 4. It encrypts the error details and sends it to the wp.com server
20 * 5. wp.com checks it and, if valid, sends a new request back to this site using the verify_xml_rpc_error REST endpoint
21 * 6. This endpoint adds this error to the Verified errors in the database
22 * 7. Triggers a workflow depending on the error (display user an error message, do some self healing, etc.)
23 *
24 * Note: This class only handles authentication/signature errors from incoming requests to this site.
25 * Outgoing request signing issues (when this site makes requests to WP.com) are not handled here.
26 *
27 * Errors are stored in the database as options in the following format:
28 *
29 * [
30 * $error_code => [
31 * $user_id => [
32 * $error_details
33 * ]
34 * ]
35 * ]
36 *
37 * For each error code we store a maximum of 5 errors for 5 different user ids.
38 *
39 * A user ID can be:
40 * * 0 for blog tokens
41 * * positive integer for user tokens
42 * * 'invalid' for malformed tokens
43 *
44 * Example error structure:
45 * [
46 * 'invalid_token' => [
47 * '123' => [
48 * 'error_code' => 'invalid_token',
49 * 'user_id' => '123',
50 * 'error_message' => 'The token is invalid',
51 * 'error_data' => ['action' => 'reconnect'],
52 * 'timestamp' => 1234567890,
53 * 'nonce' => 'abc123def',
54 * 'error_type' => 'xmlrpc'
55 * ]
56 * ]
57 * ]
58 *
59 * @since 1.14.2
60 */
61 class Error_Handler {
62
63 /**
64 * The name of the option that stores the errors
65 *
66 * @since 1.14.2
67 *
68 * @var string
69 */
70 const STORED_ERRORS_OPTION = 'jetpack_connection_xmlrpc_errors';
71
72 /**
73 * The name of the option that stores the errors
74 *
75 * @since 1.14.2
76 *
77 * @var string
78 */
79 const STORED_VERIFIED_ERRORS_OPTION = 'jetpack_connection_xmlrpc_verified_errors';
80
81 /**
82 * The prefix of the transient that controls the gate for each error code
83 *
84 * @since 1.14.2
85 *
86 * @var string
87 */
88 const ERROR_REPORTING_GATE = 'jetpack_connection_error_reporting_gate_';
89
90 /**
91 * Time in seconds a test should live in the database before being discarded
92 *
93 * @since 1.14.2
94 */
95 const ERROR_LIFE_TIME = DAY_IN_SECONDS;
96
97 /**
98 * List of known errors. Only error codes in this list will be handled
99 *
100 * @since 1.14.2
101 *
102 * @var array
103 */
104 public $known_errors = array(
105 // Incoming request token problems (Manager::internal_verify_xml_rpc_signature).
106 'malformed_token', // Token in the request is empty/garbled, or its API version doesn't match ours.
107 'malformed_user_id', // The user_id segment of the request token is not numeric.
108 'unknown_user', // The request token's user does not exist on this site.
109 // Locally stored token problems (Tokens::get_access_token).
110 'no_user_tokens', // The user_tokens option is empty; no user tokens exist at all.
111 'empty_master_user_option', // The owner's token was requested but the master_user option is empty.
112 'no_token_for_user', // No stored token for the requested user.
113 'token_malformed', // The stored token for the requested user is corrupt (missing chunks).
114 'user_id_mismatch', // The requested user ID doesn't match the user_id segment of their stored token.
115 'no_possible_tokens', // No stored blog token.
116 'no_valid_user_token', // The stored user token doesn't match the key the request was signed with.
117 'no_valid_blog_token', // The stored blog token doesn't match the key the request was signed with.
118 'unknown_token', // No stored token matches the request token's key.
119 // Signature verification problems (Jetpack_Signature), or errors WPCOM returned
120 // for an outbound request (Error_Handler::check_api_response_for_errors).
121 'could_not_sign', // Signing the request failed for an unknown reason.
122 'invalid_scheme', // Invalid URL scheme when signing.
123 'invalid_secret', // The stored token secret is invalid.
124 'invalid_token', // No token available when signing; from WPCOM: the token used was rejected.
125 'token_mismatch', // The request token doesn't match the token we hold.
126 'invalid_body', // The request body is malformed.
127 'invalid_signature', // A signature parameter is malformed, or the timestamp is off (clock skew).
128 'invalid_body_hash', // The body hash doesn't match the request body.
129 'invalid_nonce', // The request nonce could not be added (likely a reuse/replay).
130 'signature_mismatch', // Computed signature differs: wrong secret, or URL/body drift (domain change, proxy).
131 // Connection state problems (Manager::get_connection_owner).
132 'invalid_connection_owner', // The connection owner cannot be resolved: token missing or WP user deleted.
133 );
134
135 /**
136 * Holds the instance of this singleton class
137 *
138 * @since 1.14.2
139 *
140 * @var Error_Handler $instance
141 */
142 public static $instance = null;
143
144 /**
145 * Cached displayable errors to avoid duplicate processing
146 *
147 * @since 6.13.10
148 *
149 * @var array|null
150 */
151 private $cached_displayable_errors = null;
152
153 /**
154 * Initialize instance, hooks and load verified errors handlers
155 *
156 * @since 1.14.2
157 */
158 private function __construct() {
159 defined( 'JETPACK__ERRORS_PUBLIC_KEY' ) || define( 'JETPACK__ERRORS_PUBLIC_KEY', 'KdZY80axKX+nWzfrOcizf0jqiFHnrWCl9X8yuaClKgM=' );
160
161 add_action( 'rest_api_init', array( $this, 'register_verify_error_endpoint' ) );
162
163 // Handle verified errors on admin pages.
164 add_action( 'admin_init', array( $this, 'handle_verified_errors' ) );
165
166 // If the site gets reconnected, clear errors.
167 add_action( 'jetpack_site_registered', array( $this, 'delete_all_errors' ) );
168 add_action( 'jetpack_get_site_data_success', array( $this, 'delete_all_api_errors' ) );
169 add_filter( 'jetpack_connection_disconnect_site_wpcom', array( $this, 'delete_all_errors_and_return_unfiltered_value' ) );
170 add_filter( 'jetpack_connection_delete_all_tokens', array( $this, 'delete_all_errors_and_return_unfiltered_value' ) );
171 add_action( 'jetpack_unlinked_user', array( $this, 'delete_all_errors' ) );
172 add_action( 'jetpack_updated_user_token', array( $this, 'delete_all_errors' ) );
173 }
174
175 /**
176 * Gets displayable errors with predefined structure and optional filtering.
177 *
178 * This method returns a hierarchical array of errors (error_code => user_id => error_details)
179 * that can be safely displayed in My Jetpack and other UI components. It includes
180 * predefined error messages and actions, with optional filtering for specific sites.
181 * Only processes a limited set of error codes that are meant to be displayed to users.
182 *
183 * error_data.action is only set when it deviates from the default behavior
184 * (e.g. 'none' to suppress the reconnect CTA); when absent, readers fall back
185 * to offering the reconnect CTA.
186 *
187 * @since 6.13.10
188 *
189 * @return array Array of displayable errors with hierarchical structure.
190 * Example:
191 * [
192 * 'invalid_token' => [
193 * '123' => [
194 * 'error_code' => 'invalid_token',
195 * 'user_id' => '123',
196 * 'error_message' => 'Your connection with WordPress.com seems to be broken...',
197 * 'audience' => 'user',
198 * 'error_data' => [...],
199 * 'timestamp' => 1234567890,
200 * 'nonce' => 'abc123def',
201 * 'error_type' => 'xmlrpc'
202 * ]
203 * ]
204 * ]
205 */
206 public function get_displayable_errors() {
207 $viewer_id = get_current_user_id();
208
209 // Check if we have a cached result for this viewer AND no filters are applied.
210 // The output is viewer-dependent (see audience classification below), so the
211 // cache is keyed by the current user.
212 if ( is_array( $this->cached_displayable_errors )
213 && array_key_exists( $viewer_id, $this->cached_displayable_errors )
214 && ! $this->has_external_filters() ) {
215 return $this->cached_displayable_errors[ $viewer_id ];
216 }
217
218 $verified_errors = $this->get_verified_errors();
219 $displayable_errors = array();
220
221 // The common case is zero verified errors: skip the owner/transferability
222 // lookups entirely then. The external filter below still runs so consumers
223 // (e.g. wpcomsh) can inject errors into an empty set.
224 if ( ! empty( $verified_errors ) ) {
225 // Only process error codes that are meant to be displayed to users.
226 // `no_user_tokens` is deliberately excluded: with an empty user_tokens option the
227 // site already behaves as site-only connected, and the connection UI prompts users
228 // to connect their accounts. The owner flavor is covered by `invalid_connection_owner`.
229 $displayable_error_codes = array(
230 'malformed_token',
231 'token_malformed',
232 'no_possible_tokens',
233 'no_valid_user_token',
234 'no_valid_blog_token',
235 'unknown_token',
236 'could_not_sign',
237 'invalid_token',
238 'token_mismatch',
239 'invalid_signature',
240 'signature_mismatch',
241 'no_token_for_user',
242 'invalid_connection_owner',
243 );
244
245 $owner_id = (int) \Jetpack_Options::get_option( 'master_user' );
246 $viewer_is_owner = $owner_id > 0 && $viewer_id === $owner_id;
247 $is_transferable = ( new Manager() )->is_ownership_transferable();
248
249 foreach ( $verified_errors as $error_code => $users ) {
250 // Skip error codes that are not meant to be displayed
251 if ( ! in_array( $error_code, $displayable_error_codes, true ) ) {
252 continue;
253 }
254
255 foreach ( $users as $user_id => $error ) {
256 // An error that cannot be attributed to the blog token or to any user's
257 // token belongs to no audience and is not actionable by any viewer.
258 if ( 'invalid' === $user_id ) {
259 continue;
260 }
261
262 $audience = $this->classify_error_audience( $user_id, $owner_id );
263
264 $message = __( "Your connection with WordPress.com seems to be broken. If you're experiencing issues, please try reconnecting.", 'jetpack-connection' );
265 $action = null;
266
267 // A secondary admin looking at the connection owner's token error, on a
268 // site where ownership is locked (a consumer declared it non-transferable).
269 // This admin cannot resolve the error themselves, so surface an
270 // informational notice naming the owner and offer no reconnect CTA.
271 if ( 'owner' === $audience && ! $viewer_is_owner && ! $is_transferable ) {
272 // Only name the owner for viewers who can act on connection issues:
273 // this output is also printed into the initial state for
274 // lower-capability users (e.g. contributors in the editor), who
275 // shouldn't learn who owns the connection. The name is resolved from
276 // the local user rather than get_connection_owner(), which
277 // re-reports the error and fails exactly when the token is broken.
278 $owner_name = '';
279 if ( current_user_can( 'jetpack_connect' ) ) {
280 $owner = get_userdata( $owner_id );
281 $owner_name = $owner instanceof \WP_User ? $owner->display_name : '';
282 }
283
284 $message = $owner_name
285 ? sprintf(
286 /* translators: %s is the display name of the Jetpack connection owner. */
287 __( 'The connection owner (%s) needs to reconnect their WordPress.com account to restore the connection.', 'jetpack-connection' ),
288 $owner_name
289 )
290 : __( 'The connection owner needs to reconnect their WordPress.com account to restore the connection.', 'jetpack-connection' );
291 $action = 'none';
292 }
293
294 $error['audience'] = $audience;
295 $error['error_message'] = $message;
296
297 // Only emit error_data.action when it deviates from the default. Readers
298 // already fall back to the reconnect CTA when no action is set, and
299 // injecting an explicit 'reconnect' could trip consumer code paths
300 // reserved for custom actions.
301 if ( null !== $action ) {
302 $error_data = ( isset( $error['error_data'] ) && is_array( $error['error_data'] ) ) ? $error['error_data'] : array();
303 $error_data['action'] = $action;
304 $error['error_data'] = $error_data;
305 }
306
307 if ( ! isset( $displayable_errors[ $error_code ] ) ) {
308 $displayable_errors[ $error_code ] = array();
309 }
310 $displayable_errors[ $error_code ][ $user_id ] = $error;
311 }
312 }
313 }
314
315 /**
316 * Filter displayable connection errors to allow customization of error messages and actions.
317 *
318 * This filter allows sites to customize how connection errors are displayed,
319 * including modifying error messages, actions, and data. Access to this filter
320 * is controlled by should_allow_error_filtering().
321 *
322 * Consumer-injected errors take precedence over the default state. They are not
323 * required to carry the newer `audience` field: it is optional metadata used
324 * only for our own audience-aware messaging, and any reader must treat a missing
325 * value as site-wide (`$error['audience'] ?? 'site'`).
326 *
327 * @since 6.12.0
328 *
329 * @param array $displayable_errors Array of displayable errors with hierarchical structure.
330 * @param array $verified_errors Array of raw verified errors from the database.
331 */
332 if ( $this->should_allow_error_filtering() ) {
333 $displayable_errors = apply_filters( 'jetpack_connection_get_verified_errors', $displayable_errors, $verified_errors );
334 }
335
336 // Only cache if no external filters are applied
337 if ( ! $this->has_external_filters() ) {
338 if ( ! is_array( $this->cached_displayable_errors ) ) {
339 $this->cached_displayable_errors = array();
340 }
341 $this->cached_displayable_errors[ $viewer_id ] = $displayable_errors;
342 }
343
344 return $displayable_errors;
345 }
346
347 /**
348 * Classifies the audience of a stored connection error based on its user ID.
349 *
350 * The audience determines who a connection error is relevant to and, in turn,
351 * how it should be surfaced:
352 * - `site` : blog-token / site-wide errors (user ID `0`).
353 * - `owner` : errors tied to the connection owner's user token.
354 * - `user` : errors tied to a specific (non-owner) user's token.
355 *
356 * Unattributable errors (user ID 'invalid') are skipped by the display pipeline
357 * before classification, so this method only receives numeric user IDs.
358 *
359 * @since 8.8.0
360 *
361 * @param string|int $user_id The user ID associated with the error (`0` or a positive integer).
362 * @param int $owner_id The local user ID of the connection owner, or 0 if there is none.
363 * @return string One of 'site', 'owner', or 'user'.
364 */
365 private function classify_error_audience( $user_id, $owner_id ) {
366 $user_id = (int) $user_id;
367
368 if ( 0 === $user_id ) {
369 return 'site';
370 }
371
372 if ( $owner_id > 0 && $user_id === $owner_id ) {
373 return 'owner';
374 }
375
376 return 'user';
377 }
378
379 /**
380 * Sets up hooks for displaying verified errors on admin pages.
381 *
382 * This method is hooked into 'admin_init'. It retrieves displayable errors
383 * and, if any exist, sets up the necessary action and filter hooks to display
384 * them in admin notices and the React dashboard.
385 *
386 * @since 1.14.2
387 */
388 public function handle_verified_errors() {
389 $displayable_errors = $this->get_displayable_errors();
390
391 // If there are any displayable errors, set up the hooks for displaying them in React dashboard and admin notices.
392 if ( ! empty( $displayable_errors ) ) {
393 add_action( 'admin_notices', array( $this, 'generic_admin_notice_error' ) );
394 add_filter( 'react_connection_errors_initial_state', array( $this, 'jetpack_react_dashboard_error' ), 10, 1 );
395 }
396 }
397
398 /**
399 * Determines whether error filtering should be allowed.
400 *
401 * This method controls access to the jetpack_connection_displayable_errors filter.
402 * Currently, only WoA sites are allowed to use this filter.
403 *
404 * @since 6.13.10
405 *
406 * @return bool True if error filtering should be allowed, false otherwise.
407 */
408 protected function should_allow_error_filtering() {
409 $host = new \Automattic\Jetpack\Status\Host();
410 if ( $host->is_woa_site() || $host->is_vip_site() || $host->is_newspack_site() ) {
411 return true;
412 }
413
414 return false;
415 }
416
417 /**
418 * Provides displayable connection errors for the React dashboard in a flat array format.
419 *
420 * This method transforms the hierarchical displayable_errors structure into the flat format
421 * expected by the React dashboard. It's used as a filter for 'react_connection_errors_initial_state'.
422 * Returns only the first error to avoid overwhelming the user with multiple error messages.
423 *
424 * @since 8.9.0
425 *
426 * @param array $errors Existing errors from other filters (unused but required for filter signature).
427 * @return array Array containing only the first displayable error for the React dashboard.
428 * Example:
429 * [
430 * [
431 * 'code' => 'connection_error',
432 * 'message' => 'Your connection with WordPress.com seems to be broken...',
433 * 'action' => 'reconnect',
434 * 'data' => [
435 * 'api_error_code' => 'invalid_token',
436 * 'action' => 'reconnect'
437 * ]
438 * ]
439 * ]
440 */
441 public function jetpack_react_dashboard_error( $errors ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
442 $displayable_errors = $this->get_displayable_errors();
443
444 // Get the first error only
445 $first_error_code = array_key_first( $displayable_errors );
446 if ( ! $first_error_code ) {
447 return array(); // No errors
448 }
449
450 $first_user_errors = $displayable_errors[ $first_error_code ];
451 if ( ! is_array( $first_user_errors ) || empty( $first_user_errors ) ) {
452 return array(); // Invalid error structure
453 }
454
455 $first_error = reset( $first_user_errors );
456
457 // Validate error structure
458 if ( ! is_array( $first_error ) || ! isset( $first_error['error_message'] ) ) {
459 return array(); // Invalid error structure
460 }
461
462 // Determine the action - use the one from error_data if available, otherwise default to 'reconnect'
463 $action = 'reconnect'; // Default action for connection errors
464 if ( isset( $first_error['error_data']['action'] ) && is_string( $first_error['error_data']['action'] ) ) {
465 $action = $first_error['error_data']['action'];
466 }
467
468 // Safely merge error data, ensuring we don't overwrite critical fields
469 $error_data = isset( $first_error['error_data'] ) && is_array( $first_error['error_data'] ) ? $first_error['error_data'] : array();
470
471 // Build the data array with safe merging
472 $dashboard_data = array( 'api_error_code' => $first_error_code );
473
474 // Add error_data fields, but be careful not to overwrite api_error_code
475 foreach ( $error_data as $key => $value ) {
476 if ( 'api_error_code' !== $key ) {
477 $dashboard_data[ $key ] = $value;
478 }
479 }
480
481 $dashboard_error = array(
482 array(
483 'code' => 'connection_error',
484 'message' => $first_error['error_message'],
485 'action' => $action,
486 'data' => $dashboard_data,
487 ),
488 );
489
490 return $dashboard_error;
491 }
492
493 /**
494 * Gets the instance of this singleton class
495 *
496 * @since 1.14.2
497 *
498 * @return Error_Handler $instance
499 */
500 public static function get_instance() {
501 if ( self::$instance === null ) {
502 self::$instance = new self();
503 }
504 return self::$instance;
505 }
506
507 /**
508 * Keep track of a connection error that was encountered
509 *
510 * @param \WP_Error $error The error object.
511 * @param boolean $force Force the report, even if should_report_error is false.
512 * @param boolean $skip_wpcom_verification Set to 'true' to verify the error locally and skip the WP.com verification.
513 *
514 * @return void
515 * @since 1.14.2
516 */
517 public function report_error( \WP_Error $error, $force = false, $skip_wpcom_verification = false ) {
518 if ( in_array( $error->get_error_code(), $this->known_errors, true ) && ( $this->should_report_error( $error ) || $force ) ) {
519 $stored_error = $this->store_error( $error );
520 if ( $stored_error ) {
521 $skip_wpcom_verification ? $this->verify_error( $stored_error ) : $this->send_error_to_wpcom( $stored_error );
522 }
523 }
524 }
525
526 /**
527 * Checks the status of the gate
528 *
529 * This protects the site (and WPCOM) against over loads.
530 *
531 * @since 1.14.2
532 *
533 * @param \WP_Error $error the error object.
534 * @return boolean $should_report True if gate is open and the error should be reported.
535 */
536 public function should_report_error( \WP_Error $error ) {
537 if ( defined( '\\JETPACK_DEV_DEBUG' ) && constant( '\\JETPACK_DEV_DEBUG' ) ) {
538 return true;
539 }
540
541 /**
542 * Whether to bypass the gate for the error handling
543 *
544 * By default, we only process errors once an hour for each error code.
545 * This is done to avoid overflows. If you need to disable this gate, you can set this variable to true.
546 *
547 * This filter is useful for unit testing
548 *
549 * @since 1.14.2
550 *
551 * @param boolean $bypass_gate whether to bypass the gate. Default is false, do not bypass.
552 */
553 $bypass_gate = apply_filters( 'jetpack_connection_bypass_error_reporting_gate', false );
554 if ( true === $bypass_gate ) {
555 return true;
556 }
557
558 $transient = self::ERROR_REPORTING_GATE . $error->get_error_code();
559
560 if ( get_transient( $transient ) ) {
561 return false;
562 }
563
564 set_transient( $transient, true, HOUR_IN_SECONDS );
565 return true;
566 }
567
568 /**
569 * Stores the error in the database so we know there is an issue and can inform the user
570 *
571 * @since 1.14.2
572 *
573 * @param \WP_Error $error the error object.
574 * @return boolean|array False if stored errors were not updated and the error array if it was successfully stored.
575 */
576 public function store_error( \WP_Error $error ) {
577
578 $stored_errors = $this->get_stored_errors();
579 $error_array = $this->wp_error_to_array( $error );
580 $error_code = $error->get_error_code();
581 $user_id = $error_array['user_id'];
582
583 if ( ! isset( $stored_errors[ $error_code ] ) || ! is_array( $stored_errors[ $error_code ] ) ) {
584 $stored_errors[ $error_code ] = array();
585 }
586
587 $stored_errors[ $error_code ][ $user_id ] = $error_array;
588
589 // Let's store a maximum of 5 different user ids for each error code.
590 $error_code_count = is_countable( $stored_errors[ $error_code ] ) ? count( $stored_errors[ $error_code ] ) : 0;
591 if ( $error_code_count > 5 ) {
592 // array_shift will destroy keys here because they are numeric, so manually remove first item.
593 $keys = array_keys( $stored_errors[ $error_code ] );
594 unset( $stored_errors[ $error_code ][ $keys[0] ] );
595 }
596
597 if ( update_option( self::STORED_ERRORS_OPTION, $stored_errors ) ) {
598 return $error_array;
599 }
600
601 return false;
602 }
603
604 /**
605 * Builds action error data for generic JavaScript components.
606 *
607 * This helper method creates standardized error_data arrays that work with the generic
608 * JavaScript error handling components. External plugins (like wpcomsh) can use this
609 * to ensure their error structures are compatible.
610 *
611 * @since 6.16.0
612 *
613 * @param array $args Action configuration arguments - only non-empty values will be included.
614 * @return array Standardized error_data array for JavaScript components.
615 */
616 public function build_action_error_data( array $args = array() ) {
617 // Set default values for variants
618 $args = wp_parse_args(
619 $args,
620 array(
621 'action_variant' => 'primary',
622 'secondary_action_variant' => 'secondary',
623 )
624 );
625
626 // Start with core data
627 $error_data = array(
628 'blog_id' => \Jetpack_Options::get_option( 'id' ),
629 );
630
631 // Validate variant values
632 $valid_variants = array( 'primary', 'secondary' );
633 if ( ! in_array( $args['action_variant'], $valid_variants, true ) ) {
634 $args['action_variant'] = 'primary';
635 }
636 if ( ! in_array( $args['secondary_action_variant'], $valid_variants, true ) ) {
637 $args['secondary_action_variant'] = 'secondary';
638 }
639
640 // Merge extra_data first, then regular args (so args take precedence)
641 if ( ! empty( $args['extra_data'] ) && is_array( $args['extra_data'] ) ) {
642 $error_data = array_merge( $error_data, $args['extra_data'] );
643 unset( $args['extra_data'] ); // Remove from args to avoid duplication
644 }
645
646 // Filter out empty values and merge with error_data
647 $filtered_args = array_filter(
648 $args,
649 function ( $value ) {
650 return ! empty( $value );
651 }
652 );
653
654 return array_merge( $error_data, $filtered_args );
655 }
656
657 /**
658 * Builds a standardized error array for the connection error system.
659 *
660 * This method creates a consistent error array structure that can be used
661 * by both internal error handling and external plugins/customizations.
662 *
663 * @since 1.14.2
664 *
665 * @param string $error_code The error code identifier.
666 * @param string $error_message The human-readable error message.
667 * @param array $error_data Additional error data (optional).
668 * @param string $user_id The user ID associated with the error (optional).
669 * @param string $error_type The type of error (optional).
670 * @return array|false The standardized error array or false on failure.
671 * Example successful return:
672 * [
673 * 'error_code' => 'invalid_token',
674 * 'user_id' => '123',
675 * 'error_message' => 'The token is invalid',
676 * 'error_data' => ['action' => 'reconnect'],
677 * 'timestamp' => 1234567890,
678 * 'nonce' => 'abc123def',
679 * 'error_type' => 'xmlrpc'
680 * ]
681 */
682 public function build_error_array( string $error_code, string $error_message, array $error_data = array(), $user_id = '0', string $error_type = '' ) {
683 // Validate required parameters
684 if ( empty( $error_code ) || empty( $error_message ) ) {
685 return false;
686 }
687
688 // Validate user_id is a string or integer
689 if ( ! is_string( $user_id ) && ! is_int( $user_id ) ) {
690 return false;
691 }
692
693 return array(
694 'error_code' => $error_code,
695 'user_id' => $user_id,
696 'error_message' => $error_message,
697 'error_data' => $error_data,
698 'timestamp' => time(),
699 'nonce' => wp_generate_password( 10, false ),
700 'error_type' => $error_type,
701 );
702 }
703
704 /**
705 * Converts a WP_Error object in the array representation we store in the database
706 *
707 * The user attribution comes from the token in `signature_details`, which identifies
708 * the exact credential that failed. An explicit `user_id` in the error data is only
709 * consulted as a fallback when the token yields no user (e.g. non-signature errors
710 * such as `invalid_connection_owner`, which are reported with an empty token).
711 *
712 * @since 1.14.2
713 *
714 * @param \WP_Error $error the error object.
715 * @return boolean|array False if error is invalid or the error array
716 */
717 public function wp_error_to_array( \WP_Error $error ) {
718
719 $data = $error->get_error_data();
720
721 if ( ! isset( $data['signature_details'] ) || ! is_array( $data['signature_details'] ) ) {
722 return false;
723 }
724
725 $signature_details = $data['signature_details'];
726
727 if ( ! isset( $signature_details['token'] ) ) {
728 return false;
729 }
730
731 $user_id = $this->get_user_id_from_token( $signature_details['token'] );
732
733 if ( 'invalid' === $user_id && isset( $data['user_id'] ) && is_numeric( $data['user_id'] ) ) {
734 $user_id = (string) (int) $data['user_id'];
735 }
736
737 $error_data = $signature_details;
738
739 // For invalid_connection_owner, has_user_token distinguishes a missing owner
740 // token from a deleted owner WP user. Keep it so display code can tell the
741 // two flavors apart.
742 if ( isset( $data['has_user_token'] ) ) {
743 $error_data['has_user_token'] = (bool) $data['has_user_token'];
744 }
745
746 return $this->build_error_array(
747 $error->get_error_code(),
748 $error->get_error_message(),
749 $error_data,
750 $user_id,
751 empty( $data['error_type'] ) ? '' : $data['error_type']
752 );
753 }
754
755 /**
756 * Sends the error to WP.com to be verified
757 *
758 * @since 1.14.2
759 *
760 * @param array $error_array The array representation of the error as it is stored in the database.
761 * @return bool
762 */
763 public function send_error_to_wpcom( $error_array ) {
764
765 $blog_id = \Jetpack_Options::get_option( 'id' );
766
767 $encrypted_data = $this->encrypt_data_to_wpcom( $error_array );
768
769 if ( false === $encrypted_data ) {
770 return false;
771 }
772
773 $args = array(
774 'body' => array(
775 'error_data' => $encrypted_data,
776 ),
777 );
778
779 // send encrypted data to WP.com Public-API v2.
780 wp_remote_post( "https://public-api.wordpress.com/wpcom/v2/sites/{$blog_id}/jetpack-report-error/", $args );
781 return true;
782 }
783
784 /**
785 * Encrypt data to be sent over to WP.com
786 *
787 * @since 1.14.2
788 *
789 * @param array|string $data the data to be encoded.
790 * @return boolean|string The encoded string on success, false on failure
791 */
792 public function encrypt_data_to_wpcom( $data ) {
793
794 try {
795 // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
796 // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
797 $encrypted_data = base64_encode( sodium_crypto_box_seal( wp_json_encode( $data, JSON_UNESCAPED_SLASHES ), base64_decode( JETPACK__ERRORS_PUBLIC_KEY ) ) );
798 // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
799 // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
800 } catch ( \SodiumException $e ) {
801 // error encrypting data.
802 return false;
803 }
804
805 return $encrypted_data;
806 }
807
808 /**
809 * Extracts the user ID from a token
810 *
811 * @since 1.14.2
812 *
813 * @param string $token the token used to make the request.
814 * @return string $the user id or `invalid` if user id not present.
815 */
816 public function get_user_id_from_token( $token ) {
817 $user_id = 'invalid';
818
819 if ( $token ) {
820 $parsed_token = explode( ':', wp_unslash( $token ) );
821
822 if ( isset( $parsed_token[2] ) && ctype_digit( $parsed_token[2] ) ) {
823 $user_id = $parsed_token[2];
824 }
825 }
826
827 return $user_id;
828 }
829
830 /**
831 * Gets the reported errors stored in the database
832 *
833 * @since 1.14.2
834 *
835 * @return array $errors
836 */
837 public function get_stored_errors() {
838
839 $stored_errors = get_option( self::STORED_ERRORS_OPTION );
840
841 if ( ! is_array( $stored_errors ) ) {
842 $stored_errors = array();
843 }
844
845 $stored_errors = $this->garbage_collector( $stored_errors );
846
847 return $stored_errors;
848 }
849
850 /**
851 * Gets the verified errors stored in the database.
852 *
853 * This method retrieves only the errors that are actually stored in the database,
854 * without applying any filters that might inject additional errors. This is used
855 * internally by methods that need to modify and store the verified errors back
856 * to the database to prevent accidentally persisting filtered/injected errors.
857 *
858 * @since 1.14.2
859 *
860 * @return array $errors
861 */
862 public function get_verified_errors() {
863 $verified_errors = get_option( self::STORED_VERIFIED_ERRORS_OPTION );
864
865 if ( ! is_array( $verified_errors ) ) {
866 $verified_errors = array();
867 }
868
869 $verified_errors = $this->garbage_collector( $verified_errors );
870
871 return $verified_errors;
872 }
873
874 /**
875 * Removes expired errors from the array
876 *
877 * This method is called by get_stored_errors and get_verified errors and filters their result
878 * Whenever a new error is stored to the database or verified, this will be triggered and the
879 * expired error will be permanently removed from the database
880 *
881 * @since 1.14.2
882 *
883 * @param array $errors array of errors as stored in the database.
884 * @return array
885 */
886 private function garbage_collector( $errors ) {
887 foreach ( $errors as $error_code => $users ) {
888 foreach ( $users as $user_id => $error ) {
889 if ( empty( $error['timestamp'] ) || self::ERROR_LIFE_TIME < time() - (int) $error['timestamp'] ) {
890 unset( $errors[ $error_code ][ $user_id ] );
891 }
892 }
893 }
894 // Clear empty error codes.
895 $errors = array_filter(
896 $errors,
897 function ( $user_errors ) {
898 return ! empty( $user_errors );
899 }
900 );
901 return $errors;
902 }
903
904 /**
905 * Delete all stored and verified errors from the database
906 *
907 * @since 1.14.2
908 *
909 * @return void
910 */
911 public function delete_all_errors() {
912 $this->delete_stored_errors();
913 $this->delete_verified_errors();
914
915 // Invalidate cache since we deleted all errors
916 $this->invalidate_displayable_errors_cache();
917 }
918
919 /**
920 * Delete all stored and verified API errors from the database, leave the non-API errors intact.
921 *
922 * @since 1.54.0
923 *
924 * @return void
925 */
926 public function delete_all_api_errors() {
927 $type_filter = function ( $errors ) {
928 if ( is_array( $errors ) ) {
929 foreach ( $errors as $key => $error ) {
930 if ( ! empty( $error['error_type'] ) && in_array( $error['error_type'], array( 'xmlrpc', 'rest' ), true ) ) {
931 unset( $errors[ $key ] );
932 }
933 }
934 }
935
936 return count( $errors ) ? $errors : null;
937 };
938
939 $stored_errors = $this->get_stored_errors();
940 if ( is_array( $stored_errors ) && count( $stored_errors ) ) {
941 $stored_errors = array_filter( array_map( $type_filter, $stored_errors ) );
942 if ( count( $stored_errors ) ) {
943 update_option( static::STORED_ERRORS_OPTION, $stored_errors );
944 } else {
945 delete_option( static::STORED_ERRORS_OPTION );
946 }
947 }
948
949 $verified_errors = $this->get_verified_errors();
950 if ( is_array( $verified_errors ) && count( $verified_errors ) ) {
951 $verified_errors = array_filter( array_map( $type_filter, $verified_errors ) );
952 if ( count( $verified_errors ) ) {
953 update_option( static::STORED_VERIFIED_ERRORS_OPTION, $verified_errors );
954 } else {
955 delete_option( static::STORED_VERIFIED_ERRORS_OPTION );
956 }
957 }
958
959 // Invalidate cache since we may have deleted verified errors
960 $this->invalidate_displayable_errors_cache();
961 }
962
963 /**
964 * Delete all stored and verified errors from the database and returns unfiltered value
965 *
966 * This is used to hook into a couple of filters that expect true to not short circuit the disconnection flow
967 *
968 * @since 8.9.0
969 *
970 * @param mixed $check The input sent by the filter.
971 * @return boolean
972 */
973 public function delete_all_errors_and_return_unfiltered_value( $check ) {
974 $this->delete_all_errors();
975 return $check;
976 }
977
978 /**
979 * Delete the reported errors stored in the database
980 *
981 * @since 1.14.2
982 *
983 * @return boolean True, if option is successfully deleted. False on failure.
984 */
985 public function delete_stored_errors() {
986 return delete_option( self::STORED_ERRORS_OPTION );
987 }
988
989 /**
990 * Delete the verified errors stored in the database
991 *
992 * @since 1.14.2
993 *
994 * @return boolean True, if option is successfully deleted. False on failure.
995 */
996 public function delete_verified_errors() {
997 return delete_option( self::STORED_VERIFIED_ERRORS_OPTION );
998 }
999
1000 /**
1001 * Gets an error based on the nonce
1002 *
1003 * Receives a nonce and finds the related error.
1004 *
1005 * @since 1.14.2
1006 *
1007 * @param string $nonce The nonce created for the error we want to get.
1008 * @return null|array Returns the error array representation or null if error not found.
1009 */
1010 public function get_error_by_nonce( $nonce ) {
1011 $errors = $this->get_stored_errors();
1012 foreach ( $errors as $user_group ) {
1013 foreach ( $user_group as $error ) {
1014 if ( $error['nonce'] === $nonce ) {
1015 return $error;
1016 }
1017 }
1018 }
1019 return null;
1020 }
1021
1022 /**
1023 * Adds an error to the verified error list
1024 *
1025 * @since 1.14.2
1026 *
1027 * @param array $error The error array, as it was saved in the unverified errors list.
1028 * @return void
1029 */
1030 public function verify_error( $error ) {
1031
1032 $verified_errors = $this->get_verified_errors();
1033 $error_code = $error['error_code'];
1034 $user_id = $error['user_id'];
1035
1036 if ( ! isset( $verified_errors[ $error_code ] ) ) {
1037 $verified_errors[ $error_code ] = array();
1038 }
1039
1040 $verified_errors[ $error_code ][ $user_id ] = $error;
1041
1042 update_option( self::STORED_VERIFIED_ERRORS_OPTION, $verified_errors );
1043
1044 // Invalidate cache since we added a new verified error
1045 $this->invalidate_displayable_errors_cache();
1046 }
1047
1048 /**
1049 * Register REST API end point for error handling.
1050 *
1051 * @since 1.14.2
1052 *
1053 * @return void
1054 */
1055 public function register_verify_error_endpoint() {
1056 register_rest_route(
1057 'jetpack/v4',
1058 '/verify_xmlrpc_error',
1059 array(
1060 'methods' => \WP_REST_Server::CREATABLE,
1061 'callback' => array( $this, 'verify_xml_rpc_error' ),
1062 'permission_callback' => '__return_true',
1063 'args' => array(
1064 'nonce' => array(
1065 'required' => true,
1066 'type' => 'string',
1067 ),
1068 ),
1069 )
1070 );
1071 }
1072
1073 /**
1074 * Handles verification that a xml rpc error is legit and came from WordPres.com
1075 *
1076 * @since 1.14.2
1077 *
1078 * @param \WP_REST_Request $request The request sent to the WP REST API.
1079 *
1080 * @return boolean
1081 */
1082 public function verify_xml_rpc_error( \WP_REST_Request $request ) {
1083 $error = $this->get_error_by_nonce( $request['nonce'] );
1084
1085 if ( $error ) {
1086 $this->verify_error( $error );
1087 return new \WP_REST_Response( true, 200 );
1088 }
1089
1090 return new \WP_REST_Response( false, 200 );
1091 }
1092
1093 /**
1094 * Prints a generic error notice for all connection errors
1095 *
1096 * @since 8.9.0
1097 *
1098 * @return void
1099 */
1100 public function generic_admin_notice_error() {
1101 // do not add admin notice to the jetpack dashboard.
1102 global $pagenow;
1103 if ( 'admin.php' === $pagenow || isset( $_GET['page'] ) && 'jetpack' === $_GET['page'] ) { // phpcs:ignore
1104 return;
1105 }
1106
1107 if ( ! current_user_can( 'jetpack_connect' ) ) {
1108 return;
1109 }
1110
1111 /**
1112 * Filters the message to be displayed in the admin notices area when there's a connection error.
1113 *
1114 * By default we don't display any errors.
1115 *
1116 * Return an empty value to disable the message.
1117 *
1118 * @since 8.9.0
1119 *
1120 * @param string $message The error message.
1121 * @param array $errors The array of errors. See Automattic\Jetpack\Connection\Error_Handler for details on the array structure.
1122 */
1123 $message = apply_filters( 'jetpack_connection_error_notice_message', '', $this->get_displayable_errors() );
1124
1125 /**
1126 * Fires inside the admin_notices hook just before displaying the error message for a broken connection.
1127 *
1128 * If you want to disable the default message from being displayed, return an empty value in the jetpack_connection_error_notice_message filter.
1129 *
1130 * @since 8.9.0
1131 *
1132 * @param array $errors The array of errors. See Automattic\Jetpack\Connection\Error_Handler for details on the array structure.
1133 */
1134 do_action( 'jetpack_connection_error_notice', $this->get_displayable_errors() );
1135
1136 if ( empty( $message ) ) {
1137 return;
1138 }
1139
1140 wp_admin_notice(
1141 esc_html( $message ),
1142 array(
1143 'type' => 'error',
1144 'dismissible' => true,
1145 'additional_classes' => array( 'jetpack-message', 'jp-connect' ),
1146 'attributes' => array( 'style' => 'display:block !important;' ),
1147 )
1148 );
1149 }
1150
1151 /**
1152 * Check REST API response for errors, and report them to WP.com if needed.
1153 *
1154 * @see wp_remote_request() For more information on the $http_response array format.
1155 * @param array|\WP_Error $http_response The response or WP_Error on failure.
1156 * @param array $auth_data Auth data, allowed keys: `token`, `timestamp`, `nonce`, `body-hash`.
1157 * @param string $url Request URL.
1158 * @param string $method Request method.
1159 * @param string $error_type The source of an error: 'xmlrpc' or 'rest'.
1160 *
1161 * @return void
1162 */
1163 public function check_api_response_for_errors( $http_response, $auth_data, $url, $method, $error_type ) {
1164 if ( 200 === wp_remote_retrieve_response_code( $http_response ) || ! is_array( $auth_data ) || ! $url || ! $method ) {
1165 return;
1166 }
1167
1168 $body_raw = wp_remote_retrieve_body( $http_response );
1169 if ( ! $body_raw ) {
1170 return;
1171 }
1172
1173 $body = json_decode( $body_raw, true );
1174 if ( empty( $body['error'] ) || ( ! is_string( $body['error'] ) && ! is_int( $body['error'] ) ) ) {
1175 return;
1176 }
1177
1178 $error = new \WP_Error(
1179 $body['error'],
1180 empty( $body['message'] ) ? '' : $body['message'],
1181 array(
1182 'signature_details' => array(
1183 'token' => empty( $auth_data['token'] ) ? '' : $auth_data['token'],
1184 'timestamp' => empty( $auth_data['timestamp'] ) ? '' : $auth_data['timestamp'],
1185 'nonce' => empty( $auth_data['nonce'] ) ? '' : $auth_data['nonce'],
1186 'body_hash' => empty( $auth_data['body_hash'] ) ? '' : $auth_data['body_hash'],
1187 'method' => $method,
1188 'url' => $url,
1189 ),
1190 'error_type' => in_array( $error_type, array( 'xmlrpc', 'rest' ), true ) ? $error_type : '',
1191 )
1192 );
1193
1194 $this->report_error( $error, false, true );
1195 }
1196
1197 /**
1198 * Determines whether external filters are applied to the get_displayable_errors method.
1199 *
1200 * @since 6.13.10
1201 *
1202 * @return bool True if external filters are applied, false otherwise.
1203 */
1204 private function has_external_filters() {
1205 return has_filter( 'jetpack_connection_get_verified_errors' ) &&
1206 $this->should_allow_error_filtering();
1207 }
1208
1209 /**
1210 * Invalidates the cached displayable errors
1211 *
1212 * @since 6.13.10
1213 *
1214 * @return void
1215 */
1216 private function invalidate_displayable_errors_cache() {
1217 $this->cached_displayable_errors = null;
1218 }
1219 }
1220