ck' ), 400 ) ); } /** * 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() { } /** * Builds a URL to the Jetpack connection auth page. * This needs rethinking. * * @param bool $raw If true, URL will not be escaped. * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection. * If string, will be a custom redirect. * @param bool|string $from If not false, adds 'from=$from' param to the connect URL. * @param bool $register If true, will generate a register URL regardless of the existing token, since 4.9.0. * * @return string Connect URL */ public function build_connect_url( $raw, $redirect, $from, $register ) { return array( $raw, $redirect, $from, $register ); } /** * 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 ); } }