diff options
Diffstat (limited to 'plugins/jetpack/vendor/automattic/jetpack-connection')
11 files changed, 4327 insertions, 0 deletions
diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php new file mode 100644 index 00000000..084cc8e6 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php @@ -0,0 +1,122 @@ +<?php +/** + * IXR_Client + * + * @package automattic/jetpack-connection + * + * @since 1.5 + * @since 7.7 Moved to the jetpack-connection package. + */ + +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Connection\Manager; + +/** + * A Jetpack implementation of the WordPress core IXR client. + */ +class Jetpack_IXR_Client extends IXR_Client { + /** + * Jetpack args, used for the remote requests. + * + * @var array + */ + public $jetpack_args = null; + + /** + * Constructor. + * Initialize a new Jetpack IXR client instance. + * + * @param array $args Jetpack args, used for the remote requests. + * @param string|bool $path Path to perform the reuqest to. + * @param int $port Port number. + * @param int $timeout The connection timeout, in seconds. + */ + public function __construct( $args = array(), $path = false, $port = 80, $timeout = 15 ) { + $connection = new Manager(); + + $defaults = array( + 'url' => $connection->xmlrpc_api_url(), + 'user_id' => 0, + ); + + $args = wp_parse_args( $args, $defaults ); + + $this->jetpack_args = $args; + + $this->IXR_Client( $args['url'], $path, $port, $timeout ); + } + + /** + * Perform the IXR request. + * + * @return bool True if request succeeded, false otherwise. + */ + public function query() { + $args = func_get_args(); + $method = array_shift( $args ); + $request = new IXR_Request( $method, $args ); + $xml = trim( $request->getXml() ); + + $response = Client::remote_request( $this->jetpack_args, $xml ); + + if ( is_wp_error( $response ) ) { + $this->error = new IXR_Error( -10520, sprintf( 'Jetpack: [%s] %s', $response->get_error_code(), $response->get_error_message() ) ); + return false; + } + + if ( ! $response ) { + $this->error = new IXR_Error( -10520, 'Jetpack: Unknown Error' ); + return false; + } + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + $this->error = new IXR_Error( -32300, 'transport error - HTTP status code was not 200' ); + return false; + } + + $content = wp_remote_retrieve_body( $response ); + + // Now parse what we've got back. + $this->message = new IXR_Message( $content ); + if ( ! $this->message->parse() ) { + // XML error. + $this->error = new IXR_Error( -32700, 'parse error. not well formed' ); + return false; + } + + // Is the message a fault? + if ( 'fault' === $this->message->messageType ) { + $this->error = new IXR_Error( $this->message->faultCode, $this->message->faultString ); + return false; + } + + // Message must be OK. + return true; + } + + /** + * Retrieve the Jetpack error from the result of the last request. + * + * @param int $fault_code Fault code. + * @param string $fault_string Fault string. + * @return WP_Error Error object. + */ + public function get_jetpack_error( $fault_code = null, $fault_string = null ) { + if ( is_null( $fault_code ) ) { + $fault_code = $this->error->code; + } + + if ( is_null( $fault_string ) ) { + $fault_string = $this->error->message; + } + + if ( preg_match( '#jetpack:\s+\[(\w+)\]\s*(.*)?$#i', $fault_string, $match ) ) { + $code = $match[1]; + $message = $match[2]; + $status = $fault_code; + return new \WP_Error( $code, $message, $status ); + } + + return new \WP_Error( "IXR_{$fault_code}", $fault_string ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php new file mode 100644 index 00000000..da71873f --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php @@ -0,0 +1,68 @@ +<?php +/** + * IXR_ClientMulticall + * + * @package automattic/jetpack-connection + * + * @since 1.5 + * @since 7.7 Moved to the jetpack-connection package. + */ + +/** + * A Jetpack implementation of the WordPress core IXR client, capable of multiple calls in a single request. + */ +class Jetpack_IXR_ClientMulticall extends Jetpack_IXR_Client { + /** + * Storage for the IXR calls. + * + * @var array + */ + public $calls = array(); + + /** + * Add a IXR call to the client. + * First argument is the method name. + * The rest of the arguments are the params specified to the method. + */ + public function addCall() { + $args = func_get_args(); + $method_name = array_shift( $args ); + $struct = array( + 'methodName' => $method_name, + 'params' => $args, + ); + $this->calls[] = $struct; + } + + /** + * Perform the IXR multicall request. + * + * @return bool True if request succeeded, false otherwise. + */ + public function query() { + usort( $this->calls, array( $this, 'sort_calls' ) ); + + // Prepare multicall, then call the parent::query() method. + return parent::query( 'system.multicall', $this->calls ); + } + + /** + * Sort the IXR calls. + * Make sure syncs are always done first. + * + * @param array $a First call in the sorting iteration. + * @param array $b Second call in the sorting iteration. + * @return int Result of the sorting iteration. + */ + public function sort_calls( $a, $b ) { + if ( 'jetpack.syncContent' === $a['methodName'] ) { + return -1; + } + + if ( 'jetpack.syncContent' === $b['methodName'] ) { + return 1; + } + + return 0; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php new file mode 100644 index 00000000..2d6b7529 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php @@ -0,0 +1,344 @@ +<?php +/** + * The Jetpack Connection signature class file. + * + * @package automattic/jetpack-connection + */ + +use Automattic\Jetpack\Connection\Manager as Connection_Manager; + +/** + * The Jetpack Connection signature class that is used to sign requests. + */ +class Jetpack_Signature { + /** + * Token part of the access token. + * + * @access public + * @var string + */ + public $token; + + /** + * Access token secret. + * + * @access public + * @var string + */ + public $secret; + + /** + * The current request URL. + * + * @access public + * @var string + */ + public $current_request_url; + + /** + * Constructor. + * + * @param array $access_token Access token. + * @param int $time_diff Timezone difference (in seconds). + */ + public function __construct( $access_token, $time_diff = 0 ) { + $secret = explode( '.', $access_token ); + if ( 2 !== count( $secret ) ) { + return; + } + + $this->token = $secret[0]; + $this->secret = $secret[1]; + $this->time_diff = $time_diff; + } + + /** + * Sign the current request. + * + * @todo Implement a proper nonce verification. + * + * @param array $override Optional arguments to override the ones from the current request. + * @return string|WP_Error Request signature, or a WP_Error on failure. + */ + public function sign_current_request( $override = array() ) { + if ( isset( $override['scheme'] ) ) { + $scheme = $override['scheme']; + if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { + return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' ); + } + } else { + if ( is_ssl() ) { + $scheme = 'https'; + } else { + $scheme = 'http'; + } + } + + $host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : $_SERVER['SERVER_PORT']; + $host_port = intval( $host_port ); + + /** + * Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test. + * Please update the test if any changes are made in this logic. + */ + if ( is_ssl() ) { + // 443: Standard Port + // 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites + // with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with + // the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a + // site at https://example.com:80/ (which would be a strange configuration). + // JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port + // if the site is behind a proxy running on port 443 without + // X-Forwarded-Port and the back end's port is *not* 80. It's better, + // though, to configure the proxy to send X-Forwarded-Port. + $https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? JETPACK_SIGNATURE__HTTPS_PORT : 443; + $port = in_array( $host_port, array( 443, 80, $https_port ), false ) ? '' : $host_port; // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse + } else { + // 80: Standard Port + // JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port + // if the site is behind a proxy running on port 80 without + // X-Forwarded-Port. It's better, though, to configure the proxy to + // send X-Forwarded-Port. + $http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? JETPACK_SIGNATURE__HTTP_PORT : 80; + $port = in_array( $host_port, array( 80, $http_port ), false ) ? '' : $host_port; // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse + } + + $this->current_request_url = "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . stripslashes( $_SERVER['REQUEST_URI'] ); + + if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) { + $body = $override['body']; + } elseif ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { + $body = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null; + + // Convert the $_POST to the body, if the body was empty. This is how arrays are hashed + // and encoded on the Jetpack side. + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( empty( $body ) && is_array( $_POST ) && count( $_POST ) > 0 ) { + $body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + } + } elseif ( 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { + // This is a little strange-looking, but there doesn't seem to be another way to get the PUT body. + $raw_put_data = file_get_contents( 'php://input' ); + parse_str( $raw_put_data, $body ); + + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + $put_data = json_decode( $raw_put_data, true ); + if ( is_array( $put_data ) && count( $put_data ) > 0 ) { + $body = $put_data; + } + } + } else { + $body = null; + } + + if ( empty( $body ) ) { + $body = null; + } + + $a = array(); + foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) { + if ( isset( $override[ $parameter ] ) ) { + $a[ $parameter ] = $override[ $parameter ]; + } else { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $a[ $parameter ] = isset( $_GET[ $parameter ] ) ? stripslashes( $_GET[ $parameter ] ) : ''; + } + } + + $method = isset( $override['method'] ) ? $override['method'] : $_SERVER['REQUEST_METHOD']; + return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true ); + } + + /** + * Sign a specified request. + * + * @todo Having body_hash v. body-hash is annoying. Refactor to accept an array? + * @todo Use wp_json_encode() instead of json_encode()? + * + * @param string $token Request token. + * @param int $timestamp Timestamp of the request. + * @param string $nonce Request nonce. + * @param string $body_hash Request body hash. + * @param string $method Request method. + * @param string $url Request URL. + * @param mixed $body Request body. + * @param bool $verify_body_hash Whether to verify the body hash against the body. + * @return string|WP_Error Request signature, or a WP_Error on failure. + */ + public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) { + if ( ! $this->secret ) { + return new WP_Error( 'invalid_secret', 'Invalid secret' ); + } + + if ( ! $this->token ) { + return new WP_Error( 'invalid_token', 'Invalid token' ); + } + + list( $token ) = explode( '.', $token ); + + $signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' ); + + if ( 0 !== strpos( $token, "$this->token:" ) ) { + return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) ); + } + + // If we got an array at this point, let's encode it, so we can see what it looks like as a string. + if ( is_array( $body ) ) { + if ( count( $body ) > 0 ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + $body = json_encode( $body ); + + } else { + $body = ''; + } + } + + $required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' ); + if ( ! is_null( $body ) ) { + $required_parameters[] = 'body_hash'; + if ( ! is_string( $body ) ) { + return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) ); + } + } + + foreach ( $required_parameters as $required ) { + if ( ! is_scalar( $$required ) ) { + return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) ); + } + + if ( ! strlen( $$required ) ) { + return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) ); + } + } + + if ( empty( $body ) ) { + if ( $body_hash ) { + return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) ); + } + } else { + $connection = new Connection_Manager(); + if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) { + return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) ); + } + } + + $parsed = wp_parse_url( $url ); + if ( ! isset( $parsed['host'] ) ) { + return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) ); + } + + if ( ! empty( $parsed['port'] ) ) { + $port = $parsed['port']; + } else { + if ( 'http' === $parsed['scheme'] ) { + $port = 80; + } elseif ( 'https' === $parsed['scheme'] ) { + $port = 443; + } else { + return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) ); + } + } + + if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug. + return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) ); + } + + $local_time = $timestamp - $this->time_diff; + if ( $local_time < time() - 600 || $local_time > time() + 300 ) { + return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) ); + } + + if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) { + return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) ); + } + + $normalized_request_pieces = array( + $token, + $timestamp, + $nonce, + $body_hash, + strtoupper( $method ), + strtolower( $parsed['host'] ), + $port, + $parsed['path'], + // Normalized Query String. + ); + + $normalized_request_pieces = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( isset( $parsed['query'] ) ? $parsed['query'] : '' ) ); + $flat_normalized_request_pieces = array(); + foreach ( $normalized_request_pieces as $piece ) { + if ( is_array( $piece ) ) { + foreach ( $piece as $subpiece ) { + $flat_normalized_request_pieces[] = $subpiece; + } + } else { + $flat_normalized_request_pieces[] = $piece; + } + } + $normalized_request_pieces = $flat_normalized_request_pieces; + + $normalized_request_string = join( "\n", $normalized_request_pieces ) . "\n"; + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) ); + } + + /** + * Retrieve and normalize the parameters from a query string. + * + * @param string $query_string Query string. + * @return array Normalized query string parameters. + */ + public function normalized_query_parameters( $query_string ) { + parse_str( $query_string, $array ); + + unset( $array['signature'] ); + + $names = array_keys( $array ); + $values = array_values( $array ); + + $names = array_map( array( $this, 'encode_3986' ), $names ); + $values = array_map( array( $this, 'encode_3986' ), $values ); + + $pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values ); + + sort( $pairs ); + + return $pairs; + } + + /** + * Encodes a string or array of strings according to RFC 3986. + * + * @param string|array $string_or_array String or array to encode. + * @return string|array URL-encoded string or array. + */ + public function encode_3986( $string_or_array ) { + if ( is_array( $string_or_array ) ) { + return array_map( array( $this, 'encode_3986' ), $string_or_array ); + } + + return rawurlencode( $string_or_array ); + } + + /** + * Concatenates a parameter name and a parameter value with an equals sign between them. + * Supports one-dimensional arrays as `$value`. + * + * @param string $name Parameter name. + * @param mixed $value Parameter value. + * @return string A pair with parameter name and value (e.g. `name=value`). + */ + public function join_with_equal_sign( $name, $value ) { + if ( is_array( $value ) ) { + $result = array(); + foreach ( $value as $array_key => $array_value ) { + $result[] = $name . '[' . $array_key . ']=' . $array_value; + } + return $result; + } + return "{$name}={$value}"; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php new file mode 100644 index 00000000..db897a0d --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php @@ -0,0 +1,943 @@ +<?php +/** + * Jetpack XMLRPC Server. + * + * @package automattic/jetpack-connection + */ + +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Connection\Manager as Connection_Manager; +use Automattic\Jetpack\Connection\Utils as Connection_Utils; +use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Sync\Modules; +use Automattic\Jetpack\Sync\Functions; +use Automattic\Jetpack\Sync\Sender; + +/** + * Just a sack of functions. Not actually an IXR_Server + */ +class Jetpack_XMLRPC_Server { + /** + * The current error object + * + * @var \WP_Error + */ + public $error = null; + + /** + * The current user + * + * @var \WP_User + */ + public $user = null; + + /** + * The connection manager object. + * + * @var Automattic\Jetpack\Connection\Manager + */ + private $connection; + + /** + * Creates a new XMLRPC server object. + * + * @param Automattic\Jetpack\Connection\Manager $manager the connection manager object. + */ + public function __construct( $manager = null ) { + $this->connection = is_null( $manager ) ? new Connection_Manager() : $manager; + } + + /** + * Whitelist of the XML-RPC methods available to the Jetpack Server. If the + * user is not authenticated (->login()) then the methods are never added, + * so they will get a "does not exist" error. + * + * @param array $core_methods Core XMLRPC methods. + */ + public function xmlrpc_methods( $core_methods ) { + $jetpack_methods = array( + 'jetpack.jsonAPI' => array( $this, 'json_api' ), + 'jetpack.verifyAction' => array( $this, 'verify_action' ), + 'jetpack.getUser' => array( $this, 'get_user' ), + 'jetpack.remoteRegister' => array( $this, 'remote_register' ), + 'jetpack.remoteProvision' => array( $this, 'remote_provision' ), + ); + + $this->user = $this->login(); + + if ( $this->user ) { + $jetpack_methods = array_merge( + $jetpack_methods, + array( + 'jetpack.testConnection' => array( $this, 'test_connection' ), + 'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ), + 'jetpack.featuresAvailable' => array( $this, 'features_available' ), + 'jetpack.featuresEnabled' => array( $this, 'features_enabled' ), + 'jetpack.disconnectBlog' => array( $this, 'disconnect_blog' ), + 'jetpack.unlinkUser' => array( $this, 'unlink_user' ), + 'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ), + ) + ); + + if ( isset( $core_methods['metaWeblog.editPost'] ) ) { + $jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject']; + $jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' ); + } + + /** + * Filters the XML-RPC methods available to Jetpack for authenticated users. + * + * @since 1.1.0 + * + * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. + * @param array $core_methods Available core XML-RPC methods. + * @param \WP_User $user Information about a given WordPress user. + */ + $jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user ); + } + + /** + * Filters the XML-RPC methods available to Jetpack for unauthenticated users. + * + * @since 3.0.0 + * + * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. + * @param array $core_methods Available core XML-RPC methods. + */ + return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods ); + } + + /** + * Whitelist of the bootstrap XML-RPC methods + */ + public function bootstrap_xmlrpc_methods() { + return array( + 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ), + 'jetpack.remoteRegister' => array( $this, 'remote_register' ), + ); + } + + /** + * Additional method needed for authorization calls. + */ + public function authorize_xmlrpc_methods() { + return array( + 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ), + ); + } + + /** + * Remote provisioning methods. + */ + public function provision_xmlrpc_methods() { + return array( + 'jetpack.remoteRegister' => array( $this, 'remote_register' ), + 'jetpack.remoteProvision' => array( $this, 'remote_provision' ), + 'jetpack.remoteConnect' => array( $this, 'remote_connect' ), + 'jetpack.getUser' => array( $this, 'get_user' ), + ); + } + + /** + * Used to verify whether a local user exists and what role they have. + * + * @param int|string|array $request One of: + * int|string The local User's ID, username, or email address. + * array A request array containing: + * 0: int|string The local User's ID, username, or email address. + * + * @return array|\IXR_Error Information about the user, or error if no such user found: + * roles: string[] The user's rols. + * login: string The user's username. + * email_hash string[] The MD5 hash of the user's normalized email address. + * caps string[] The user's capabilities. + * allcaps string[] The user's granular capabilities, merged from role capabilities. + * token_key string The Token Key of the user's Jetpack token. Empty string if none. + */ + public function get_user( $request ) { + $user_id = is_array( $request ) ? $request[0] : $request; + + if ( ! $user_id ) { + return $this->error( + new Jetpack_Error( + 'invalid_user', + __( 'Invalid user identifier.', 'jetpack' ), + 400 + ), + 'get_user' + ); + } + + $user = $this->get_user_by_anything( $user_id ); + + if ( ! $user ) { + return $this->error( + new Jetpack_Error( + 'user_unknown', + __( 'User not found.', 'jetpack' ), + 404 + ), + 'get_user' + ); + } + + $user_token = $this->connection->get_access_token( $user->ID ); + + if ( $user_token ) { + list( $user_token_key ) = explode( '.', $user_token->secret ); + if ( $user_token_key === $user_token->secret ) { + $user_token_key = ''; + } + } else { + $user_token_key = ''; + } + + return array( + 'id' => $user->ID, + 'login' => $user->user_login, + 'email_hash' => md5( strtolower( trim( $user->user_email ) ) ), + 'roles' => $user->roles, + 'caps' => $user->caps, + 'allcaps' => $user->allcaps, + 'token_key' => $user_token_key, + ); + } + + /** + * Remote authorization XMLRPC method handler. + * + * @param array $request the request. + */ + public function remote_authorize( $request ) { + $user = get_user_by( 'id', $request['state'] ); + + /** + * Happens on various request handling events in the Jetpack XMLRPC server. + * The action combines several types of events: + * - remote_authorize + * - remote_provision + * - get_user. + * + * @since 8.0.0 + * + * @param String $action the action name, i.e., 'remote_authorize'. + * @param String $stage the execution stage, can be 'begin', 'success', 'error', etc. + * @param Array $parameters extra parameters from the event. + * @param WP_User $user the acting user. + */ + do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'begin', array(), $user ); + + foreach ( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) { + if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) { + return $this->error( + new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ), + 'remote_authorize' + ); + } + } + + if ( ! $user ) { + return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' ); + } + + if ( $this->connection->is_active() && $this->connection->is_user_connected( $request['state'] ) ) { + return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ), 'remote_authorize' ); + } + + $verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) ); + + if ( is_a( $verified, 'IXR_Error' ) ) { + return $this->error( $verified, 'remote_authorize' ); + } + + wp_set_current_user( $request['state'] ); + + $result = $this->connection->authorize( $request ); + + if ( is_wp_error( $result ) ) { + return $this->error( $result, 'remote_authorize' ); + } + + // This action is documented in class.jetpack-xmlrpc-server.php. + do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'success' ); + + return array( + 'result' => $result, + ); + } + + /** + * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to + * register this site so that a plan can be provisioned. + * + * @param array $request An array containing at minimum nonce and local_user keys. + * + * @return \WP_Error|array + */ + public function remote_register( $request ) { + // This action is documented in class.jetpack-xmlrpc-server.php. + do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'begin', array() ); + + $user = $this->fetch_and_verify_local_user( $request ); + + if ( ! $user ) { + return $this->error( + new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), + 'remote_register' + ); + } + + if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { + return $this->error( $user, 'remote_register' ); + } + + if ( empty( $request['nonce'] ) ) { + return $this->error( + new Jetpack_Error( + 'nonce_missing', + __( 'The required "nonce" parameter is missing.', 'jetpack' ), + 400 + ), + 'remote_register' + ); + } + + $nonce = sanitize_text_field( $request['nonce'] ); + unset( $request['nonce'] ); + + $api_url = Connection_Utils::fix_url_for_bad_hosts( + $this->connection->api_url( 'partner_provision_nonce_check' ) + ); + $response = Client::_wp_remote_request( + esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ), + array( 'method' => 'GET' ), + true + ); + + if ( + 200 !== wp_remote_retrieve_response_code( $response ) || + 'OK' !== trim( wp_remote_retrieve_body( $response ) ) + ) { + return $this->error( + new Jetpack_Error( + 'invalid_nonce', + __( 'There was an issue validating this request.', 'jetpack' ), + 400 + ), + 'remote_register' + ); + } + + if ( ! Jetpack_Options::get_option( 'id' ) || ! $this->connection->get_access_token() || ! empty( $request['force'] ) ) { + wp_set_current_user( $user->ID ); + + // This code mostly copied from Jetpack::admin_page_load. + Jetpack::maybe_set_version_option(); + $registered = Jetpack::try_registration(); + if ( is_wp_error( $registered ) ) { + return $this->error( $registered, 'remote_register' ); + } elseif ( ! $registered ) { + return $this->error( + new Jetpack_Error( + 'registration_error', + __( 'There was an unspecified error registering the site', 'jetpack' ), + 400 + ), + 'remote_register' + ); + } + } + + // This action is documented in class.jetpack-xmlrpc-server.php. + do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'success' ); + + return array( + 'client_id' => Jetpack_Options::get_option( 'id' ), + ); + } + + /** + * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to + * register this site so that a plan can be provisioned. + * + * @param array $request An array containing at minimum a nonce key and a local_username key. + * + * @return \WP_Error|array + */ + public function remote_provision( $request ) { + $user = $this->fetch_and_verify_local_user( $request ); + + if ( ! $user ) { + return $this->error( + new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), + 'remote_provision' + ); + } + + if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { + return $this->error( $user, 'remote_provision' ); + } + + $site_icon = get_site_icon_url(); + + $auto_enable_sso = ( ! $this->connection->is_active() || Jetpack::is_module_active( 'sso' ) ); + + /** This filter is documented in class.jetpack-cli.php */ + if ( apply_filters( 'jetpack_start_enable_sso', $auto_enable_sso ) ) { + $redirect_uri = add_query_arg( + array( + 'action' => 'jetpack-sso', + 'redirect_to' => rawurlencode( admin_url() ), + ), + wp_login_url() // TODO: come back to Jetpack dashboard? + ); + } else { + $redirect_uri = admin_url(); + } + + // Generate secrets. + $roles = new Roles(); + $role = $roles->translate_user_to_role( $user ); + $secrets = $this->connection->generate_secrets( 'authorize', $user->ID ); + + $response = array( + 'jp_version' => JETPACK__VERSION, + 'redirect_uri' => $redirect_uri, + 'user_id' => $user->ID, + 'user_email' => $user->user_email, + 'user_login' => $user->user_login, + 'scope' => $this->connection->sign_role( $role, $user->ID ), + 'secret' => $secrets['secret_1'], + 'is_active' => $this->connection->is_active(), + ); + + if ( $site_icon ) { + $response['site_icon'] = $site_icon; + } + + if ( ! empty( $request['onboarding'] ) ) { + Jetpack::create_onboarding_token(); + $response['onboarding_token'] = Jetpack_Options::get_option( 'onboarding' ); + } + + return $response; + } + + /** + * Given an array containing a local user identifier and a nonce, will attempt to fetch and set + * an access token for the given user. + * + * @param array $request An array containing local_user and nonce keys at minimum. + * @param \IXR_Client $ixr_client The client object, optional. + * @return mixed + */ + public function remote_connect( $request, $ixr_client = false ) { + if ( $this->connection->is_active() ) { + return $this->error( + new WP_Error( + 'already_connected', + __( 'Jetpack is already connected.', 'jetpack' ), + 400 + ), + 'remote_connect' + ); + } + + $user = $this->fetch_and_verify_local_user( $request ); + + if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { + return $this->error( + new WP_Error( + 'input_error', + __( 'Valid user is required.', 'jetpack' ), + 400 + ), + 'remote_connect' + ); + } + + if ( empty( $request['nonce'] ) ) { + return $this->error( + new WP_Error( + 'input_error', + __( 'A non-empty nonce must be supplied.', 'jetpack' ), + 400 + ), + 'remote_connect' + ); + } + + if ( ! $ixr_client ) { + $ixr_client = new Jetpack_IXR_Client(); + } + $ixr_client->query( + 'jetpack.getUserAccessToken', + array( + 'nonce' => sanitize_text_field( $request['nonce'] ), + 'external_user_id' => $user->ID, + ) + ); + + $token = $ixr_client->isError() ? false : $ixr_client->getResponse(); + if ( empty( $token ) ) { + return $this->error( + new WP_Error( + 'token_fetch_failed', + __( 'Failed to fetch user token from WordPress.com.', 'jetpack' ), + 400 + ), + 'remote_connect' + ); + } + $token = sanitize_text_field( $token ); + + Connection_Utils::update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true ); + + $this->do_post_authorization(); + + return $this->connection->is_active(); + } + + /** + * Getter for the local user to act as. + * + * @param array $request the current request data. + */ + private function fetch_and_verify_local_user( $request ) { + if ( empty( $request['local_user'] ) ) { + return $this->error( + new Jetpack_Error( + 'local_user_missing', + __( 'The required "local_user" parameter is missing.', 'jetpack' ), + 400 + ), + 'remote_provision' + ); + } + + // Local user is used to look up by login, email or ID. + $local_user_info = $request['local_user']; + + return $this->get_user_by_anything( $local_user_info ); + } + + /** + * Gets the user object by its data. + * + * @param string $user_id can be any identifying user data. + */ + private function get_user_by_anything( $user_id ) { + $user = get_user_by( 'login', $user_id ); + + if ( ! $user ) { + $user = get_user_by( 'email', $user_id ); + } + + if ( ! $user ) { + $user = get_user_by( 'ID', $user_id ); + } + + return $user; + } + + /** + * Possible error_codes: + * + * - verify_secret_1_missing + * - verify_secret_1_malformed + * - verify_secrets_missing: verification secrets are not found in database + * - verify_secrets_incomplete: verification secrets are only partially found in database + * - verify_secrets_expired: verification secrets have expired + * - verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com + * - state_missing: required parameter of state not found + * - state_malformed: state is not a digit + * - invalid_state: state in request does not match the stored state + * + * The 'authorize' and 'register' actions have additional error codes + * + * state_missing: a state ( user id ) was not supplied + * state_malformed: state is not the correct data type + * invalid_state: supplied state does not match the stored state + * + * @param array $params action An array of 3 parameters: + * [0]: string action. Possible values are `authorize`, `publicize` and `register`. + * [1]: string secret_1. + * [2]: int state. + * @return \IXR_Error|string IXR_Error on failure, secret_2 on success. + */ + public function verify_action( $params ) { + $action = isset( $params[0] ) ? $params[0] : ''; + $verify_secret = isset( $params[1] ) ? $params[1] : ''; + $state = isset( $params[2] ) ? $params[2] : ''; + + $result = $this->connection->verify_secrets( $action, $verify_secret, $state ); + + if ( is_wp_error( $result ) ) { + return $this->error( $result ); + } + + return $result; + } + + /** + * Wrapper for wp_authenticate( $username, $password ); + * + * @return \WP_User|bool + */ + public function login() { + $this->connection->require_jetpack_authentication(); + $user = wp_authenticate( 'username', 'password' ); + if ( is_wp_error( $user ) ) { + if ( 'authentication_failed' === $user->get_error_code() ) { // Generic error could mean most anything. + $this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 ); + } else { + $this->error = $user; + } + return false; + } elseif ( ! $user ) { // Shouldn't happen. + $this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 ); + return false; + } + + return $user; + } + + /** + * Returns the current error as an \IXR_Error + * + * @param \WP_Error|\IXR_Error $error The error object, optional. + * @param string $event_name The event name. + * @param \WP_User $user The user object. + * @return bool|\IXR_Error + */ + public function error( $error = null, $event_name = null, $user = null ) { + if ( null !== $event_name ) { + // This action is documented in class.jetpack-xmlrpc-server.php. + do_action( 'jetpack_xmlrpc_server_event', $event_name, 'fail', $error, $user ); + } + + if ( ! is_null( $error ) ) { + $this->error = $error; + } + + if ( is_wp_error( $this->error ) ) { + $code = $this->error->get_error_data(); + if ( ! $code ) { + $code = -10520; + } + $message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() ); + return new \IXR_Error( $code, $message ); + } elseif ( is_a( $this->error, 'IXR_Error' ) ) { + return $this->error; + } + + return false; + } + + /* API Methods */ + + /** + * Just authenticates with the given Jetpack credentials. + * + * @return string The current Jetpack version number + */ + public function test_connection() { + return JETPACK__VERSION; + } + + /** + * Test the API user code. + * + * @param array $args arguments identifying the test site. + */ + public function test_api_user_code( $args ) { + $client_id = (int) $args[0]; + $user_id = (int) $args[1]; + $nonce = (string) $args[2]; + $verify = (string) $args[3]; + + if ( ! $client_id || ! $user_id || ! strlen( $nonce ) || 32 !== strlen( $verify ) ) { + return false; + } + + $user = get_user_by( 'id', $user_id ); + if ( ! $user || is_wp_error( $user ) ) { + return false; + } + + /* phpcs:ignore + debugging + error_log( "CLIENT: $client_id" ); + error_log( "USER: $user_id" ); + error_log( "NONCE: $nonce" ); + error_log( "VERIFY: $verify" ); + */ + + $jetpack_token = $this->connection->get_access_token( $user_id ); + + $api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true ); + if ( ! $api_user_code ) { + return false; + } + + $hmac = hash_hmac( + 'md5', + json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + (object) array( + 'client_id' => (int) $client_id, + 'user_id' => (int) $user_id, + 'nonce' => (string) $nonce, + 'code' => (string) $api_user_code, + ) + ), + $jetpack_token->secret + ); + + if ( ! hash_equals( $hmac, $verify ) ) { + return false; + } + + return $user_id; + } + + /** + * Disconnect this blog from the connected wordpress.com account + * + * @return boolean + */ + public function disconnect_blog() { + + // For tracking. + if ( ! empty( $this->user->ID ) ) { + wp_set_current_user( $this->user->ID ); + } + + /** + * Fired when we want to log an event to the Jetpack event log. + * + * @since 7.7.0 + * + * @param string $code Unique name for the event. + * @param string $data Optional data about the event. + */ + do_action( 'jetpack_event_log', 'disconnect' ); + Jetpack::disconnect(); + + return true; + } + + /** + * Unlink a user from WordPress.com + * + * This will fail if called by the Master User. + */ + public function unlink_user() { + /** + * Fired when we want to log an event to the Jetpack event log. + * + * @since 7.7.0 + * + * @param string $code Unique name for the event. + * @param string $data Optional data about the event. + */ + do_action( 'jetpack_event_log', 'unlink' ); + return Connection_Manager::disconnect_user(); + } + + /** + * Returns any object that is able to be synced. + * + * @deprecated since 7.8.0 + * @see Automattic\Jetpack\Sync\Sender::sync_object() + * + * @param array $args the synchronized object parameters. + * @return string Encoded sync object. + */ + public function sync_object( $args ) { + _deprecated_function( __METHOD__, 'jetpack-7.8', 'Automattic\\Jetpack\\Sync\\Sender::sync_object' ); + return Sender::get_instance()->sync_object( $args ); + } + + /** + * Returns the home URL and site URL for the current site which can be used on the WPCOM side for + * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM + * and the remote Jetpack site. + * + * @return array + */ + public function validate_urls_for_idc_mitigation() { + return array( + 'home' => Functions::home_url(), + 'siteurl' => Functions::site_url(), + ); + } + + /** + * Returns what features are available. Uses the slug of the module files. + * + * @return array + */ + public function features_available() { + $raw_modules = Jetpack::get_available_modules(); + $modules = array(); + foreach ( $raw_modules as $module ) { + $modules[] = Jetpack::get_module_slug( $module ); + } + + return $modules; + } + + /** + * Returns what features are enabled. Uses the slug of the modules files. + * + * @return array + */ + public function features_enabled() { + $raw_modules = Jetpack::get_active_modules(); + $modules = array(); + foreach ( $raw_modules as $module ) { + $modules[] = Jetpack::get_module_slug( $module ); + } + + return $modules; + } + + /** + * Updates the attachment parent object. + * + * @param array $args attachment and parent identifiers. + */ + public function update_attachment_parent( $args ) { + $attachment_id = (int) $args[0]; + $parent_id = (int) $args[1]; + + return wp_update_post( + array( + 'ID' => $attachment_id, + 'post_parent' => $parent_id, + ) + ); + } + + /** + * Serve a JSON API request. + * + * @param array $args request arguments. + */ + public function json_api( $args = array() ) { + $json_api_args = $args[0]; + $verify_api_user_args = $args[1]; + + $method = (string) $json_api_args[0]; + $url = (string) $json_api_args[1]; + $post_body = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2]; + $user_details = (array) $json_api_args[4]; + $locale = (string) $json_api_args[5]; + + if ( ! $verify_api_user_args ) { + $user_id = 0; + } elseif ( 'internal' === $verify_api_user_args[0] ) { + $user_id = (int) $verify_api_user_args[1]; + if ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + if ( ! $user || is_wp_error( $user ) ) { + return false; + } + } + } else { + $user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args ); + if ( ! $user_id ) { + return false; + } + } + + /* phpcs:ignore + debugging + error_log( "-- begin json api via jetpack debugging -- " ); + error_log( "METHOD: $method" ); + error_log( "URL: $url" ); + error_log( "POST BODY: $post_body" ); + error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) ); + error_log( "VERIFIED USER_ID: " . (int) $user_id ); + error_log( "-- end json api via jetpack debugging -- " ); + */ + + if ( 'en' !== $locale ) { + // .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them. + $new_locale = $locale; + if ( strpos( $locale, '-' ) !== false ) { + $locale_pieces = explode( '-', $locale ); + $new_locale = $locale_pieces[0]; + $new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : ''; + } else { + // .com might pass 'fr' because thats what our language files are named as, where core seems + // to do fr_FR - so try that if we don't think we can load the file. + if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) { + $new_locale = $locale . '_' . strtoupper( $locale ); + } + } + + if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) { + unload_textdomain( 'default' ); + load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' ); + } + } + + $old_user = wp_get_current_user(); + wp_set_current_user( $user_id ); + + if ( $user_id ) { + $token_key = false; + } else { + $verified = $this->connection->verify_xml_rpc_signature(); + $token_key = $verified['token_key']; + } + + $token = $this->connection->get_access_token( $user_id, $token_key ); + if ( ! $token || is_wp_error( $token ) ) { + return false; + } + + define( 'REST_API_REQUEST', true ); + define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' ); + + // needed? + require_once ABSPATH . 'wp-admin/includes/admin.php'; + + require_once JETPACK__PLUGIN_DIR . 'class.json-api.php'; + $api = WPCOM_JSON_API::init( $method, $url, $post_body ); + $api->token_details['user'] = $user_details; + require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php'; + + $display_errors = ini_set( 'display_errors', 0 ); // phpcs:ignore WordPress.PHP.IniSet + ob_start(); + $api->serve( false ); + $output = ob_get_clean(); + ini_set( 'display_errors', $display_errors ); // phpcs:ignore WordPress.PHP.IniSet + + $nonce = wp_generate_password( 10, false ); + $hmac = hash_hmac( 'md5', $nonce . $output, $token->secret ); + + wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 ); + + return array( + (string) $output, + (string) $nonce, + (string) $hmac, + ); + } + + /** + * Handles authorization actions after connecting a site, such as enabling modules. + * + * This do_post_authorization() is used in this class, as opposed to calling + * Jetpack::handle_post_authorization_actions() directly so that we can mock this method as necessary. + * + * @return void + */ + public function do_post_authorization() { + /** This filter is documented in class.jetpack-cli.php */ + $enable_sso = apply_filters( 'jetpack_start_enable_sso', true ); + Jetpack::handle_post_authorization_actions( $enable_sso, false, false ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php new file mode 100644 index 00000000..de77dfc4 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php @@ -0,0 +1,13 @@ +<?php +/** + * WordPress IXR classes aren't always loaded by default. + * + * Here we ensure that they are loaded before we declare our implementations. + * + * @package automattic/jetpack-connection + * @since 7.7 + */ + +if ( defined( 'ABSPATH' ) && defined( 'WPINC' ) ) { + require_once ABSPATH . WPINC . '/class-IXR.php'; +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php new file mode 100644 index 00000000..0070f294 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php @@ -0,0 +1,455 @@ +<?php +/** + * The Connection Client class file. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +use Automattic\Jetpack\Constants; + +/** + * The Client class that is used to connect to WordPress.com Jetpack API. + */ +class Client { + const WPCOM_JSON_API_VERSION = '1.1'; + + /** + * Makes an authorized remote request using Jetpack_Signature + * + * @param Array $args the arguments for the remote request. + * @param Array|String $body the request body. + * @return array|WP_Error WP HTTP response on success + */ + public static function remote_request( $args, $body = null ) { + $defaults = array( + 'url' => '', + 'user_id' => 0, + 'blog_id' => 0, + 'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ), + 'method' => 'POST', + 'timeout' => 10, + 'redirection' => 0, + 'headers' => array(), + 'stream' => false, + 'filename' => null, + 'sslverify' => true, + ); + + $args = wp_parse_args( $args, $defaults ); + + $args['blog_id'] = (int) $args['blog_id']; + + if ( 'header' !== $args['auth_location'] ) { + $args['auth_location'] = 'query_string'; + } + + $connection = new Manager(); + $token = $connection->get_access_token( $args['user_id'] ); + if ( ! $token ) { + return new \WP_Error( 'missing_token' ); + } + + $method = strtoupper( $args['method'] ); + + $timeout = intval( $args['timeout'] ); + + $redirection = $args['redirection']; + $stream = $args['stream']; + $filename = $args['filename']; + $sslverify = $args['sslverify']; + + $request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' ); + + @list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( empty( $token ) || empty( $secret ) ) { + return new \WP_Error( 'malformed_token' ); + } + + $token_key = sprintf( + '%s:%d:%d', + $token_key, + Constants::get_constant( 'JETPACK__API_VERSION' ), + $token->external_user_id + ); + + $time_diff = (int) \Jetpack_Options::get_option( 'time_diff' ); + $jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff ); + + $timestamp = time() + $time_diff; + + if ( function_exists( 'wp_generate_password' ) ) { + $nonce = wp_generate_password( 10, false ); + } else { + $nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 ); + } + + // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing. + if ( is_null( $body ) ) { + $body_hash = ''; + + } else { + // Allow arrays to be used in passing data. + $body_to_hash = $body; + + if ( is_array( $body ) ) { + // We cast this to a new variable, because the array form of $body needs to be + // maintained so it can be passed into the request later on in the code. + if ( count( $body ) > 0 ) { + $body_to_hash = wp_json_encode( self::_stringify_data( $body ) ); + } else { + $body_to_hash = ''; + } + } + + if ( ! is_string( $body_to_hash ) ) { + return new \WP_Error( 'invalid_body', 'Body is malformed.' ); + } + + $body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } + + $auth = array( + 'token' => $token_key, + 'timestamp' => $timestamp, + 'nonce' => $nonce, + 'body-hash' => $body_hash, + ); + + if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) { + $url_args = array( + 'for' => 'jetpack', + 'wpcom_blog_id' => \Jetpack_Options::get_option( 'id' ), + ); + } else { + $url_args = array(); + } + + if ( 'header' !== $args['auth_location'] ) { + $url_args += $auth; + } + + $url = add_query_arg( urlencode_deep( $url_args ), $args['url'] ); + $url = Utils::fix_url_for_bad_hosts( $url ); + + $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false ); + + if ( ! $signature || is_wp_error( $signature ) ) { + return $signature; + } + + // Send an Authorization header so various caches/proxies do the right thing. + $auth['signature'] = $signature; + $auth['version'] = Constants::get_constant( 'JETPACK__VERSION' ); + $header_pieces = array(); + foreach ( $auth as $key => $value ) { + $header_pieces[] = sprintf( '%s="%s"', $key, $value ); + } + $request['headers'] = array_merge( + $args['headers'], + array( + 'Authorization' => 'X_JETPACK ' . join( ' ', $header_pieces ), + ) + ); + + if ( 'header' !== $args['auth_location'] ) { + $url = add_query_arg( 'signature', rawurlencode( $signature ), $url ); + } + + return self::_wp_remote_request( $url, $request ); + } + + /** + * Wrapper for wp_remote_request(). Turns off SSL verification for certain SSL errors. + * This is lame, but many, many, many hosts have misconfigured SSL. + * + * When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if: + * 1. a certificate error is found AND + * 2. not verifying the certificate works around the problem. + * + * The option is checked on each request. + * + * @internal + * @see Utils::fix_url_for_bad_hosts() + * + * @param String $url the request URL. + * @param Array $args request arguments. + * @param Boolean $set_fallback whether to allow flagging this request to use a fallback certficate override. + * @return array|WP_Error WP HTTP response on success + */ + public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + /** + * SSL verification (`sslverify`) for the JetpackClient remote request + * defaults to off, use this filter to force it on. + * + * Return `true` to ENABLE SSL verification, return `false` + * to DISABLE SSL verification. + * + * @since 3.6.0 + * + * @param bool Whether to force `sslverify` or not. + */ + if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) { + return wp_remote_request( $url, $args ); + } + + $fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ); + if ( false === $fallback ) { + \Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 ); + } + + if ( (int) $fallback ) { + // We're flagged to fallback. + $args['sslverify'] = false; + } + + $response = wp_remote_request( $url, $args ); + + if ( + ! $set_fallback // We're not allowed to set the flag on this request, so whatever happens happens. + || + isset( $args['sslverify'] ) && ! $args['sslverify'] // No verification - no point in doing it again. + || + ! is_wp_error( $response ) // Let it ride. + ) { + self::set_time_diff( $response, $set_fallback ); + return $response; + } + + // At this point, we're not flagged to fallback and we are allowed to set the flag on this request. + + $message = $response->get_error_message(); + + // Is it an SSL Certificate verification error? + if ( + false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error. + && + false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error. + && + false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found. + && + false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful + // Different versions of curl have different error messages + // this string should catch them all. + && + false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights. + ) { + // No, it is not. + return $response; + } + + // Redo the request without SSL certificate verification. + $args['sslverify'] = false; + $response = wp_remote_request( $url, $args ); + + if ( ! is_wp_error( $response ) ) { + // The request went through this time, flag for future fallbacks. + \Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() ); + self::set_time_diff( $response, $set_fallback ); + } + + return $response; + } + + /** + * Sets the time difference for correct signature computation. + * + * @param HTTP_Response $response the response object. + * @param Boolean $force_set whether to force setting the time difference. + */ + public static function set_time_diff( &$response, $force_set = false ) { + $code = wp_remote_retrieve_response_code( $response ); + + // Only trust the Date header on some responses. + if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + return; + } + + $date = wp_remote_retrieve_header( $response, 'date' ); + if ( ! $date ) { + return; + } + + $time = (int) strtotime( $date ); + if ( 0 >= $time ) { + return; + } + + $time_diff = $time - time(); + + if ( $force_set ) { // During register. + \Jetpack_Options::update_option( 'time_diff', $time_diff ); + } else { // Otherwise. + $old_diff = \Jetpack_Options::get_option( 'time_diff' ); + if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) { + \Jetpack_Options::update_option( 'time_diff', $time_diff ); + } + } + } + + /** + * Queries the WordPress.com REST API with a user token. + * + * @param string $path REST API path. + * @param string $version REST API version. Default is `2`. + * @param array $args Arguments to {@see WP_Http}. Default is `array()`. + * @param string $body Body passed to {@see WP_Http}. Default is `null`. + * @param string $base_api_path REST API root. Default is `wpcom`. + * + * @return array|WP_Error $response Response data, else {@see WP_Error} on failure. + */ + public static function wpcom_json_api_request_as_user( + $path, + $version = '2', + $args = array(), + $body = null, + $base_api_path = 'wpcom' + ) { + $base_api_path = trim( $base_api_path, '/' ); + $version = ltrim( $version, 'v' ); + $path = ltrim( $path, '/' ); + + $args = array_intersect_key( + $args, + array( + 'headers' => 'array', + 'method' => 'string', + 'timeout' => 'int', + 'redirection' => 'int', + 'stream' => 'boolean', + 'filename' => 'string', + 'sslverify' => 'boolean', + ) + ); + + $args['user_id'] = get_current_user_id(); + $args['method'] = isset( $args['method'] ) ? strtoupper( $args['method'] ) : 'GET'; + $args['url'] = sprintf( + '%s://%s/%s/v%s/%s', + self::protocol(), + Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' ), + $base_api_path, + $version, + $path + ); + + if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) { + $args['headers'] = array( 'Content-Type' => 'application/json' ); + } + + if ( isset( $body ) && ! is_string( $body ) ) { + $body = wp_json_encode( $body ); + } + + return self::remote_request( $args, $body ); + } + + /** + * Query the WordPress.com REST API using the blog token + * + * @param String $path The API endpoint relative path. + * @param String $version The API version. + * @param Array $args Request arguments. + * @param String $body Request body. + * @param String $base_api_path (optional) the API base path override, defaults to 'rest'. + * @return Array|WP_Error $response Data. + */ + public static function wpcom_json_api_request_as_blog( + $path, + $version = self::WPCOM_JSON_API_VERSION, + $args = array(), + $body = null, + $base_api_path = 'rest' + ) { + $filtered_args = array_intersect_key( + $args, + array( + 'headers' => 'array', + 'method' => 'string', + 'timeout' => 'int', + 'redirection' => 'int', + 'stream' => 'boolean', + 'filename' => 'string', + 'sslverify' => 'boolean', + ) + ); + + // unprecedingslashit. + $_path = preg_replace( '/^\//', '', $path ); + + // Use GET by default whereas `remote_request` uses POST. + $request_method = ( isset( $filtered_args['method'] ) ) ? $filtered_args['method'] : 'GET'; + + $url = sprintf( + '%s://%s/%s/v%s/%s', + self::protocol(), + Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' ), + $base_api_path, + $version, + $_path + ); + + $validated_args = array_merge( + $filtered_args, + array( + 'url' => $url, + 'blog_id' => (int) \Jetpack_Options::get_option( 'id' ), + 'method' => $request_method, + ) + ); + + return self::remote_request( $validated_args, $body ); + } + + /** + * Takes an array or similar structure and recursively turns all values into strings. This is used to + * make sure that body hashes are made ith the string version, which is what will be seen after a + * server pulls up the data in the $_POST array. + * + * @param Array|Mixed $data the data that needs to be stringified. + * + * @return array|string + */ + public static function _stringify_data( $data ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + + // Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string. + if ( is_bool( $data ) ) { + return $data ? '1' : '0'; + } + + // Cast objects into arrays. + if ( is_object( $data ) ) { + $data = (array) $data; + } + + // Non arrays at this point should be just converted to strings. + if ( ! is_array( $data ) ) { + return (string) $data; + } + + foreach ( $data as $key => &$value ) { + $value = self::_stringify_data( $value ); + } + + return $data; + } + + /** + * Gets protocol string. + * + * @return string `https` (if possible), else `http`. + */ + public static function protocol() { + /** + * Determines whether Jetpack can send outbound https requests to the WPCOM api. + * + * @since 3.6.0 + * + * @param bool $proto Defaults to true. + */ + $https = apply_filters( 'jetpack_can_make_outbound_https', true ); + + return $https ? 'https' : 'http'; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php new file mode 100644 index 00000000..f37dbf88 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php @@ -0,0 +1,2169 @@ +<?php +/** + * The Jetpack Connection manager class file. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +use Automattic\Jetpack\Constants; +use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Tracking; + +/** + * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com + * and Jetpack. + */ +class Manager { + + const SECRETS_MISSING = 'secrets_missing'; + const SECRETS_EXPIRED = 'secrets_expired'; + const SECRETS_OPTION_NAME = 'jetpack_secrets'; + const MAGIC_NORMAL_TOKEN_KEY = ';normal;'; + const JETPACK_MASTER_USER = true; + + /** + * The procedure that should be run to generate secrets. + * + * @var Callable + */ + protected $secret_callable; + + /** + * A copy of the raw POST data for signature verification purposes. + * + * @var String + */ + protected $raw_post_data; + + /** + * Verification data needs to be stored to properly verify everything. + * + * @var Object + */ + private $xmlrpc_verification = null; + + /** + * Initializes required listeners. This is done separately from the constructors + * because some objects sometimes need to instantiate separate objects of this class. + * + * @todo Implement a proper nonce verification. + */ + public function init() { + $this->setup_xmlrpc_handlers( + $_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $this->is_active(), + $this->verify_xml_rpc_signature() + ); + + if ( $this->is_active() ) { + add_filter( 'xmlrpc_methods', array( $this, 'public_xmlrpc_methods' ) ); + } else { + add_action( 'rest_api_init', array( $this, 'initialize_rest_api_registration_connector' ) ); + } + + add_action( 'jetpack_clean_nonces', array( $this, 'clean_nonces' ) ); + if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) { + wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); + } + } + + /** + * Sets up the XMLRPC request handlers. + * + * @param Array $request_params incoming request parameters. + * @param Boolean $is_active whether the connection is currently active. + * @param Boolean $is_signed whether the signature check has been successful. + * @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one. + */ + public function setup_xmlrpc_handlers( + $request_params, + $is_active, + $is_signed, + \Jetpack_XMLRPC_Server $xmlrpc_server = null + ) { + add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 ); + + if ( + ! isset( $request_params['for'] ) + || 'jetpack' !== $request_params['for'] + ) { + return false; + } + + // Alternate XML-RPC, via ?for=jetpack&jetpack=comms. + if ( + isset( $request_params['jetpack'] ) + && 'comms' === $request_params['jetpack'] + ) { + if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) { + // Use the real constant here for WordPress' sake. + define( 'XMLRPC_REQUEST', true ); + } + + add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) ); + + add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 ); + } + + if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) { + return false; + } + // Display errors can cause the XML to be not well formed. + @ini_set( 'display_errors', false ); // phpcs:ignore + + if ( $xmlrpc_server ) { + $this->xmlrpc_server = $xmlrpc_server; + } else { + $this->xmlrpc_server = new \Jetpack_XMLRPC_Server(); + } + + $this->require_jetpack_authentication(); + + if ( $is_active ) { + // Hack to preserve $HTTP_RAW_POST_DATA. + add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); + + if ( $is_signed ) { + // The actual API methods. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) ); + } else { + // The jetpack.authorize method should be available for unauthenticated users on a site with an + // active Jetpack connection, so that additional users can link their account. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) ); + } + } else { + // The bootstrap API methods. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) ); + + if ( $is_signed ) { + // The jetpack Provision method is available for blog-token-signed requests. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) ); + } else { + new XMLRPC_Connector( $this ); + } + } + + // Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on. + add_filter( 'pre_option_enable_xmlrpc', '__return_true' ); + return true; + } + + /** + * Initializes the REST API connector on the init hook. + */ + public function initialize_rest_api_registration_connector() { + new REST_Connector( $this ); + } + + /** + * Since a lot of hosts use a hammer approach to "protecting" WordPress sites, + * and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive + * security/firewall policies, we provide our own alternate XML RPC API endpoint + * which is accessible via a different URI. Most of the below is copied directly + * from /xmlrpc.php so that we're replicating it as closely as possible. + * + * @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things. + */ + public function alternate_xmlrpc() { + // phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved + // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited + global $HTTP_RAW_POST_DATA; + + // Some browser-embedded clients send cookies. We don't want them. + $_COOKIE = array(); + + // A fix for mozBlog and other cases where '<?xml' isn't on the very first line. + if ( isset( $HTTP_RAW_POST_DATA ) ) { + $HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA ); + } + + // phpcs:enable + + include_once ABSPATH . 'wp-admin/includes/admin.php'; + include_once ABSPATH . WPINC . '/class-IXR.php'; + include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'; + + /** + * Filters the class used for handling XML-RPC requests. + * + * @since 3.1.0 + * + * @param string $class The name of the XML-RPC server class. + */ + $wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' ); + $wp_xmlrpc_server = new $wp_xmlrpc_server_class(); + + // Fire off the request. + nocache_headers(); + $wp_xmlrpc_server->serve_request(); + + exit; + } + + /** + * Removes all XML-RPC methods that are not `jetpack.*`. + * Only used in our alternate XML-RPC endpoint, where we want to + * ensure that Core and other plugins' methods are not exposed. + * + * @param array $methods a list of registered WordPress XMLRPC methods. + * @return array filtered $methods + */ + public function remove_non_jetpack_xmlrpc_methods( $methods ) { + $jetpack_methods = array(); + + foreach ( $methods as $method => $callback ) { + if ( 0 === strpos( $method, 'jetpack.' ) ) { + $jetpack_methods[ $method ] = $callback; + } + } + + return $jetpack_methods; + } + + /** + * Removes all other authentication methods not to allow other + * methods to validate unauthenticated requests. + */ + public function require_jetpack_authentication() { + // Don't let anyone authenticate. + $_COOKIE = array(); + remove_all_filters( 'authenticate' ); + remove_all_actions( 'wp_login_failed' ); + + if ( $this->is_active() ) { + // Allow Jetpack authentication. + add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 ); + } + } + + /** + * Authenticates XML-RPC and other requests from the Jetpack Server + * + * @param WP_User|Mixed $user user object if authenticated. + * @param String $username username. + * @param String $password password string. + * @return WP_User|Mixed authenticated user or error. + */ + public function authenticate_jetpack( $user, $username, $password ) { + if ( is_a( $user, '\\WP_User' ) ) { + return $user; + } + + $token_details = $this->verify_xml_rpc_signature(); + + if ( ! $token_details ) { + return $user; + } + + if ( 'user' !== $token_details['type'] ) { + return $user; + } + + if ( ! $token_details['user_id'] ) { + return $user; + } + + nocache_headers(); + + return new \WP_User( $token_details['user_id'] ); + } + + /** + * Verifies the signature of the current request. + * + * @return false|array + */ + public function verify_xml_rpc_signature() { + if ( is_null( $this->xmlrpc_verification ) ) { + $this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature(); + + if ( is_wp_error( $this->xmlrpc_verification ) ) { + /** + * Action for logging XMLRPC signature verification errors. This data is sensitive. + * + * Error codes: + * - malformed_token + * - malformed_user_id + * - unknown_token + * - could_not_sign + * - invalid_nonce + * - signature_mismatch + * + * @since 7.5.0 + * + * @param WP_Error $signature_verification_error The verification error + */ + do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification ); + } + } + + return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification; + } + + /** + * Verifies the signature of the current request. + * + * This function has side effects and should not be used. Instead, + * use the memoized version `->verify_xml_rpc_signature()`. + * + * @internal + * @todo Refactor to use proper nonce verification. + */ + private function internal_verify_xml_rpc_signature() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // It's not for us. + if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) { + return false; + } + + $signature_details = array( + 'token' => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '', + 'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '', + 'nonce' => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '', + 'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '', + 'method' => wp_unslash( $_SERVER['REQUEST_METHOD'] ), + 'url' => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later. + 'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '', + ); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) ); + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( + empty( $token_key ) + || + empty( $version ) || strval( JETPACK__API_VERSION ) !== $version + ) { + return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) ); + } + + if ( '0' === $user_id ) { + $token_type = 'blog'; + $user_id = 0; + } else { + $token_type = 'user'; + if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) { + return new \WP_Error( + 'malformed_user_id', + 'Malformed user_id in request', + compact( 'signature_details' ) + ); + } + $user_id = (int) $user_id; + + $user = new \WP_User( $user_id ); + if ( ! $user || ! $user->exists() ) { + return new \WP_Error( + 'unknown_user', + sprintf( 'User %d does not exist', $user_id ), + compact( 'signature_details' ) + ); + } + } + + $token = $this->get_access_token( $user_id, $token_key, false ); + if ( is_wp_error( $token ) ) { + $token->add_data( compact( 'signature_details' ) ); + return $token; + } elseif ( ! $token ) { + return new \WP_Error( + 'unknown_token', + sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ), + compact( 'signature_details' ) + ); + } + + $jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) ); + // phpcs:disable WordPress.Security.NonceVerification.Missing + if ( isset( $_POST['_jetpack_is_multipart'] ) ) { + $post_data = $_POST; + $file_hashes = array(); + foreach ( $post_data as $post_data_key => $post_data_value ) { + if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) { + continue; + } + $post_data_key = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) ); + $file_hashes[ $post_data_key ] = $post_data_value; + } + + foreach ( $file_hashes as $post_data_key => $post_data_value ) { + unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] ); + $post_data[ $post_data_key ] = $post_data_value; + } + + ksort( $post_data ); + + $body = http_build_query( stripslashes_deep( $post_data ) ); + } elseif ( is_null( $this->raw_post_data ) ) { + $body = file_get_contents( 'php://input' ); + } else { + $body = null; + } + // phpcs:enable + + $signature = $jetpack_signature->sign_current_request( + array( 'body' => is_null( $body ) ? $this->raw_post_data : $body ) + ); + + $signature_details['url'] = $jetpack_signature->current_request_url; + + if ( ! $signature ) { + return new \WP_Error( + 'could_not_sign', + 'Unknown signature error', + compact( 'signature_details' ) + ); + } elseif ( is_wp_error( $signature ) ) { + return $signature; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $timestamp = (int) $_GET['timestamp']; + $nonce = stripslashes( (string) $_GET['nonce'] ); + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + // Use up the nonce regardless of whether the signature matches. + if ( ! $this->add_nonce( $timestamp, $nonce ) ) { + return new \WP_Error( + 'invalid_nonce', + 'Could not add nonce', + compact( 'signature_details' ) + ); + } + + // Be careful about what you do with this debugging data. + // If a malicious requester has access to the expected signature, + // bad things might be possible. + $signature_details['expected'] = $signature; + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! hash_equals( $signature, $_GET['signature'] ) ) { + return new \WP_Error( + 'signature_mismatch', + 'Signature mismatch', + compact( 'signature_details' ) + ); + } + + /** + * Action for additional token checking. + * + * @since 7.7.0 + * + * @param Array $post_data request data. + * @param Array $token_data token data. + */ + return apply_filters( + 'jetpack_signature_check_token', + array( + 'type' => $token_type, + 'token_key' => $token_key, + 'user_id' => $token->external_user_id, + ), + $token, + $this->raw_post_data + ); + } + + /** + * Returns true if the current site is connected to WordPress.com. + * + * @return Boolean is the site connected? + */ + public function is_active() { + return (bool) $this->get_access_token( self::JETPACK_MASTER_USER ); + } + + /** + * Returns true if the site has both a token and a blog id, which indicates a site has been registered. + * + * @access public + * + * @return bool + */ + public function is_registered() { + $blog_id = \Jetpack_Options::get_option( 'id' ); + $has_token = $this->is_active(); + return $blog_id && $has_token; + } + + /** + * Checks to see if the connection owner of the site is missing. + * + * @return bool + */ + public function is_missing_connection_owner() { + $connection_owner = $this->get_connection_owner_id(); + if ( ! get_user_by( 'id', $connection_owner ) ) { + return true; + } + + return false; + } + + /** + * Returns true if the user with the specified identifier is connected to + * WordPress.com. + * + * @param Integer|Boolean $user_id the user identifier. + * @return Boolean is the user connected? + */ + public function is_user_connected( $user_id = false ) { + $user_id = false === $user_id ? get_current_user_id() : absint( $user_id ); + if ( ! $user_id ) { + return false; + } + + return (bool) $this->get_access_token( $user_id ); + } + + /** + * Returns the local user ID of the connection owner. + * + * @return string|int Returns the ID of the connection owner or False if no connection owner found. + */ + public function get_connection_owner_id() { + $user_token = $this->get_access_token( JETPACK_MASTER_USER ); + $connection_owner = false; + if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) { + $connection_owner = $user_token->external_user_id; + } + + return $connection_owner; + } + + /** + * Returns an array of user_id's that have user tokens for communicating with wpcom. + * Able to select by specific capability. + * + * @param string $capability The capability of the user. + * @return array Array of WP_User objects if found. + */ + public function get_connected_users( $capability = 'any' ) { + $connected_users = array(); + $connected_user_ids = array_keys( \Jetpack_Options::get_option( 'user_tokens' ) ); + + if ( ! empty( $connected_user_ids ) ) { + foreach ( $connected_user_ids as $id ) { + // Check for capability. + if ( 'any' !== $capability && ! user_can( $id, $capability ) ) { + continue; + } + + $connected_users[] = get_userdata( $id ); + } + } + + return $connected_users; + } + + /** + * Get the wpcom user data of the current|specified connected user. + * + * @todo Refactor to properly load the XMLRPC client independently. + * + * @param Integer $user_id the user identifier. + * @return Object the user object. + */ + public function get_connected_user_data( $user_id = null ) { + if ( ! $user_id ) { + $user_id = get_current_user_id(); + } + + $transient_key = "jetpack_connected_user_data_$user_id"; + $cached_user_data = get_transient( $transient_key ); + + if ( $cached_user_data ) { + return $cached_user_data; + } + + $xml = new \Jetpack_IXR_Client( + array( + 'user_id' => $user_id, + ) + ); + $xml->query( 'wpcom.getUser' ); + if ( ! $xml->isError() ) { + $user_data = $xml->getResponse(); + set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS ); + return $user_data; + } + + return false; + } + + /** + * Returns a user object of the connection owner. + * + * @return object|false False if no connection owner found. + */ + public function get_connection_owner() { + $user_token = $this->get_access_token( JETPACK_MASTER_USER ); + + $connection_owner = false; + if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) { + $connection_owner = get_userdata( $user_token->external_user_id ); + } + + return $connection_owner; + } + + /** + * Returns true if the provided user is the Jetpack connection owner. + * If user ID is not specified, the current user will be used. + * + * @param Integer|Boolean $user_id the user identifier. False for current user. + * @return Boolean True the user the connection owner, false otherwise. + */ + public function is_connection_owner( $user_id = false ) { + if ( ! $user_id ) { + $user_id = get_current_user_id(); + } + + $user_token = $this->get_access_token( JETPACK_MASTER_USER ); + + return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id; + } + + /** + * Connects the user with a specified ID to a WordPress.com user using the + * remote login flow. + * + * @access public + * + * @param Integer $user_id (optional) the user identifier, defaults to current user. + * @param String $redirect_url the URL to redirect the user to for processing, defaults to + * admin_url(). + * @return WP_Error only in case of a failed user lookup. + */ + public function connect_user( $user_id = null, $redirect_url = null ) { + $user = null; + if ( null === $user_id ) { + $user = wp_get_current_user(); + } else { + $user = get_user_by( 'ID', $user_id ); + } + + if ( empty( $user ) ) { + return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' ); + } + + if ( null === $redirect_url ) { + $redirect_url = admin_url(); + } + + // Using wp_redirect intentionally because we're redirecting outside. + wp_redirect( $this->get_authorization_url( $user ) ); // phpcs:ignore WordPress.Security.SafeRedirect + exit(); + } + + /** + * Unlinks the current user from the linked WordPress.com user. + * + * @access public + * @static + * + * @todo Refactor to properly load the XMLRPC client independently. + * + * @param Integer $user_id the user identifier. + * @return Boolean Whether the disconnection of the user was successful. + */ + public static function disconnect_user( $user_id = null ) { + $tokens = \Jetpack_Options::get_option( 'user_tokens' ); + if ( ! $tokens ) { + return false; + } + + $user_id = empty( $user_id ) ? get_current_user_id() : intval( $user_id ); + + if ( \Jetpack_Options::get_option( 'master_user' ) === $user_id ) { + return false; + } + + if ( ! isset( $tokens[ $user_id ] ) ) { + return false; + } + + $xml = new \Jetpack_IXR_Client( compact( 'user_id' ) ); + $xml->query( 'jetpack.unlink_user', $user_id ); + + unset( $tokens[ $user_id ] ); + + \Jetpack_Options::update_option( 'user_tokens', $tokens ); + + /** + * Fires after the current user has been unlinked from WordPress.com. + * + * @since 4.1.0 + * + * @param int $user_id The current user's ID. + */ + do_action( 'jetpack_unlinked_user', $user_id ); + + return true; + } + + /** + * Returns the requested Jetpack API URL. + * + * @param String $relative_url the relative API path. + * @return String API URL. + */ + public function api_url( $relative_url ) { + $api_base = Constants::get_constant( 'JETPACK__API_BASE' ); + $version = Constants::get_constant( 'JETPACK__API_VERSION' ); + + $api_base = $api_base ? $api_base : 'https://jetpack.wordpress.com/jetpack.'; + $version = $version ? '/' . $version . '/' : '/1/'; + + /** + * Filters the API URL that Jetpack uses for server communication. + * + * @since 8.0.0 + * + * @param String $url the generated URL. + * @param String $relative_url the relative URL that was passed as an argument. + * @param String $api_base the API base string that is being used. + * @param String $version the version string that is being used. + */ + return apply_filters( + 'jetpack_api_url', + rtrim( $api_base . $relative_url, '/\\' ) . $version, + $relative_url, + $api_base, + $version + ); + } + + /** + * Returns the Jetpack XMLRPC WordPress.com API endpoint URL. + * + * @return String XMLRPC API URL. + */ + public function xmlrpc_api_url() { + $base = preg_replace( + '#(https?://[^?/]+)(/?.*)?$#', + '\\1', + Constants::get_constant( 'JETPACK__API_BASE' ) + ); + return untrailingslashit( $base ) . '/xmlrpc.php'; + } + + /** + * Attempts Jetpack registration which sets up the site for connection. Should + * remain public because the call to action comes from the current site, not from + * WordPress.com. + * + * @param String $api_endpoint (optional) an API endpoint to use, defaults to 'register'. + * @return Integer zero on success, or a bitmask on failure. + */ + public function register( $api_endpoint = 'register' ) { + add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) ); + $secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 ); + + if ( + empty( $secrets['secret_1'] ) || + empty( $secrets['secret_2'] ) || + empty( $secrets['exp'] ) + ) { + return new \WP_Error( 'missing_secrets' ); + } + + // Better to try (and fail) to set a higher timeout than this system + // supports than to have register fail for more users than it should. + $timeout = $this->set_min_time_limit( 60 ) / 2; + + $gmt_offset = get_option( 'gmt_offset' ); + if ( ! $gmt_offset ) { + $gmt_offset = 0; + } + + $stats_options = get_option( 'stats_options' ); + $stats_id = isset( $stats_options['blog_id'] ) + ? $stats_options['blog_id'] + : null; + + /** + * Filters the request body for additional property addition. + * + * @since 7.7.0 + * + * @param Array $post_data request data. + * @param Array $token_data token data. + */ + $body = apply_filters( + 'jetpack_register_request_body', + array( + 'siteurl' => site_url(), + 'home' => home_url(), + 'gmt_offset' => $gmt_offset, + 'timezone_string' => (string) get_option( 'timezone_string' ), + 'site_name' => (string) get_option( 'blogname' ), + 'secret_1' => $secrets['secret_1'], + 'secret_2' => $secrets['secret_2'], + 'site_lang' => get_locale(), + 'timeout' => $timeout, + 'stats_id' => $stats_id, + 'state' => get_current_user_id(), + 'site_created' => $this->get_assumed_site_creation_date(), + 'jetpack_version' => Constants::get_constant( 'JETPACK__VERSION' ), + ) + ); + + $args = array( + 'method' => 'POST', + 'body' => $body, + 'headers' => array( + 'Accept' => 'application/json', + ), + 'timeout' => $timeout, + ); + + $args['body'] = $this->apply_activation_source_to_args( $args['body'] ); + + // TODO: fix URLs for bad hosts. + $response = Client::_wp_remote_request( + $this->api_url( $api_endpoint ), + $args, + true + ); + + // Make sure the response is valid and does not contain any Jetpack errors. + $registration_details = $this->validate_remote_register_response( $response ); + + if ( is_wp_error( $registration_details ) ) { + return $registration_details; + } elseif ( ! $registration_details ) { + return new \WP_Error( + 'unknown_error', + 'Unknown error registering your Jetpack site.', + wp_remote_retrieve_response_code( $response ) + ); + } + + if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) { + return new \WP_Error( + 'jetpack_secret', + 'Unable to validate registration of your Jetpack site.', + wp_remote_retrieve_response_code( $response ) + ); + } + + if ( isset( $registration_details->jetpack_public ) ) { + $jetpack_public = (int) $registration_details->jetpack_public; + } else { + $jetpack_public = false; + } + + \Jetpack_Options::update_options( + array( + 'id' => (int) $registration_details->jetpack_id, + 'blog_token' => (string) $registration_details->jetpack_secret, + 'public' => $jetpack_public, + ) + ); + + /** + * Fires when a site is registered on WordPress.com. + * + * @since 3.7.0 + * + * @param int $json->jetpack_id Jetpack Blog ID. + * @param string $json->jetpack_secret Jetpack Blog Token. + * @param int|bool $jetpack_public Is the site public. + */ + do_action( + 'jetpack_site_registered', + $registration_details->jetpack_id, + $registration_details->jetpack_secret, + $jetpack_public + ); + + if ( isset( $registration_details->token ) ) { + /** + * Fires when a user token is sent along with the registration data. + * + * @since 7.6.0 + * + * @param object $token the administrator token for the newly registered site. + */ + do_action( 'jetpack_site_registered_user_token', $registration_details->token ); + } + + return true; + } + + /** + * Takes the response from the Jetpack register new site endpoint and + * verifies it worked properly. + * + * @since 2.6 + * + * @param Mixed $response the response object, or the error object. + * @return string|WP_Error A JSON object on success or Jetpack_Error on failures + **/ + protected function validate_remote_register_response( $response ) { + if ( is_wp_error( $response ) ) { + return new \WP_Error( + 'register_http_request_failed', + $response->get_error_message() + ); + } + + $code = wp_remote_retrieve_response_code( $response ); + $entity = wp_remote_retrieve_body( $response ); + + if ( $entity ) { + $registration_response = json_decode( $entity ); + } else { + $registration_response = false; + } + + $code_type = intval( $code / 100 ); + if ( 5 === $code_type ) { + return new \WP_Error( 'wpcom_5??', $code ); + } elseif ( 408 === $code ) { + return new \WP_Error( 'wpcom_408', $code ); + } elseif ( ! empty( $registration_response->error ) ) { + if ( + 'xml_rpc-32700' === $registration_response->error + && ! function_exists( 'xml_parser_create' ) + ) { + $error_description = __( "PHP's XML extension is not available. Jetpack requires the XML extension to communicate with WordPress.com. Please contact your hosting provider to enable PHP's XML extension.", 'jetpack' ); + } else { + $error_description = isset( $registration_response->error_description ) + ? (string) $registration_response->error_description + : ''; + } + + return new \WP_Error( + (string) $registration_response->error, + $error_description, + $code + ); + } elseif ( 200 !== $code ) { + return new \WP_Error( 'wpcom_bad_response', $code ); + } + + // Jetpack ID error block. + if ( empty( $registration_response->jetpack_id ) ) { + return new \WP_Error( + 'jetpack_id', + /* translators: %s is an error message string */ + sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ), + $entity + ); + } elseif ( ! is_scalar( $registration_response->jetpack_id ) ) { + return new \WP_Error( + 'jetpack_id', + /* translators: %s is an error message string */ + sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ), + $entity + ); + } elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) { + return new \WP_Error( + 'jetpack_id', + /* translators: %s is an error message string */ + sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ), + $entity + ); + } + + return $registration_response; + } + + /** + * Adds a used nonce to a list of known nonces. + * + * @param int $timestamp the current request timestamp. + * @param string $nonce the nonce value. + * @return bool whether the nonce is unique or not. + */ + public function add_nonce( $timestamp, $nonce ) { + global $wpdb; + static $nonces_used_this_request = array(); + + if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) { + return $nonces_used_this_request[ "$timestamp:$nonce" ]; + } + + // This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce. + $timestamp = (int) $timestamp; + $nonce = esc_sql( $nonce ); + + // Raw query so we can avoid races: add_option will also update. + $show_errors = $wpdb->show_errors( false ); + + $old_nonce = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" ) + ); + + if ( is_null( $old_nonce ) ) { + $return = $wpdb->query( + $wpdb->prepare( + "INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)", + "jetpack_nonce_{$timestamp}_{$nonce}", + time(), + 'no' + ) + ); + } else { + $return = false; + } + + $wpdb->show_errors( $show_errors ); + + $nonces_used_this_request[ "$timestamp:$nonce" ] = $return; + + return $return; + } + + /** + * Cleans nonces that were saved when calling ::add_nonce. + * + * @todo Properly prepare the query before executing it. + * + * @param bool $all whether to clean even non-expired nonces. + */ + public function clean_nonces( $all = false ) { + global $wpdb; + + $sql = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s"; + $sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' ); + + if ( true !== $all ) { + $sql .= ' AND CAST( `option_value` AS UNSIGNED ) < %d'; + $sql_args[] = time() - 3600; + } + + $sql .= ' ORDER BY `option_id` LIMIT 100'; + + $sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + for ( $i = 0; $i < 1000; $i++ ) { + if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + break; + } + } + } + + /** + * Builds the timeout limit for queries talking with the wpcom servers. + * + * Based on local php max_execution_time in php.ini + * + * @since 5.4 + * @return int + **/ + public function get_max_execution_time() { + $timeout = (int) ini_get( 'max_execution_time' ); + + // Ensure exec time set in php.ini. + if ( ! $timeout ) { + $timeout = 30; + } + return $timeout; + } + + /** + * Sets a minimum request timeout, and returns the current timeout + * + * @since 5.4 + * @param Integer $min_timeout the minimum timeout value. + **/ + public function set_min_time_limit( $min_timeout ) { + $timeout = $this->get_max_execution_time(); + if ( $timeout < $min_timeout ) { + $timeout = $min_timeout; + set_time_limit( $timeout ); + } + return $timeout; + } + + /** + * Get our assumed site creation date. + * Calculated based on the earlier date of either: + * - Earliest admin user registration date. + * - Earliest date of post of any post type. + * + * @since 7.2.0 + * + * @return string Assumed site creation date and time. + */ + public function get_assumed_site_creation_date() { + $cached_date = get_transient( 'jetpack_assumed_site_creation_date' ); + if ( ! empty( $cached_date ) ) { + return $cached_date; + } + + $earliest_registered_users = get_users( + array( + 'role' => 'administrator', + 'orderby' => 'user_registered', + 'order' => 'ASC', + 'fields' => array( 'user_registered' ), + 'number' => 1, + ) + ); + $earliest_registration_date = $earliest_registered_users[0]->user_registered; + + $earliest_posts = get_posts( + array( + 'posts_per_page' => 1, + 'post_type' => 'any', + 'post_status' => 'any', + 'orderby' => 'date', + 'order' => 'ASC', + ) + ); + + // If there are no posts at all, we'll count only on user registration date. + if ( $earliest_posts ) { + $earliest_post_date = $earliest_posts[0]->post_date; + } else { + $earliest_post_date = PHP_INT_MAX; + } + + $assumed_date = min( $earliest_registration_date, $earliest_post_date ); + set_transient( 'jetpack_assumed_site_creation_date', $assumed_date ); + + return $assumed_date; + } + + /** + * Adds the activation source string as a parameter to passed arguments. + * + * @todo Refactor to use rawurlencode() instead of urlencode(). + * + * @param Array $args arguments that need to have the source added. + * @return Array $amended arguments. + */ + public static function apply_activation_source_to_args( $args ) { + list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' ); + + if ( $activation_source_name ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode + $args['_as'] = urlencode( $activation_source_name ); + } + + if ( $activation_source_keyword ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode + $args['_ak'] = urlencode( $activation_source_keyword ); + } + + return $args; + } + + /** + * Returns the callable that would be used to generate secrets. + * + * @return Callable a function that returns a secure string to be used as a secret. + */ + protected function get_secret_callable() { + if ( ! isset( $this->secret_callable ) ) { + /** + * Allows modification of the callable that is used to generate connection secrets. + * + * @param Callable a function or method that returns a secret string. + */ + $this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' ); + } + + return $this->secret_callable; + } + + /** + * Generates two secret tokens and the end of life timestamp for them. + * + * @param String $action The action name. + * @param Integer $user_id The user identifier. + * @param Integer $exp Expiration time in seconds. + */ + public function generate_secrets( $action, $user_id = false, $exp = 600 ) { + if ( false === $user_id ) { + $user_id = get_current_user_id(); + } + + $callable = $this->get_secret_callable(); + + $secrets = \Jetpack_Options::get_raw_option( + self::SECRETS_OPTION_NAME, + array() + ); + + $secret_name = 'jetpack_' . $action . '_' . $user_id; + + if ( + isset( $secrets[ $secret_name ] ) && + $secrets[ $secret_name ]['exp'] > time() + ) { + return $secrets[ $secret_name ]; + } + + $secret_value = array( + 'secret_1' => call_user_func( $callable ), + 'secret_2' => call_user_func( $callable ), + 'exp' => time() + $exp, + ); + + $secrets[ $secret_name ] = $secret_value; + + \Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); + return $secrets[ $secret_name ]; + } + + /** + * Returns two secret tokens and the end of life timestamp for them. + * + * @param String $action The action name. + * @param Integer $user_id The user identifier. + * @return string|array an array of secrets or an error string. + */ + public function get_secrets( $action, $user_id ) { + $secret_name = 'jetpack_' . $action . '_' . $user_id; + $secrets = \Jetpack_Options::get_raw_option( + self::SECRETS_OPTION_NAME, + array() + ); + + if ( ! isset( $secrets[ $secret_name ] ) ) { + return self::SECRETS_MISSING; + } + + if ( $secrets[ $secret_name ]['exp'] < time() ) { + $this->delete_secrets( $action, $user_id ); + return self::SECRETS_EXPIRED; + } + + return $secrets[ $secret_name ]; + } + + /** + * Deletes secret tokens in case they, for example, have expired. + * + * @param String $action The action name. + * @param Integer $user_id The user identifier. + */ + public function delete_secrets( $action, $user_id ) { + $secret_name = 'jetpack_' . $action . '_' . $user_id; + $secrets = \Jetpack_Options::get_raw_option( + self::SECRETS_OPTION_NAME, + array() + ); + if ( isset( $secrets[ $secret_name ] ) ) { + unset( $secrets[ $secret_name ] ); + \Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); + } + } + + /** + * Deletes all connection tokens and transients from the local Jetpack site. + */ + public function delete_all_connection_tokens() { + \Jetpack_Options::delete_option( + array( + 'blog_token', + 'user_token', + 'user_tokens', + 'master_user', + 'time_diff', + 'fallback_no_verify_ssl_certs', + ) + ); + + \Jetpack_Options::delete_raw_option( 'jetpack_secrets' ); + + // Delete cached connected user data. + $transient_key = 'jetpack_connected_user_data_' . get_current_user_id(); + delete_transient( $transient_key ); + } + + /** + * Tells WordPress.com to disconnect the site and clear all tokens from cached site. + */ + public function disconnect_site_wpcom() { + $xml = new \Jetpack_IXR_Client(); + $xml->query( 'jetpack.deregister', get_current_user_id() ); + } + + /** + * Responds to a WordPress.com call to register the current site. + * Should be changed to protected. + * + * @param array $registration_data Array of [ secret_1, user_id ]. + */ + public function handle_registration( array $registration_data ) { + list( $registration_secret_1, $registration_user_id ) = $registration_data; + if ( empty( $registration_user_id ) ) { + return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 ); + } + + return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id ); + } + + /** + * Verify a Previously Generated Secret. + * + * @param string $action The type of secret to verify. + * @param string $secret_1 The secret string to compare to what is stored. + * @param int $user_id The user ID of the owner of the secret. + * @return \WP_Error|string WP_Error on failure, secret_2 on success. + */ + public function verify_secrets( $action, $secret_1, $user_id ) { + $allowed_actions = array( 'register', 'authorize', 'publicize' ); + if ( ! in_array( $action, $allowed_actions, true ) ) { + return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 ); + } + + $user = get_user_by( 'id', $user_id ); + + /** + * We've begun verifying the previously generated secret. + * + * @since 7.5.0 + * + * @param string $action The type of secret to verify. + * @param \WP_User $user The user object. + */ + do_action( 'jetpack_verify_secrets_begin', $action, $user ); + + $return_error = function( \WP_Error $error ) use ( $action, $user ) { + /** + * Verifying of the previously generated secret has failed. + * + * @since 7.5.0 + * + * @param string $action The type of secret to verify. + * @param \WP_User $user The user object. + * @param \WP_Error $error The error object. + */ + do_action( 'jetpack_verify_secrets_fail', $action, $user, $error ); + + return $error; + }; + + $stored_secrets = $this->get_secrets( $action, $user_id ); + $this->delete_secrets( $action, $user_id ); + + $error = null; + if ( empty( $secret_1 ) ) { + $error = $return_error( + new \WP_Error( + 'verify_secret_1_missing', + /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ + sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ), + 400 + ) + ); + } elseif ( ! is_string( $secret_1 ) ) { + $error = $return_error( + new \WP_Error( + 'verify_secret_1_malformed', + /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ + sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ), + 400 + ) + ); + } elseif ( empty( $user_id ) ) { + // $user_id is passed around during registration as "state". + $error = $return_error( + new \WP_Error( + 'state_missing', + /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ + sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ), + 400 + ) + ); + } elseif ( ! ctype_digit( (string) $user_id ) ) { + $error = $return_error( + new \WP_Error( + 'state_malformed', + /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ + sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ), + 400 + ) + ); + } elseif ( self::SECRETS_MISSING === $stored_secrets ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_missing', + __( 'Verification secrets not found', 'jetpack' ), + 400 + ) + ); + } elseif ( self::SECRETS_EXPIRED === $stored_secrets ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_expired', + __( 'Verification took too long', 'jetpack' ), + 400 + ) + ); + } elseif ( ! $stored_secrets ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_empty', + __( 'Verification secrets are empty', 'jetpack' ), + 400 + ) + ); + } elseif ( is_wp_error( $stored_secrets ) ) { + $stored_secrets->add_data( 400 ); + $error = $return_error( $stored_secrets ); + } elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_incomplete', + __( 'Verification secrets are incomplete', 'jetpack' ), + 400 + ) + ); + } elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_mismatch', + __( 'Secret mismatch', 'jetpack' ), + 400 + ) + ); + } + + // Something went wrong during the checks, returning the error. + if ( ! empty( $error ) ) { + return $error; + } + + /** + * We've succeeded at verifying the previously generated secret. + * + * @since 7.5.0 + * + * @param string $action The type of secret to verify. + * @param \WP_User $user The user object. + */ + do_action( 'jetpack_verify_secrets_success', $action, $user ); + + return $stored_secrets['secret_2']; + } + + /** + * Responds to a WordPress.com call to authorize the current user. + * Should be changed to protected. + */ + public function handle_authorization() { + + } + + /** + * Obtains the auth token. + * + * @param array $data The request data. + * @return object|\WP_Error Returns the auth token on success. + * Returns a \WP_Error on failure. + */ + public function get_token( $data ) { + $roles = new Roles(); + $role = $roles->translate_current_user_to_role(); + + if ( ! $role ) { + return new \WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack' ) ); + } + + $client_secret = $this->get_access_token(); + if ( ! $client_secret ) { + return new \WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack' ) ); + } + + /** + * Filter the URL of the first time the user gets redirected back to your site for connection + * data processing. + * + * @since 8.0.0 + * + * @param string $redirect_url Defaults to the site admin URL. + */ + $processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) ); + + $redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : ''; + + /** + * Filter the URL to redirect the user back to when the authentication process + * is complete. + * + * @since 8.0.0 + * + * @param string $redirect_url Defaults to the site URL. + */ + $redirect = apply_filters( 'jetpack_token_redirect_url', $redirect ); + + $redirect_uri = ( 'calypso' === $data['auth_type'] ) + ? $data['redirect_uri'] + : add_query_arg( + array( + 'action' => 'authorize', + '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), + 'redirect' => $redirect ? rawurlencode( $redirect ) : false, + ), + esc_url( $processing_url ) + ); + + /** + * Filters the token request data. + * + * @since 8.0.0 + * + * @param Array $request_data request data. + */ + $body = apply_filters( + 'jetpack_token_request_body', + array( + 'client_id' => \Jetpack_Options::get_option( 'id' ), + 'client_secret' => $client_secret->secret, + 'grant_type' => 'authorization_code', + 'code' => $data['code'], + 'redirect_uri' => $redirect_uri, + ) + ); + + $args = array( + 'method' => 'POST', + 'body' => $body, + 'headers' => array( + 'Accept' => 'application/json', + ), + ); + + $response = Client::_wp_remote_request( Utils::fix_url_for_bad_hosts( $this->api_url( 'token' ) ), $args ); + + if ( is_wp_error( $response ) ) { + return new \WP_Error( 'token_http_request_failed', $response->get_error_message() ); + } + + $code = wp_remote_retrieve_response_code( $response ); + $entity = wp_remote_retrieve_body( $response ); + + if ( $entity ) { + $json = json_decode( $entity ); + } else { + $json = false; + } + + if ( 200 !== $code || ! empty( $json->error ) ) { + if ( empty( $json->error ) ) { + return new \WP_Error( 'unknown', '', $code ); + } + + $error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : ''; + + return new \WP_Error( (string) $json->error, $error_description, $code ); + } + + if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) { + return new \WP_Error( 'access_token', '', $code ); + } + + if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) { + return new \WP_Error( 'token_type', '', $code ); + } + + if ( empty( $json->scope ) ) { + return new \WP_Error( 'scope', 'No Scope', $code ); + } + + @list( $role, $hmac ) = explode( ':', $json->scope ); + if ( empty( $role ) || empty( $hmac ) ) { + return new \WP_Error( 'scope', 'Malformed Scope', $code ); + } + + if ( $this->sign_role( $role ) !== $json->scope ) { + return new \WP_Error( 'scope', 'Invalid Scope', $code ); + } + + $cap = $roles->translate_role_to_cap( $role ); + if ( ! $cap ) { + return new \WP_Error( 'scope', 'No Cap', $code ); + } + + if ( ! current_user_can( $cap ) ) { + return new \WP_Error( 'scope', 'current_user_cannot', $code ); + } + + /** + * Fires after user has successfully received an auth token. + * + * @since 3.9.0 + */ + do_action( 'jetpack_user_authorized' ); + + return (string) $json->access_token; + } + + /** + * Builds a URL to the Jetpack connection auth page. + * + * @param WP_User $user (optional) defaults to the current logged in user. + * @param String $redirect (optional) a redirect URL to use instead of the default. + * @return string Connect URL. + */ + public function get_authorization_url( $user = null, $redirect = null ) { + + if ( empty( $user ) ) { + $user = wp_get_current_user(); + } + + $roles = new Roles(); + $role = $roles->translate_user_to_role( $user ); + $signed_role = $this->sign_role( $role ); + + /** + * Filter the URL of the first time the user gets redirected back to your site for connection + * data processing. + * + * @since 8.0.0 + * + * @param string $redirect_url Defaults to the site admin URL. + */ + $processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) ); + + /** + * Filter the URL to redirect the user back to when the authorization process + * is complete. + * + * @since 8.0.0 + * + * @param string $redirect_url Defaults to the site URL. + */ + $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); + + $secrets = $this->generate_secrets( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS ); + + /** + * Filter the type of authorization. + * 'calypso' completes authorization on wordpress.com/jetpack/connect + * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com. + * + * @since 4.3.3 + * + * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'. + */ + $auth_type = apply_filters( 'jetpack_auth_type', 'calypso' ); + + /** + * Filters the user connection request data for additional property addition. + * + * @since 8.0.0 + * + * @param Array $request_data request data. + */ + $body = apply_filters( + 'jetpack_connect_request_body', + array( + 'response_type' => 'code', + 'client_id' => \Jetpack_Options::get_option( 'id' ), + 'redirect_uri' => add_query_arg( + array( + 'action' => 'authorize', + '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), + 'redirect' => rawurlencode( $redirect ), + ), + esc_url( $processing_url ) + ), + 'state' => $user->ID, + 'scope' => $signed_role, + 'user_email' => $user->user_email, + 'user_login' => $user->user_login, + 'is_active' => $this->is_active(), + 'jp_version' => Constants::get_constant( 'JETPACK__VERSION' ), + 'auth_type' => $auth_type, + 'secret' => $secrets['secret_1'], + 'blogname' => get_option( 'blogname' ), + 'site_url' => site_url(), + 'home_url' => home_url(), + 'site_icon' => get_site_icon_url(), + 'site_lang' => get_locale(), + 'site_created' => $this->get_assumed_site_creation_date(), + ) + ); + + $body = $this->apply_activation_source_to_args( urlencode_deep( $body ) ); + + $api_url = $this->api_url( 'authorize' ); + + return add_query_arg( $body, $api_url ); + } + + /** + * Authorizes the user by obtaining and storing the user token. + * + * @param array $data The request data. + * @return string|\WP_Error Returns a string on success. + * Returns a \WP_Error on failure. + */ + public function authorize( $data = array() ) { + /** + * Action fired when user authorization starts. + * + * @since 8.0.0 + */ + do_action( 'jetpack_authorize_starting' ); + + $roles = new Roles(); + $role = $roles->translate_current_user_to_role(); + + if ( ! $role ) { + return new \WP_Error( 'no_role', 'Invalid request.', 400 ); + } + + $cap = $roles->translate_role_to_cap( $role ); + if ( ! $cap ) { + return new \WP_Error( 'no_cap', 'Invalid request.', 400 ); + } + + if ( ! empty( $data['error'] ) ) { + return new \WP_Error( $data['error'], 'Error included in the request.', 400 ); + } + + if ( ! isset( $data['state'] ) ) { + return new \WP_Error( 'no_state', 'Request must include state.', 400 ); + } + + if ( ! ctype_digit( $data['state'] ) ) { + return new \WP_Error( $data['error'], 'State must be an integer.', 400 ); + } + + $current_user_id = get_current_user_id(); + if ( $current_user_id !== (int) $data['state'] ) { + return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 ); + } + + if ( empty( $data['code'] ) ) { + return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 ); + } + + $token = $this->get_token( $data ); + + if ( is_wp_error( $token ) ) { + $code = $token->get_error_code(); + if ( empty( $code ) ) { + $code = 'invalid_token'; + } + return new \WP_Error( $code, $token->get_error_message(), 400 ); + } + + if ( ! $token ) { + return new \WP_Error( 'no_token', 'Error generating token.', 400 ); + } + + $is_master_user = ! $this->is_active(); + + Utils::update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_master_user ); + + if ( ! $is_master_user ) { + /** + * Action fired when a secondary user has been authorized. + * + * @since 8.0.0 + */ + do_action( 'jetpack_authorize_ending_linked' ); + return 'linked'; + } + + /** + * Action fired when the master user has been authorized. + * + * @since 8.0.0 + * + * @param array $data The request data. + */ + do_action( 'jetpack_authorize_ending_authorized', $data ); + + return 'authorized'; + } + + /** + * Disconnects from the Jetpack servers. + * Forgets all connection details and tells the Jetpack servers to do the same. + */ + public function disconnect_site() { + + } + + /** + * The Base64 Encoding of the SHA1 Hash of the Input. + * + * @param string $text The string to hash. + * @return string + */ + public function sha1_base64( $text ) { + return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } + + /** + * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase. + * + * @param string $domain The domain to check. + * + * @return bool|WP_Error + */ + public function is_usable_domain( $domain ) { + + // If it's empty, just fail out. + if ( ! $domain ) { + return new \WP_Error( + 'fail_domain_empty', + /* translators: %1$s is a domain name. */ + sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) + ); + } + + /** + * Skips the usuable domain check when connecting a site. + * + * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com + * + * @since 4.1.0 + * + * @param bool If the check should be skipped. Default false. + */ + if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) { + return true; + } + + // None of the explicit localhosts. + $forbidden_domains = array( + 'wordpress.com', + 'localhost', + 'localhost.localdomain', + '127.0.0.1', + 'local.wordpress.test', // VVV pattern. + 'local.wordpress-trunk.test', // VVV pattern. + 'src.wordpress-develop.test', // VVV pattern. + 'build.wordpress-develop.test', // VVV pattern. + ); + if ( in_array( $domain, $forbidden_domains, true ) ) { + return new \WP_Error( + 'fail_domain_forbidden', + sprintf( + /* translators: %1$s is a domain name. */ + __( + 'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', + 'jetpack' + ), + $domain + ) + ); + } + + // No .test or .local domains. + if ( preg_match( '#\.(test|local)$#i', $domain ) ) { + return new \WP_Error( + 'fail_domain_tld', + sprintf( + /* translators: %1$s is a domain name. */ + __( + 'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', + 'jetpack' + ), + $domain + ) + ); + } + + // No WPCOM subdomains. + if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) { + return new \WP_Error( + 'fail_subdomain_wpcom', + sprintf( + /* translators: %1$s is a domain name. */ + __( + 'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', + 'jetpack' + ), + $domain + ) + ); + } + + // If PHP was compiled without support for the Filter module (very edge case). + if ( ! function_exists( 'filter_var' ) ) { + // Just pass back true for now, and let wpcom sort it out. + return true; + } + + return true; + } + + /** + * Gets the requested token. + * + * Tokens are one of two types: + * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token, + * though some sites can have multiple "Special" Blog Tokens (see below). These tokens + * are not associated with a user account. They represent the site's connection with + * the Jetpack servers. + * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token. + * + * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the + * token, and $private is a secret that should never be displayed anywhere or sent + * over the network; it's used only for signing things. + * + * Blog Tokens can be "Normal" or "Special". + * * Normal: The result of a normal connection flow. They look like + * "{$random_string_1}.{$random_string_2}" + * That is, $token_key and $private are both random strings. + * Sites only have one Normal Blog Token. Normal Tokens are found in either + * Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN + * constant (rare). + * * Special: A connection token for sites that have gone through an alternative + * connection flow. They look like: + * ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}" + * That is, $private is a random string and $token_key has a special structure with + * lots of semicolons. + * Most sites have zero Special Blog Tokens. Special tokens are only found in the + * JETPACK_BLOG_TOKEN constant. + * + * In particular, note that Normal Blog Tokens never start with ";" and that + * Special Blog Tokens always do. + * + * When searching for a matching Blog Tokens, Blog Tokens are examined in the following + * order: + * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant) + * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' )) + * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant) + * + * @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token. + * @param string|false $token_key If provided, check that the token matches the provided input. + * @param bool|true $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found. + * + * @return object|false + */ + public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { + $possible_special_tokens = array(); + $possible_normal_tokens = array(); + $user_tokens = \Jetpack_Options::get_option( 'user_tokens' ); + + if ( $user_id ) { + if ( ! $user_tokens ) { + return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' ); + } + if ( self::JETPACK_MASTER_USER === $user_id ) { + $user_id = \Jetpack_Options::get_option( 'master_user' ); + if ( ! $user_id ) { + return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' ); + } + } + if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) { + return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( 'No token for user %d', $user_id ) ); + } + $user_token_chunks = explode( '.', $user_tokens[ $user_id ] ); + if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) { + return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( 'Token for user %d is malformed', $user_id ) ); + } + if ( $user_token_chunks[2] !== (string) $user_id ) { + return $suppress_errors ? false : new \WP_Error( 'user_id_mismatch', sprintf( 'Requesting user_id %d does not match token user_id %d', $user_id, $user_token_chunks[2] ) ); + } + $possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}"; + } else { + $stored_blog_token = \Jetpack_Options::get_option( 'blog_token' ); + if ( $stored_blog_token ) { + $possible_normal_tokens[] = $stored_blog_token; + } + + $defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' ); + + if ( $defined_tokens_string ) { + $defined_tokens = explode( ',', $defined_tokens_string ); + foreach ( $defined_tokens as $defined_token ) { + if ( ';' === $defined_token[0] ) { + $possible_special_tokens[] = $defined_token; + } else { + $possible_normal_tokens[] = $defined_token; + } + } + } + } + + if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { + $possible_tokens = $possible_normal_tokens; + } else { + $possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens ); + } + + if ( ! $possible_tokens ) { + return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' ); + } + + $valid_token = false; + + if ( false === $token_key ) { + // Use first token. + $valid_token = $possible_tokens[0]; + } elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { + // Use first normal token. + $valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check. + } else { + // Use the token matching $token_key or false if none. + // Ensure we check the full key. + $token_check = rtrim( $token_key, '.' ) . '.'; + + foreach ( $possible_tokens as $possible_token ) { + if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) { + $valid_token = $possible_token; + break; + } + } + } + + if ( ! $valid_token ) { + return $suppress_errors ? false : new \WP_Error( 'no_valid_token' ); + } + + return (object) array( + 'secret' => $valid_token, + 'external_user_id' => (int) $user_id, + ); + } + + /** + * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths + * since it is passed by reference to various methods. + * Capture it here so we can verify the signature later. + * + * @param Array $methods an array of available XMLRPC methods. + * @return Array the same array, since this method doesn't add or remove anything. + */ + public function xmlrpc_methods( $methods ) { + $this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA']; + return $methods; + } + + /** + * Resets the raw post data parameter for testing purposes. + */ + public function reset_raw_post_data() { + $this->raw_post_data = null; + } + + /** + * Registering an additional method. + * + * @param Array $methods an array of available XMLRPC methods. + * @return Array the amended array in case the method is added. + */ + public function public_xmlrpc_methods( $methods ) { + if ( array_key_exists( 'wp.getOptions', $methods ) ) { + $methods['wp.getOptions'] = array( $this, 'jetpack_get_options' ); + } + return $methods; + } + + /** + * Handles a getOptions XMLRPC method call. + * + * @param Array $args method call arguments. + * @return an amended XMLRPC server options array. + */ + public function jetpack_get_options( $args ) { + global $wp_xmlrpc_server; + + $wp_xmlrpc_server->escape( $args ); + + $username = $args[1]; + $password = $args[2]; + + $user = $wp_xmlrpc_server->login( $username, $password ); + if ( ! $user ) { + return $wp_xmlrpc_server->error; + } + + $options = array(); + $user_data = $this->get_connected_user_data(); + if ( is_array( $user_data ) ) { + $options['jetpack_user_id'] = array( + 'desc' => __( 'The WP.com user ID of the connected user', 'jetpack' ), + 'readonly' => true, + 'value' => $user_data['ID'], + ); + $options['jetpack_user_login'] = array( + 'desc' => __( 'The WP.com username of the connected user', 'jetpack' ), + 'readonly' => true, + 'value' => $user_data['login'], + ); + $options['jetpack_user_email'] = array( + 'desc' => __( 'The WP.com user email of the connected user', 'jetpack' ), + 'readonly' => true, + 'value' => $user_data['email'], + ); + $options['jetpack_user_site_count'] = array( + 'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack' ), + 'readonly' => true, + 'value' => $user_data['site_count'], + ); + } + $wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options ); + $args = stripslashes_deep( $args ); + return $wp_xmlrpc_server->wp_getOptions( $args ); + } + + /** + * Adds Jetpack-specific options to the output of the XMLRPC options method. + * + * @param Array $options standard Core options. + * @return Array amended options. + */ + public function xmlrpc_options( $options ) { + $jetpack_client_id = false; + if ( $this->is_active() ) { + $jetpack_client_id = \Jetpack_Options::get_option( 'id' ); + } + $options['jetpack_version'] = array( + 'desc' => __( 'Jetpack Plugin Version', 'jetpack' ), + 'readonly' => true, + 'value' => Constants::get_constant( 'JETPACK__VERSION' ), + ); + + $options['jetpack_client_id'] = array( + 'desc' => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack' ), + 'readonly' => true, + 'value' => $jetpack_client_id, + ); + return $options; + } + + /** + * Resets the saved authentication state in between testing requests. + */ + public function reset_saved_auth_state() { + $this->xmlrpc_verification = null; + } + + /** + * Sign a user role with the master access token. + * If not specified, will default to the current user. + * + * @access public + * + * @param string $role User role. + * @param int $user_id ID of the user. + * @return string Signed user role. + */ + public function sign_role( $role, $user_id = null ) { + if ( empty( $user_id ) ) { + $user_id = (int) get_current_user_id(); + } + + if ( ! $user_id ) { + return false; + } + + $token = $this->get_access_token(); + if ( ! $token || is_wp_error( $token ) ) { + return false; + } + + return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php new file mode 100644 index 00000000..2231193b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php @@ -0,0 +1,54 @@ +<?php +/** + * Sets up the Connection REST API endpoints. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +/** + * Registers the REST routes for Connections. + */ +class REST_Connector { + /** + * The Connection Manager. + * + * @var Manager + */ + private $connection; + + /** + * Constructor. + * + * @param Manager $connection The Connection Manager. + */ + public function __construct( Manager $connection ) { + $this->connection = $connection; + + // Register a site. + register_rest_route( + 'jetpack/v4', + '/verify_registration', + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'verify_registration' ), + ) + ); + } + + /** + * Handles verification that a site is registered. + * + * @since 5.4.0 + * + * @param \WP_REST_Request $request The request sent to the WP REST API. + * + * @return string|WP_Error + */ + public function verify_registration( \WP_REST_Request $request ) { + $registration_data = array( $request['secret_1'], $request['state'] ); + + return $this->connection->handle_registration( $registration_data ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php new file mode 100644 index 00000000..1c280262 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php @@ -0,0 +1,62 @@ +<?php +/** + * The Jetpack Connection package Utils class file. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +use Automattic\Jetpack\Constants; + +/** + * Provides utility methods for the Connection package. + */ +class Utils { + + /** + * Some hosts disable the OpenSSL extension and so cannot make outgoing HTTPS requests. + * This method sets the URL scheme to HTTP when HTTPS requests can't be made. + * + * @param string $url The url. + * @return string The url with the required URL scheme. + */ + public static function fix_url_for_bad_hosts( $url ) { + // If we receive an http url, return it. + if ( 'http' === wp_parse_url( $url, PHP_URL_SCHEME ) ) { + return $url; + } + + // If the url should never be https, ensure it isn't https. + if ( 'NEVER' === Constants::get_constant( 'JETPACK_CLIENT__HTTPS' ) ) { + return set_url_scheme( $url, 'http' ); + } + + // Otherwise, return the https url. + return $url; + } + + /** + * Enters a user token into the user_tokens option + * + * @param int $user_id The user id. + * @param string $token The user token. + * @param bool $is_master_user Whether the user is the master user. + * @return bool + */ + public static function update_user_token( $user_id, $token, $is_master_user ) { + // Not designed for concurrent updates. + $user_tokens = \Jetpack_Options::get_option( 'user_tokens' ); + if ( ! is_array( $user_tokens ) ) { + $user_tokens = array(); + } + $user_tokens[ $user_id ] = $token; + if ( $is_master_user ) { + $master_user = $user_id; + $options = compact( 'user_tokens', 'master_user' ); + } else { + $options = compact( 'user_tokens' ); + } + return \Jetpack_Options::update_options( $options ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php new file mode 100644 index 00000000..813f5e95 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php @@ -0,0 +1,80 @@ +<?php +/** + * Sets up the Connection XML-RPC methods. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +/** + * Registers the XML-RPC methods for Connections. + */ +class XMLRPC_Connector { + /** + * The Connection Manager. + * + * @var Manager + */ + private $connection; + + /** + * Constructor. + * + * @param Manager $connection The Connection Manager. + */ + public function __construct( Manager $connection ) { + $this->connection = $connection; + + // Adding the filter late to avoid being overwritten by Jetpack's XMLRPC server. + add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ), 20 ); + } + + /** + * Attached to the `xmlrpc_methods` filter. + * + * @param array $methods The already registered XML-RPC methods. + * @return array + */ + public function xmlrpc_methods( $methods ) { + return array_merge( + $methods, + array( + 'jetpack.verifyRegistration' => array( $this, 'verify_registration' ), + ) + ); + } + + /** + * Handles verification that a site is registered. + * + * @param array $registration_data The data sent by the XML-RPC client: + * [ $secret_1, $user_id ]. + * + * @return string|IXR_Error + */ + public function verify_registration( $registration_data ) { + return $this->output( $this->connection->handle_registration( $registration_data ) ); + } + + /** + * Normalizes output for XML-RPC. + * + * @param mixed $data The data to output. + */ + private function output( $data ) { + if ( is_wp_error( $data ) ) { + $code = $data->get_error_data(); + if ( ! $code ) { + $code = -10520; + } + + return new \IXR_Error( + $code, + sprintf( 'Jetpack: [%s] %s', $data->get_error_code(), $data->get_error_message() ) + ); + } + + return $data; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php new file mode 100644 index 00000000..176c8523 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php @@ -0,0 +1,17 @@ +<?php +/** + * The Jetpack Connection Interface file. + * No longer used. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +/** + * This interface is no longer used and is now deprecated. + * + * @deprecated since 7.8 + */ +interface Manager_Interface { +} |