summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php')
-rw-r--r--plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php2169
1 files changed, 2169 insertions, 0 deletions
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 );
+ }
+}