<?php /** * The Server_Sandbox class. * * This feature is only useful for Automattic developers. * It configures Jetpack to talk to staging/sandbox servers * on WordPress.com instead of production servers. * * @package automattic/jetpack-sandbox */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Constants; /** * The Server_Sandbox class. */ class Server_Sandbox { /** * Sets up the action hooks for the server sandbox. */ public function init() { if ( did_action( 'jetpack_server_sandbox_init' ) ) { return; } add_action( 'requests-requests.before_request', array( $this, 'server_sandbox' ), 10, 4 ); add_action( 'admin_bar_menu', array( $this, 'admin_bar_add_sandbox_item' ), 999 ); /** * Fires when the server sandbox is initialized. This action is used to ensure that * the server sandbox action hooks are set up only once. * * @since 1.30.7 */ do_action( 'jetpack_server_sandbox_init' ); } /** * Returns the new url and host values. * * @param string $sandbox Sandbox domain. * @param string $url URL of request about to be made. * @param array $headers Headers of request about to be made. * @param string $data The body of request about to be made. * @param string $method The method of request about to be made. * * @return array [ 'url' => new URL, 'host' => new Host, 'new_signature => New signature if url was changed ] */ public function server_sandbox_request_parameters( $sandbox, $url, $headers, $data = null, $method = 'GET' ) { $host = ''; $new_signature = ''; if ( ! is_string( $sandbox ) || ! is_string( $url ) ) { return array( 'url' => $url, 'host' => $host, 'new_signature' => $new_signature, ); } $url_host = wp_parse_url( $url, PHP_URL_HOST ); switch ( $url_host ) { case 'public-api.wordpress.com': case 'jetpack.wordpress.com': case 'jetpack.com': case 'dashboard.wordpress.com': $host = isset( $headers['Host'] ) ? $headers['Host'] : $url_host; $original_url = $url; $url = preg_replace( '@^(https?://)' . preg_quote( $url_host, '@' ) . '(?=[/?#].*|$)@', '${1}' . $sandbox, $url, 1 ); /** * Whether to add the X Debug query parameter to the request made to the Sandbox * * @since 1.36.0 * * @param bool $add_parameter Whether to add the parameter to the request or not. Default is to false. * @param string $url The URL of the request being made. * @param string $host The host of the request being made. */ if ( apply_filters( 'jetpack_sandbox_add_profile_parameter', false, $url, $host ) ) { $url = add_query_arg( 'XDEBUG_PROFILE', 1, $url ); // URL has been modified since the signature was created. We'll need a new one. $original_url = add_query_arg( 'XDEBUG_PROFILE', 1, $original_url ); $new_signature = $this->get_new_signature( $original_url, $headers, $data, $method ); } } return compact( 'url', 'host', 'new_signature' ); } /** * Gets a new signature for the request * * @param string $url The new URL to be signed. * @param array $headers The headers of the request about to be made. * @param string $data The body of request about to be made. * @param string $method The method of the request about to be made. * @return string|null */ private function get_new_signature( $url, $headers, $data, $method ) { if ( ! empty( $headers['Authorization'] ) ) { $a_headers = $this->extract_authorization_headers( $headers ); if ( ! empty( $a_headers ) ) { $token_details = explode( ':', $a_headers['token'] ); if ( count( $token_details ) === 3 ) { $user_id = $token_details[2]; $token = ( new Tokens() )->get_access_token( $user_id ); $time_diff = (int) \Jetpack_Options::get_option( 'time_diff' ); $jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff ); $signature = $jetpack_signature->sign_request( $a_headers['token'], $a_headers['timestamp'], $a_headers['nonce'], $a_headers['body-hash'], $method, $url, $data, false ); if ( $signature && ! is_wp_error( $signature ) ) { return $signature; } elseif ( is_wp_error( $signature ) ) { $this->log_new_signature_error( $signature->get_error_message() ); } } else { $this->log_new_signature_error( 'Malformed token on Authorization Header' ); } } else { $this->log_new_signature_error( 'Error extracting Authorization Header' ); } } else { $this->log_new_signature_error( 'Empty Authorization Header' ); } } /** * Logs error if the attempt to create a new signature fails * * @param string $message The error message. * @return void */ private function log_new_signature_error( $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( "SANDBOXING: Error re-signing the request. '%s'", $message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } /** * Extract the values in the Authorization header into an array * * @param array $headers The headers of the request about to be made. * @return array|null */ public function extract_authorization_headers( $headers ) { if ( ! empty( $headers['Authorization'] ) && is_string( $headers['Authorization'] ) ) { $header = str_replace( 'X_JETPACK ', '', $headers['Authorization'] ); $vars = explode( ' ', $header ); $result = array(); foreach ( $vars as $var ) { $elements = explode( '"', $var ); if ( count( $elements ) === 3 ) { $result[ substr( $elements[0], 0, -1 ) ] = $elements[1]; } } return $result; } } /** * Modifies parameters of request in order to send the request to the * server specified by `JETPACK__SANDBOX_DOMAIN`. * * Attached to the `requests-requests.before_request` filter. * * @param string $url URL of request about to be made. * @param array $headers Headers of request about to be made. * @param array|string $data Data of request about to be made. * @param string $type Type of request about to be made. * @return void */ public function server_sandbox( &$url, &$headers, &$data = null, &$type = null ) { if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) { return; } $original_url = $url; $request_parameters = $this->server_sandbox_request_parameters( Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $url, $headers, $data, $type ); $url = $request_parameters['url']; if ( $request_parameters['host'] ) { $headers['Host'] = $request_parameters['host']; if ( $request_parameters['new_signature'] ) { $headers['Authorization'] = preg_replace( '/signature=\"[^\"]+\"/', 'signature="' . $request_parameters['new_signature'] . '"', $headers['Authorization'] ); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( "SANDBOXING via '%s': '%s'", Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $original_url ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } } /** * Adds a "Jetpack API Sandboxed" item to the admin bar if the JETPACK__SANDBOX_DOMAIN * constant is set. * * Attached to the `admin_bar_menu` action. * * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance. */ public function admin_bar_add_sandbox_item( $wp_admin_bar ) { if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) { return; } $node = array( 'id' => 'jetpack-connection-api-sandbox', 'title' => 'Jetpack API Sandboxed', 'meta' => array( 'title' => 'Sandboxing via ' . Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), ), ); $wp_admin_bar->add_menu( $node ); } }