ccess public * @since 9.2.0 * * @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 9.2.0 * * @return bool */ public function has_connected_user() { return (bool) count( $this->get_connected_users() ); } /** * 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' ) { return $this->get_tokens()->get_connected_users( $capability ); } /** * Returns true if the site has a connected Blog owner (master_user). * * @access public * @since 9.2.0 * * @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 9.6.0 * * @return bool */ public function is_userless() { 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 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 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 ); } 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(); } /** * 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. * @return Boolean Whether the disconnection of the user was successful. */ public function disconnect_user( $user_id = null, $can_overwrite_primary_user = false ) { $user_id = empty( $user_id ) ? get_current_user_id() : (int) $user_id; $result = $this->get_tokens()->disconnect_user( $user_id, $can_overwrite_primary_user ); if ( $result ) { $xml = new \Jetpack_IXR_Client( compact( 'user_id' ) ); $xml->query( 'jetpack.unlink_user', $user_id ); // 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 4.1.0 * * @param int $user_id The current user's ID. */ do_action( 'jetpack_unlinked_user', $user_id ); } return $result; } /** * 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 whether the connection manager should use the iframe authorization * flow instead of the regular redirect-based flow. * * @since 8.3.0 * * @param Boolean $is_iframe_flow_used should the iframe flow be used, defaults to false. */ $iframe_flow = apply_filters( 'jetpack_use_iframe_authorization_flow', false ); // Do not modify anything that is not related to authorize requests. if ( 'authorize' === $relative_url && $iframe_flow ) { $relative_url = 'authorize_iframe'; } /** * 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 $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' ) { 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' ) ); } 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' ), '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, ) ); $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, 'public' => $jetpack_public, ) ); $this->get_tokens()->update_blog_token( (string) $registration_details->jetpack_secret ); /** * 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 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' ); } 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. * * @deprecated since 9.5.0 * @see Nonce_Handler::add() */ public function add_nonce( $timestamp, $nonce ) { _deprecated_function( __METHOD__, 'jetpack-9.5.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 9.5.0 * @see Nonce_Handler::clean_all() */ public function clean_nonces( $all = false ) { _deprecated_function( __METHOD__, 'jetpack-9.5.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 8.7.0 * * @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 user-less 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 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; } /** * 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 9.5 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__, 'jetpack-9.5', 'Automattic\\Jetpack\\Connection\\Secrets->get' ); return ( new Secrets() )->get( $action, $user_id ); } /** * Deletes secret tokens in case they, for example, have expired. * * @deprecated 9.5 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__, 'jetpack-9.5', '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 8.7.0 */ 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; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 8.7.0 */ 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::disable_plugin() * @see Manager::disconnect_site_wpcom() * @see Manager::delete_all_connection_tokens() * * @return bool */ public function remove_connection() { $this->disable_plugin(); $this->disconnect_site_wpcom(); $this->delete_all_connection_tokens(); 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 ); $this->delete_all_connection_tokens( 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 userless 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_userless() ) { 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' ), 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 9.5 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__, 'jetpack-9.5', 'Automattic\\Jetpack\\Connection\\Tokens->validate' ); return $this->get_tokens()->validate( $user_id ); } /** * Verify a Previously Generated Secret. * * @deprecated 9.5 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__, 'jetpack-9.5', '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. * * @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->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 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 = ( 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 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( '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->is_active(), // TODO Deprecate this. '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_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 3.9.0 */ do_action( 'jetpack_user_authorized' ); if ( ! $is_connection_owner ) { /** * 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 ); \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. */ 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. * * @deprecated 9.5 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__, 'jetpack-9.5', '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 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_connected() ) { $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 ) { 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 */ 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( true ); 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. * * @return bool */ public function disable_plugin() { if ( ! $this->plugin ) { return false; } return $this->plugin->disable(); } /** * 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. * * @return bool */ public function enable_plugin() { if ( ! $this->plugin ) { return false; } return $this->plugin->enable(); } /** * 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. * * @return bool */ public function is_plugin_enabled() { if ( ! $this->plugin ) { return true; } return $this->plugin->is_enabled(); } /** * 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' ), (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 ); } 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 ); return true; } /** * Fetches a signed token. * * @deprecated 9.5 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__, 'jetpack-9.5', '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; } }