rror * envelope are captured. `Jetpack_IXR_Client::query()` reports faults itself, via * check_xmlrpc_fault_for_errors(). * * @see wp_remote_request() For more information on the $http_response array format. * @param array|\WP_Error $http_response The response or WP_Error on failure. * @param array $auth_data Auth data, allowed keys: `token`, `timestamp`, `nonce`, `body-hash`. * @param string $url Request URL. * @param string $method Request method. * @param string $error_type The transport of the outgoing request: `ERROR_TYPE_XMLRPC` or `ERROR_TYPE_REST`. * * @return void */ public function check_api_response_for_errors( $http_response, $auth_data, $url, $method, $error_type ) { if ( 200 === wp_remote_retrieve_response_code( $http_response ) || ! is_array( $auth_data ) || ! $url || ! $method ) { return; } $body_raw = wp_remote_retrieve_body( $http_response ); if ( ! $body_raw ) { return; } $body = json_decode( $body_raw, true ); // Support both error envelopes: the legacy v1 JSON-API shape (`error`) and the // WP-API v2 shape (`code`), the latter used by `wpcom/v2` endpoints such as // `jetpack-wpcom-user-data`. Prefer `error` for backwards compatibility. $error_code = is_array( $body ) ? ( $body['error'] ?? $body['code'] ?? null ) : null; if ( empty( $error_code ) || ( ! is_string( $error_code ) && ! is_int( $error_code ) ) ) { return; } $error = self::build_connection_wp_error( (string) $error_code, empty( $body['message'] ) ? '' : $body['message'], array( 'token' => empty( $auth_data['token'] ) ? '' : $auth_data['token'], 'timestamp' => empty( $auth_data['timestamp'] ) ? '' : $auth_data['timestamp'], 'nonce' => empty( $auth_data['nonce'] ) ? '' : $auth_data['nonce'], // `Client::build_signed_request()` builds this key as `body-hash` (it is sent as an // `Authorization` header parameter). The snake_case fallback keeps callers that pass // the stored `signature_details` shape working. 'body_hash' => $auth_data['body-hash'] ?? $auth_data['body_hash'] ?? '', 'method' => $method, 'url' => $url, ), $error_type, self::DIRECTION_OUTGOING ); $this->report_error( $error, false, true ); } /** * Check the result of signing an outgoing request for errors, and store them if needed. * * This is the second entry point of the outgoing-request error flow (flow 2 in the class * docblock). It handles failures from `Client::build_signed_request()`, which * occur before a request is sent and therefore have no response to inspect. * * Like response errors in flow 2, these are stored as verified without a WP.com * round-trip because the site's own token and URL state provides the evidence. * The hourly reporting gate in `report_error()` still applies. * * Codes reaching this method include `malformed_token` and `invalid_body` (from * `Client::build_signed_request()`), plus the signing errors returned by * `Jetpack_Signature::sign_request()` (e.g. `invalid_scheme`, `unknown_scheme_port`), * plus the token-lookup errors raised by `Tokens::get_access_token()` (e.g. * `no_user_tokens`, `no_token_for_user`). `tokens_locked` also reaches here but is not * in `known_errors`, so `report_error()` silently discards it — see the comment on * `Client::build_signed_request()`'s `tokens_locked` branch for why. * * This includes token lookup, request validation, and request signing errors. * * @since 8.10.1 * * @param mixed $signing_result The return value of `Client::build_signed_request()`. Ignored unless it is a `WP_Error`. * @param string $url Request URL. * @param string $method Request method. * @param string $error_type The transport of the outgoing request: `ERROR_TYPE_XMLRPC` or `ERROR_TYPE_REST`. * * @return void */ public function check_signed_request_for_errors( $signing_result, $url, $method, $error_type ) { if ( ! is_wp_error( $signing_result ) ) { return; } $data = $signing_result->get_error_data(); // The signing errors raised by `Jetpack_Signature` already carry the details of the // request they failed to sign; the ones raised by `Client` itself carry nothing. $signature_details = isset( $data['signature_details'] ) && is_array( $data['signature_details'] ) ? $data['signature_details'] : array(); $signature_details += array( 'method' => $method, 'url' => $url, ); $error = self::build_connection_wp_error( (string) $signing_result->get_error_code(), $signing_result->get_error_message(), $signature_details, $error_type, self::DIRECTION_OUTGOING, // `Tokens::get_access_token()` attaches `user_id` to the WP_Errors it raises when it // has already resolved one (see its docblock); pass it through as the attribution // fallback consulted by `wp_error_to_array()`. Errors with no token to look up at all // (e.g. `tokens_locked`, `malformed_token` from `Client` itself) carry no such data, // and fall back to unattributed there. array( 'user_id' => isset( $data['user_id'] ) ? (int) $data['user_id'] : 0 ) ); $this->report_error( $error, false, true ); } /** * Check an outgoing XML-RPC request's fault response for errors, and store them if needed. * * This is the third entry point of the outgoing-request error flow (flow 2 in the class * docblock). XML-RPC faults arrive as HTTP 200 responses with an XML body, so * check_api_response_for_errors() never sees them — it returns immediately on a 200, * and decodes the body as JSON rather than XML anyway. `Jetpack_IXR_Client::query()` * calls this method directly from its fault branch instead. * * The code/message pair is recovered from the fault string by the caller, via * `Jetpack_IXR_Client::parse_jetpack_fault_string()` — the class that owns the * `Jetpack: [code] message` convention. An unparseable fault string is the caller's * concern, not this method's; a fault code reaching here is untrusted input, and it's * `report_error()`'s `$known_errors` allowlist, not this method, that keeps an * unrecognized code from being stored. In practice every `jetpack.*` XML-RPC handler * on WP.com emits fixed string literals here, never attacker- or request-composed * ones, and the handful that are also `$known_errors` (`unknown_token`, * `signature_mismatch`, `invalid_token`, `token_mismatch`, `invalid_signature`) are * the same codes this site itself raises for the same failure — WP.com is just * verifying signatures with the same scheme. * * @since 8.10.4 * * @param string $error_code The Jetpack error code parsed from the fault string. * @param string $error_message The error message parsed from the fault string. * @param string $url Request URL. * @param string $method Request method. * @param int $user_id The local user ID the request was signed for, or `0` for the blog token. * * @return void */ public function check_xmlrpc_fault_for_errors( string $error_code, string $error_message, string $url, string $method, int $user_id = 0 ) { $error = self::build_connection_wp_error( $error_code, $error_message, array( 'method' => $method, 'url' => $url, ), self::ERROR_TYPE_XMLRPC, self::DIRECTION_OUTGOING, array( 'user_id' => $user_id ) ); $this->report_error( $error, false, true ); } /** * Determines whether external filters are applied to the get_displayable_errors method. * * @since 6.13.10 * * @return bool True if external filters are applied, false otherwise. */ private function has_external_filters() { return has_filter( 'jetpack_connection_get_verified_errors' ) && $this->should_allow_error_filtering(); } /** * Invalidates the cached displayable errors * * @since 6.13.10 * * @return void */ private function invalidate_displayable_errors_cache() { $this->cached_displayable_errors = null; } }