sible. * * @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things. * * @return never */ public function alternate_xmlrpc() { // Some browser-embedded clients send cookies. We don't want them. $_COOKIE = array(); 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 1.7.0 * @since-jetpack 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 ( str_starts_with( $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_connected() ) { // 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable 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 ( $this->xmlrpc_verification === null ) { $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. * * @since 1.7.0 * @since-jetpack 7.5.0 * * @param WP_Error $signature_verification_error The verification error */ do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification ); Error_Handler::get_instance()->report_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, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // 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' => isset( $_SERVER['REQUEST_METHOD'] ) ? wp_unslash( $_SERVER['REQUEST_METHOD'] ) : null, 'url' => wp_unslash( ( isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : null ) . ( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : null ) ), // Temp - will get real signature URL later. 'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '', ); $error_type = 'xmlrpc'; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $jetpack_api_version = Constants::get_constant( 'JETPACK__API_VERSION' ); if ( empty( $token_key ) || empty( $version ) || (string) $jetpack_api_version !== $version ) { return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details', 'error_type' ) ); } 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', 'error_type' ) ); } $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', 'error_type' ) ); } } $token = $this->get_tokens()->get_access_token( $user_id, $token_key, false ); if ( is_wp_error( $token ) ) { $token->add_data( compact( 'signature_details', 'error_type' ) ); 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', 'error_type' ) ); } $jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- Used to verify a cryptographic signature of the post data. Also a nonce is verified later in the function. if ( isset( $_POST['_jetpack_is_multipart'] ) ) { $post_data = $_POST; // We need all of $_POST in order to verify a cryptographic signature of the post data. $file_hashes = array(); foreach ( $post_data as $post_data_key => $post_data_value ) { if ( ! str_starts_with( $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 ( $this->raw_post_data === null ) { $body = file_get_contents( 'php://input' ); } else { $body = null; } // phpcs:enable $signature = $jetpack_signature->sign_current_request( array( 'body' => $body === null ? $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', 'error_type' ) ); } elseif ( is_wp_error( $signature ) ) { return $signature; } // phpcs:disable WordPress.Security.NonceVerification.Recommended $timestamp = (int) $_GET['timestamp']; $nonce = wp_unslash( (string) $_GET['nonce'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP Core doesn't sanitize nonces either. // phpcs:enable WordPress.Security.NonceVerification.Recommended // Use up the nonce regardless of whether the signature matches. if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) { return new \WP_Error( 'invalid_nonce', 'Could not add nonce', compact( 'signature_details', 'error_type' ) ); } // 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, wp_unslash( $_GET['signature'] ) ) ) { return new \WP_Error( 'signature_mismatch', 'Signature mismatch', compact( 'signature_details', 'error_type' ) ); } /** * Action for additional token checking. * * @since 1.7.0 * @since-jetpack 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 and has the minimum requirements to enable Jetpack UI. * * This method is deprecated since version 1.25.0 of this package. Please use has_connected_owner instead. * * Since this method has a wide spread use, we decided not to throw any deprecation warnings for now. * * @deprecated 1.25.0 * @see Manager::has_connected_owner * @return Boolean is the site connected? */ public function is_active() { return (bool) $this->get_tokens()->get_access_token( true ); } /** * Obtains an instance of the Tokens class. * * @return Tokens the Tokens object */ public function get_tokens() { return new Tokens(); } /** * Returns true if the site has both a token and a blog id, which indicates a site has been registered. * * @access public * @deprecated 1.12.1 Use is_connected instead * @see Manager::is_connected * * @return bool */ public function is_registered() { _deprecated_function( __METHOD__, '1.12.1' ); return $this->is_connected(); } /** * Returns true if the site has both a token and a blog id, which indicates a site has been connected. * * @access public * @since 1.21.1 * * @return bool */ public function is_connected() { $has_blog_id = (bool) \Jetpack_Options::get_option( 'id' ); $has_blog_token = (bool) $this->get_tokens()->get_access_token(); return $has_blog_id && $has_blog_token; } /** * Returns true if the site has at least one connected administrator. * * @access public * @since 1.21.1 * * @return bool */ public function has_connected_admin() { return (bool) count( $this->get_connected_users( 'manage_options' ) ); } /** * Returns true if the site has any connected user. * * @access public * @since 1.21.1 * * @return bool */ public function has_connected_user() { return (bool) count( $this->get_connected_users( 'any', 1 ) ); } /** * Returns an array of users that have user tokens for communicating with wpcom. * Able to select by specific capability. * * @since 9.9.1 Added $limit parameter. * * @param string $capability The capability of the user. * @param int|null $limit How many connected users to get before returning. * @return WP_User[] Array of WP_User objects if found. */ public function get_connected_users( $capability = 'any', $limit = null ) { $connected_users = array(); $user_tokens = $this->get_tokens()->get_user_tokens(); if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) { return $connected_users; } $connected_user_ids = array_keys( $user_tokens ); if ( ! empty( $connected_user_ids ) ) { foreach ( $connected_user_ids as $id ) { // Check for capability. if ( 'any' !== $capability && ! user_can( $id, $capability ) ) { continue; } $user_data = get_userdata( $id ); if ( $user_data instanceof \WP_User ) { $connected_users[] = $user_data; if ( $limit && count( $connected_users ) >= $limit ) { return $connected_users; } } } } return $connected_users; } /** * Returns true if the site has a connected Blog owner (master_user). * * @access public * @since 1.21.1 * * @return bool */ public function has_connected_owner() { return (bool) $this->get_connection_owner_id(); } /** * Returns true if the site is connected only at a site level. * * Note that we are explicitly checking for the existence of the master_user option in order to account for cases where we don't have any user tokens (user-level connection) but the master_user option is set, which could be the result of a problematic user connection. * * @access public * @since 1.25.0 * @deprecated 1.27.0 * * @return bool */ public function is_userless() { _deprecated_function( __METHOD__, '1.27.0', 'Automattic\\Jetpack\\Connection\\Manager::is_site_connection' ); return $this->is_site_connection(); } /** * Returns true if the site is connected only at a site level. * * Note that we are explicitly checking for the existence of the master_user option in order to account for cases where we don't have any user tokens (user-level connection) but the master_user option is set, which could be the result of a problematic user connection. * * @access public * @since 1.27.0 * * @return bool */ public function is_site_connection() { return $this->is_connected() && ! $this->has_connected_user() && ! \Jetpack_Options::get_option( 'master_user' ); } /** * 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 int $user_id the user identifier. Default is the current user. * @return bool 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_tokens()->get_access_token( $user_id ); } /** * Returns the local user ID of the connection owner. * * @return bool|int Returns the ID of the connection owner or False if no connection owner found. */ public function get_connection_owner_id() { $owner = $this->get_connection_owner(); return $owner instanceof \WP_User ? $owner->ID : false; } /** * 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 bool|array An array with the WPCOM user data on success, false otherwise. */ public function get_connected_user_data( $user_id = null ) { if ( ! $user_id ) { $user_id = get_current_user_id(); } // Check if the user is connected and return false otherwise. if ( ! $this->is_user_connected( $user_id ) ) { return false; } $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 WP_User|false False if no connection owner found. */ public function get_connection_owner() { $user_id = \Jetpack_Options::get_option( 'master_user' ); if ( ! $user_id ) { return false; } // Make sure user is connected. $user_token = $this->get_tokens()->get_access_token( $user_id ); $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 ); } if ( $connection_owner === false ) { Error_Handler::get_instance()->report_error( new WP_Error( 'invalid_connection_owner', 'Invalid connection owner', array( 'user_id' => $user_id, 'has_user_token' => (bool) $user_token, 'error_type' => 'connection', 'signature_details' => array( 'token' => '', ), ) ), false, true ); } 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(); } return ( (int) $user_id ) === $this->get_connection_owner_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, $redirect_url ) ); // phpcs:ignore WordPress.Security.SafeRedirect exit(); } /** * Force user disconnect. * * @param int $user_id Local (external) user ID. * * @return bool */ public function disconnect_user_force( $user_id ) { if ( ! (int) $user_id ) { // Missing user ID. return false; } return $this->disconnect_user( $user_id, true, true ); } /** * 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. * @param bool $can_overwrite_primary_user Allow for the primary user to be disconnected. * @param bool $force_disconnect_locally Disconnect user locally even if we were unable to disconnect them from WP.com. * @return Boolean Whether the disconnection of the user was successful. */ public function disconnect_user( $user_id = null, $can_overwrite_primary_user = false, $force_disconnect_locally = false ) { $user_id = empty( $user_id ) ? get_current_user_id() : (int) $user_id; $is_primary_user = Jetpack_Options::get_option( 'master_user' ) === $user_id; if ( $is_primary_user && ! $can_overwrite_primary_user ) { return false; } if ( in_array( $user_id, self::$disconnected_users, true ) ) { // The user is already disconnected. return false; } // Attempt to disconnect the user from WordPress.com. $is_disconnected_from_wpcom = $this->unlink_user_from_wpcom( $user_id ); $is_disconnected_locally = false; if ( $is_disconnected_from_wpcom || $force_disconnect_locally ) { // Disconnect the user locally. $is_disconnected_locally = $this->get_tokens()->disconnect_user( $user_id ); if ( $is_disconnected_locally ) { // Delete cached connected user data. $transient_key = "jetpack_connected_user_data_$user_id"; delete_transient( $transient_key ); /** * Fires after the current user has been unlinked from WordPress.com. * * @since 1.7.0 * @since-jetpack 4.1.0 * * @param int $user_id The current user's ID. */ do_action( 'jetpack_unlinked_user', $user_id ); if ( $is_primary_user ) { Jetpack_Options::delete_option( 'master_user' ); } } } self::$disconnected_users[] = $user_id; return $is_disconnected_from_wpcom && $is_disconnected_locally; } /** * Request to wpcom for a user to be unlinked from their WordPress.com account * * @param int $user_id The user identifier. * * @return bool Whether the disconnection of the user was successful. */ public function unlink_user_from_wpcom( $user_id ) { // Attempt to disconnect the user from WordPress.com. $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.unlink_user', $user_id ); if ( $xml->isError() ) { return false; } return (bool) $xml->getResponse(); } /** * Update the connection owner. * * @since 1.29.0 * * @param Integer $new_owner_id The ID of the user to become the connection owner. * * @return true|WP_Error True if owner successfully changed, WP_Error otherwise. */ public function update_connection_owner( $new_owner_id ) { $roles = new Roles(); if ( ! user_can( $new_owner_id, $roles->translate_role_to_cap( 'administrator' ) ) ) { return new WP_Error( 'new_owner_not_admin', __( 'New owner is not admin', 'jetpack-connection' ), array( 'status' => 400 ) ); } $old_owner_id = $this->get_connection_owner_id(); if ( $old_owner_id === $new_owner_id ) { return new WP_Error( 'new_owner_is_existing_owner', __( 'New owner is same as existing owner', 'jetpack-connection' ), array( 'status' => 400 ) ); } if ( ! $this->is_user_connected( $new_owner_id ) ) { return new WP_Error( 'new_owner_not_connected', __( 'New owner is not connected', 'jetpack-connection' ), array( 'status' => 400 ) ); } // Notify WPCOM about the connection owner change. $owner_updated_wpcom = $this->update_connection_owner_wpcom( $new_owner_id ); if ( $owner_updated_wpcom ) { // Update the connection owner in Jetpack only if they were successfully updated on WPCOM. // This will ensure consistency with WPCOM. \Jetpack_Options::update_option( 'master_user', $new_owner_id ); // Track it. ( new Tracking() )->record_user_event( 'set_connection_owner_success' ); return true; } return new WP_Error( 'error_setting_new_owner', __( 'Could not confirm new owner.', 'jetpack-connection' ), array( 'status' => 500 ) ); } /** * Request to WPCOM to update the connection owner. * * @since 1.29.0 * * @param Integer $new_owner_id The ID of the user to become the connection owner. * * @return Boolean Whether the ownership transfer was successful. */ public function update_connection_owner_wpcom( $new_owner_id ) { // Notify WPCOM about the connection owner change. $xml = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id(), ) ); $xml->query( 'jetpack.switchBlogOwner', array( 'new_blog_owner' => $new_owner_id, ) ); if ( $xml->isError() ) { return false; } return (bool) $xml->getResponse(); } /** * 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' ); $api_version = '/' . Constants::get_constant( 'JETPACK__API_VERSION' ) . '/'; /** * Filters the API URL that Jetpack uses for server communication. * * @since 1.7.0 * @since-jetpack 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 $api_version the API version string that is being used. */ return apply_filters( 'jetpack_api_url', rtrim( $api_base . $relative_url, '/\\' ) . $api_version, $relative_url, $api_base, $api_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 true|WP_Error The error object. */ public function register( $api_endpoint = 'register' ) { // Clean-up leftover tokens just in-case. // This fixes an edge case that was preventing users to register when the blog token was missing but // there were still leftover user tokens present. $this->delete_all_connection_tokens( true ); add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) ); $secrets = ( new Secrets() )->generate( 'register', get_current_user_id(), 600 ); if ( false === $secrets ) { return new WP_Error( 'cannot_save_secrets', __( 'Jetpack experienced an issue trying to save options (cannot_save_secrets). We suggest that you contact your hosting provider, and ask them for help checking that the options table is writable on your site.', 'jetpack-connection' ) ); } 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; /* This action is documented in src/class-package-version-tracker.php */ $package_versions = apply_filters( 'jetpack_package_versions', array() ); $active_plugins_using_connection = Plugin_Storage::get_all(); /** * Filters the request body for additional property addition. * * @since 1.7.0 * @since-jetpack 7.7.0 * * @param array $post_data request data. * @param Array $token_data token data. */ $body = apply_filters( 'jetpack_register_request_body', array_merge( array( 'siteurl' => Urls::site_url(), 'home' => Urls::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' ), 'ABSPATH' => Constants::get_constant( 'ABSPATH' ), 'current_user_email' => wp_get_current_user()->user_email, 'connect_plugin' => $this->get_plugin() ? $this->get_plugin()->get_slug() : null, 'package_versions' => $package_versions, 'active_connected_plugins' => $active_plugins_using_connection, ), self::$extra_register_params ) ); $args = array( 'method' => 'POST', 'body' => $body, 'headers' => array( 'Accept' => 'application/json', ), 'timeout' => $timeout, ); $args['body'] = static::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, 'public' => $jetpack_public, ) ); update_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION, $package_versions ); $this->get_tokens()->update_blog_token( (string) $registration_details->jetpack_secret ); if ( ! Jetpack_Options::get_option( 'id' ) || ! $this->get_tokens()->get_access_token() ) { return new WP_Error( 'connection_data_save_failed', 'Failed to save connection data in the database' ); } $alternate_authorization_url = isset( $registration_details->alternate_authorization_url ) ? $registration_details->alternate_authorization_url : ''; add_filter( 'jetpack_register_site_rest_response', function ( $response ) use ( $alternate_authorization_url ) { $response['alternateAuthorizeUrl'] = $alternate_authorization_url; return $response; } ); /** * Fires when a site is registered on WordPress.com. * * @since 1.7.0 * @since-jetpack 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 1.7.0 * @since-jetpack 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; } /** * Attempts Jetpack registration. * * @param bool $tos_agree Whether the user agreed to TOS. * * @return bool|WP_Error */ public function try_registration( $tos_agree = true ) { if ( $tos_agree ) { $terms_of_service = new Terms_Of_Service(); $terms_of_service->agree(); } /** * Action fired when the user attempts the registration. * * @since 1.26.0 */ $pre_register = apply_filters( 'jetpack_pre_register', null ); if ( is_wp_error( $pre_register ) ) { return $pre_register; } $tracking_data = array(); if ( null !== $this->get_plugin() ) { $tracking_data['plugin_slug'] = $this->get_plugin()->get_slug(); } $tracking = new Tracking(); $tracking->record_user_event( 'jpc_register_begin', $tracking_data ); add_filter( 'jetpack_register_request_body', array( Utils::class, 'filter_register_request_body' ) ); $result = $this->register(); remove_filter( 'jetpack_register_request_body', array( Utils::class, 'filter_register_request_body' ) ); // If there was an error with registration and the site was not registered, record this so we can show a message. if ( ! $result || is_wp_error( $result ) ) { return $result; } return true; } /** * Adds a parameter to the register request body * * @since 1.26.0 * * @param string $name The name of the parameter to be added. * @param string $value The value of the parameter to be added. * * @throws \InvalidArgumentException If supplied arguments are not strings. * @return void */ public function add_register_request_param( $name, $value ) { if ( ! is_string( $name ) || ! is_string( $value ) ) { throw new \InvalidArgumentException( 'name and value must be strings' ); } self::$extra_register_params[ $name ] = $value; } /** * Takes the response from the Jetpack register new site endpoint and * verifies it worked properly. * * @since 1.7.0 * @since-jetpack 2.6.0 * * @param Mixed $response the response object, or the error object. * @return string|WP_Error A JSON object on success or WP_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 = (int) ( $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-connection' ); } 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-connection' ), $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-connection' ), $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-connection' ), $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. * * @deprecated since 1.24.0 * @see Nonce_Handler::add() */ public function add_nonce( $timestamp, $nonce ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Nonce_Handler::add' ); return ( new Nonce_Handler() )->add( $timestamp, $nonce ); } /** * 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. * * @deprecated since 1.24.0 * @see Nonce_Handler::clean_all() */ public function clean_nonces( $all = false ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Nonce_Handler::clean_all' ); ( new Nonce_Handler() )->clean_all( $all ? PHP_INT_MAX : ( time() - Nonce_Handler::LIFETIME ) ); } /** * Sets the Connection custom capabilities. * * @param string[] $caps Array of the user's capabilities. * @param string $cap Capability name. * @param int $user_id The user ID. * @param array $args Adds the context to the cap. Typically the object ID. */ public function jetpack_connection_custom_caps( $caps, $cap, $user_id, $args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable switch ( $cap ) { case 'jetpack_connect': case 'jetpack_reconnect': $is_offline_mode = ( new Status() )->is_offline_mode(); if ( $is_offline_mode ) { $caps = array( 'do_not_allow' ); break; } // Pass through. If it's not offline mode, these should match disconnect. // Let users disconnect if it's offline mode, just in case things glitch. case 'jetpack_disconnect': /** * Filters the jetpack_disconnect capability. * * @since 1.14.2 * * @param array An array containing the capability name. */ $caps = apply_filters( 'jetpack_disconnect_cap', array( 'manage_options' ) ); break; case 'jetpack_connect_user': $is_offline_mode = ( new Status() )->is_offline_mode(); if ( $is_offline_mode ) { $caps = array( 'do_not_allow' ); break; } // With site connections in mind, non-admin users can connect their account only if a connection owner exists. $caps = $this->has_connected_owner() ? array( 'read' ) : array( 'manage_options' ); break; } return $caps; } /** * Builds the timeout limit for queries talking with the wpcom servers. * * Based on local php max_execution_time in php.ini * * @since 1.7.0 * @since-jetpack 5.4.0 * @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 1.7.0 * @since-jetpack 5.4.0 * @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 1.7.0 * @since-jetpack 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; } /** * 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 ) { return ( new Secrets() )->generate( $action, $user_id, $exp ); } /** * Returns two secret tokens and the end of life timestamp for them. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->get() instead. * * @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 ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->get' ); return ( new Secrets() )->get( $action, $user_id ); } /** * Deletes secret tokens in case they, for example, have expired. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->delete() instead. * * @param String $action The action name. * @param Integer $user_id The user identifier. */ public function delete_secrets( $action, $user_id ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->delete' ); ( new Secrets() )->delete( $action, $user_id ); } /** * Deletes all connection tokens and transients from the local Jetpack site. * If the plugin object has been provided in the constructor, the function first checks * whether it's the only active connection. * If there are any other connections, the function will do nothing and return `false` * (unless `$ignore_connected_plugins` is set to `true`). * * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * * @return bool True if disconnected successfully, false otherwise. */ public function delete_all_connection_tokens( $ignore_connected_plugins = false ) { // refuse to delete if we're not the last Jetpack plugin installed. if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 1.14.2 */ if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true ) ) { return false; } \Jetpack_Options::delete_option( array( 'master_user', 'time_diff', 'fallback_no_verify_ssl_certs', ) ); ( new Secrets() )->delete_all(); $this->get_tokens()->delete_all(); // Delete cached connected user data. $transient_key = 'jetpack_connected_user_data_' . get_current_user_id(); delete_transient( $transient_key ); // Delete all XML-RPC errors. Error_Handler::get_instance()->delete_all_errors(); return true; } /** * Tells WordPress.com to disconnect the site and clear all tokens from cached site. * If the plugin object has been provided in the constructor, the function first check * whether it's the only active connection. * If there are any other connections, the function will do nothing and return `false` * (unless `$ignore_connected_plugins` is set to `true`). * * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * * @return bool True if disconnected successfully, false otherwise. */ public function disconnect_site_wpcom( $ignore_connected_plugins = false ) { if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } if ( ( new Status() )->is_offline_mode() && ! apply_filters( 'jetpack_connection_disconnect_site_wpcom_offline_mode', false ) ) { // Prevent potential disconnect of the live site by removing WPCOM tokens. return false; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 1.14.2 */ if ( ! apply_filters( 'jetpack_connection_disconnect_site_wpcom', true, $this ) ) { return false; } $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.deregister', get_current_user_id() ); return true; } /** * Disconnect the plugin and remove the tokens. * This function will automatically perform "soft" or "hard" disconnect depending on whether other plugins are using the connection. * This is a proxy method to simplify the Connection package API. * * @see Manager::disconnect_site() * * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called. * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * @return bool */ public function remove_connection( $disconnect_wpcom = true, $ignore_connected_plugins = false ) { $this->disconnect_site( $disconnect_wpcom, $ignore_connected_plugins ); return true; } /** * Completely clearing up the connection, and initiating reconnect. * * @return true|WP_Error True if reconnected successfully, a `WP_Error` object otherwise. */ public function reconnect() { ( new Tracking() )->record_user_event( 'restore_connection_reconnect' ); $this->disconnect_site_wpcom( true ); return $this->register(); } /** * Validate the tokens, and refresh the invalid ones. * * @return string|bool|WP_Error True if connection restored or string indicating what's to be done next. A `WP_Error` object or false otherwise. */ public function restore() { // If this is a site connection we need to trigger a full reconnection as our only secure means of // communication with WPCOM, aka the blog token, is compromised. if ( $this->is_site_connection() ) { return $this->reconnect(); } $validate_tokens_response = $this->get_tokens()->validate(); // If token validation failed, trigger a full reconnection. if ( is_array( $validate_tokens_response ) && isset( $validate_tokens_response['blog_token']['is_healthy'] ) && isset( $validate_tokens_response['user_token']['is_healthy'] ) ) { $blog_token_healthy = $validate_tokens_response['blog_token']['is_healthy']; $user_token_healthy = $validate_tokens_response['user_token']['is_healthy']; } else { $blog_token_healthy = false; $user_token_healthy = false; } // Tokens are both valid, or both invalid. We can't fix the problem we don't see, so the full reconnection is needed. if ( $blog_token_healthy === $user_token_healthy ) { $result = $this->reconnect(); return ( true === $result ) ? 'authorize' : $result; } if ( ! $blog_token_healthy ) { return $this->refresh_blog_token(); } if ( ! $user_token_healthy ) { return ( true === $this->refresh_user_token() ) ? 'authorize' : false; } return false; } /** * 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-connection' ), 400 ); } return ( new Secrets() )->verify( 'register', $registration_secret_1, (int) $registration_user_id ); } /** * Perform the API request to validate the blog and user tokens. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->validate_tokens() instead. * * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default. * * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`. */ public function validate_tokens( $user_id = null ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->validate' ); return $this->get_tokens()->validate( $user_id ); } /** * Verify a Previously Generated Secret. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->verify() instead. * * @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 ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->verify' ); return ( new Secrets() )->verify( $action, $secret_1, $user_id ); } /** * 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 ) { return $this->get_tokens()->get( $data, $this->api_url( 'token' ) ); } /** * Builds a URL to the Jetpack connection auth page. * * @since 2.7.6 Added optional $from and $raw parameters. * * @param WP_User|null $user (optional) defaults to the current logged in user. * @param string|null $redirect (optional) a redirect URL to use instead of the default. * @param bool|string $from If not false, adds 'from=$from' param to the connect URL. * @param bool $raw If true, URL will not be escaped. * * @return string Connect URL. */ public function get_authorization_url( $user = null, $redirect = null, $from = false, $raw = false ) { if ( empty( $user ) ) { $user = wp_get_current_user(); } $roles = new Roles(); $role = $roles->translate_user_to_role( $user ); $signed_role = $this->get_tokens()->sign_role( $role ); /** * Filter the URL of the first time the user gets redirected back to your site for connection * data processing. * * @since 1.7.0 * @since-jetpack 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 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site URL. */ $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); $secrets = ( new Secrets() )->generate( '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 1.7.0 * @since-jetpack 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 1.7.0 * @since-jetpack 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( 'handler' => 'jetpack-connection-webhooks', 'action' => 'authorize', '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), 'redirect' => $redirect ? rawurlencode( $redirect ) : false, ), esc_url( $processing_url ) ), 'state' => $user->ID, 'scope' => $signed_role, 'user_email' => $user->user_email, 'user_login' => $user->user_login, 'is_active' => $this->has_connected_owner(), // TODO Deprecate this. 'jp_version' => (string) Constants::get_constant( 'JETPACK__VERSION' ), 'auth_type' => $auth_type, 'secret' => $secrets['secret_1'], 'blogname' => get_option( 'blogname' ), 'site_url' => Urls::site_url(), 'home_url' => Urls::home_url(), 'site_icon' => get_site_icon_url(), 'site_lang' => get_locale(), 'site_created' => $this->get_assumed_site_creation_date(), 'allow_site_connection' => ! $this->has_connected_owner(), 'calypso_env' => ( new Host() )->get_calypso_env(), 'source' => ( new Host() )->get_source_query(), ) ); $body = static::apply_activation_source_to_args( urlencode_deep( $body ) ); $api_url = $this->api_url( 'authorize' ); $url = add_query_arg( $body, $api_url ); if ( is_network_admin() ) { $url = add_query_arg( 'is_multisite', network_admin_url( 'admin.php?page=jetpack-settings' ), $url ); } if ( $from ) { $url = add_query_arg( 'from', $from, $url ); } if ( $raw ) { $url = esc_url_raw( $url ); } /** * Filter the URL used when connecting a user to a WordPress.com account. * * @since 2.0.0 * @since 2.7.6 Added $raw parameter. * * @param string $url Connection URL. * @param bool $raw If true, URL will not be escaped. */ return apply_filters( 'jetpack_build_authorize_url', $url, $raw ); } /** * 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 1.7.0 * @since-jetpack 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_tokens()->get( $data, $this->api_url( 'token' ) ); 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_connection_owner = ! $this->has_connected_owner(); $this->get_tokens()->update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_connection_owner ); /** * Fires after user has successfully received an auth token. * * @since 1.7.0 * @since-jetpack 3.9.0 */ do_action( 'jetpack_user_authorized' ); if ( ! $is_connection_owner ) { /** * Action fired when a secondary user has been authorized. * * @since 1.7.0 * @since-jetpack 8.0.0 */ do_action( 'jetpack_authorize_ending_linked' ); return 'linked'; } /** * Action fired when the master user has been authorized. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param array $data The request data. */ do_action( 'jetpack_authorize_ending_authorized', $data ); \Jetpack_Options::delete_raw_option( 'jetpack_last_connect_url_check' ); ( new Nonce_Handler() )->reschedule(); return 'authorized'; } /** * Disconnects from the Jetpack servers. * Forgets all connection details and tells the Jetpack servers to do the same. * * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called. * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. */ public function disconnect_site( $disconnect_wpcom = true, $ignore_connected_plugins = true ) { if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } wp_clear_scheduled_hook( 'jetpack_clean_nonces' ); ( new Nonce_Handler() )->clean_all(); /** * Fires when a site is disconnected. * * @since 1.36.3 */ do_action( 'jetpack_site_before_disconnected' ); // If the site is in an IDC because sync is not allowed, // let's make sure to not disconnect the production site. if ( $disconnect_wpcom ) { $tracking = new Tracking(); $tracking->record_user_event( 'disconnect_site', array() ); $this->disconnect_site_wpcom( $ignore_connected_plugins ); } $this->delete_all_connection_tokens( $ignore_connected_plugins ); // Remove tracked package versions, since they depend on the Jetpack Connection. delete_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION ); $jetpack_unique_connection = \Jetpack_Options::get_option( 'unique_connection' ); if ( $jetpack_unique_connection ) { // Check then record unique disconnection if site has never been disconnected previously. if ( - 1 === $jetpack_unique_connection['disconnected'] ) { $jetpack_unique_connection['disconnected'] = 1; } else { if ( 0 === $jetpack_unique_connection['disconnected'] ) { $a8c_mc_stats_instance = new A8c_Mc_Stats(); $a8c_mc_stats_instance->add( 'connections', 'unique-disconnect' ); $a8c_mc_stats_instance->do_server_side_stats(); } // increment number of times disconnected. $jetpack_unique_connection['disconnected'] += 1; } \Jetpack_Options::update_option( 'unique_connection', $jetpack_unique_connection ); } /** * Fires when a site is disconnected. * * @since 1.30.1 */ do_action( 'jetpack_site_disconnected' ); } /** * 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-connection' ), $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 1.7.0 * @since-jetpack 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', '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-connection' ), $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-connection' ), $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-connection' ), $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; } $domain = preg_replace( '#^https?://#', '', untrailingslashit( $domain ) ); if ( filter_var( $domain, FILTER_VALIDATE_IP ) && ! filter_var( $domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return new \WP_Error( 'fail_ip_forbidden', sprintf( /* translators: %1$s is a domain name. */ __( 'IP address `%1$s` just failed is_usable_domain check as it is in the private network.', 'jetpack-connection' ), $domain ) ); } return true; } /** * Gets the requested token. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_access_token() instead. * * @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 * * @see $this->get_tokens()->get_access_token() */ public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_access_token' ); return $this->get_tokens()->get_access_token( $user_id, $token_key, $suppress_errors ); } /** * 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 = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null; 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 array|IXR_Error 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-connection' ), 'readonly' => true, 'value' => $user_data['ID'], ); $options['jetpack_user_login'] = array( 'desc' => __( 'The WP.com username of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['login'], ); $options['jetpack_user_email'] = array( 'desc' => __( 'The WP.com user email of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['email'], ); $options['jetpack_user_site_count'] = array( 'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack-connection' ), '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_connected() ) { $jetpack_client_id = \Jetpack_Options::get_option( 'id' ); } $options['jetpack_version'] = array( 'desc' => __( 'Jetpack Plugin Version', 'jetpack-connection' ), '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-connection' ), '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 ) { return $this->get_tokens()->sign_role( $role, $user_id ); } /** * Set the plugin instance. * * @param Plugin $plugin_instance The plugin instance. * * @return $this */ public function set_plugin_instance( Plugin $plugin_instance ) { $this->plugin = $plugin_instance; return $this; } /** * Retrieve the plugin management object. * * @return Plugin|null */ public function get_plugin() { return $this->plugin; } /** * Get all connected plugins information, excluding those disconnected by user. * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded * Even if you don't use Jetpack Config, it may be introduced later by other plugins, * so please make sure not to run the method too early in the code. * * @return array|WP_Error */ public function get_connected_plugins() { $maybe_plugins = Plugin_Storage::get_all(); if ( $maybe_plugins instanceof WP_Error ) { return $maybe_plugins; } return $maybe_plugins; } /** * Force plugin disconnect. After its called, the plugin will not be allowed to use the connection. * Note: this method does not remove any access tokens. * * @deprecated since 1.39.0 * @return bool */ public function disable_plugin() { return null; } /** * Force plugin reconnect after user-initiated disconnect. * After its called, the plugin will be allowed to use the connection again. * Note: this method does not initialize access tokens. * * @deprecated since 1.39.0. * @return bool */ public function enable_plugin() { return null; } /** * Whether the plugin is allowed to use the connection, or it's been disconnected by user. * If no plugin slug was passed into the constructor, always returns true. * * @deprecated 1.42.0 This method no longer has a purpose after the removal of the soft disconnect feature. * * @return bool */ public function is_plugin_enabled() { return true; } /** * Perform the API request to refresh the blog token. * Note that we are making this request on behalf of the Jetpack master user, * given they were (most probably) the ones that registered the site at the first place. * * @return WP_Error|bool The result of updating the blog_token option. */ public function refresh_blog_token() { ( new Tracking() )->record_user_event( 'restore_connection_refresh_blog_token' ); $blog_id = \Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { return new WP_Error( 'site_not_registered', 'Site not registered.' ); } $url = sprintf( '%s/%s/v%s/%s', Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ), 'wpcom', '2', 'sites/' . $blog_id . '/jetpack-refresh-blog-token' ); $method = 'POST'; $user_id = get_current_user_id(); $response = Client::remote_request( compact( 'url', 'method', 'user_id' ) ); if ( is_wp_error( $response ) ) { return new WP_Error( 'refresh_blog_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 ) { if ( empty( $json->code ) ) { return new WP_Error( 'unknown', '', $code ); } /* translators: Error description string. */ $error_description = isset( $json->message ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->message ) : ''; return new WP_Error( (string) $json->code, $error_description, $code ); } if ( empty( $json->jetpack_secret ) || ! is_scalar( $json->jetpack_secret ) ) { return new WP_Error( 'jetpack_secret', '', $code ); } Error_Handler::get_instance()->delete_all_errors(); return $this->get_tokens()->update_blog_token( (string) $json->jetpack_secret ); } /** * Disconnect the user from WP.com, and initiate the reconnect process. * * @return bool */ public function refresh_user_token() { ( new Tracking() )->record_user_event( 'restore_connection_refresh_user_token' ); $this->disconnect_user( null, true, true ); return true; } /** * Fetches a signed token. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_signed_token() instead. * * @param object $token the token. * @return WP_Error|string a signed token */ public function get_signed_token( $token ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_signed_token' ); return $this->get_tokens()->get_signed_token( $token ); } /** * If the site-level connection is active, add the list of plugins using connection to the heartbeat (except Jetpack itself) * * @param array $stats The Heartbeat stats array. * @return array $stats */ public function add_stats_to_heartbeat( $stats ) { if ( ! $this->is_connected() ) { return $stats; } $active_plugins_using_connection = Plugin_Storage::get_all(); foreach ( array_keys( $active_plugins_using_connection ) as $plugin_slug ) { if ( 'jetpack' !== $plugin_slug ) { $stats_group = isset( $active_plugins_using_connection['jetpack'] ) ? 'combined-connection' : 'standalone-connection'; $stats[ $stats_group ][] = $plugin_slug; } } return $stats; } /** * Get the WPCOM or self-hosted site ID. * * @param bool $quiet Return null instead of an error. * * @return int|WP_Error|null */ public static function get_site_id( $quiet = false ) { $is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM ); $site_id = $is_wpcom ? get_current_blog_id() : \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return $quiet ? null : new \WP_Error( 'unavailable_site_id', __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ), 403 ); } return (int) $site_id; } /** * Check if Jetpack is ready for uninstall cleanup. * * @param string $current_plugin_slug The current plugin's slug. * * @return bool */ public static function is_ready_for_cleanup( $current_plugin_slug ) { $active_plugins = get_option( Plugin_Storage::ACTIVE_PLUGINS_OPTION_NAME ); return empty( $active_plugins ) || ! is_array( $active_plugins ) || ( count( $active_plugins ) === 1 && array_key_exists( $current_plugin_slug, $active_plugins ) ); } }