) 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 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(), ) ); $body = static::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 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', '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-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; } 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 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. * * @return int|WP_Error */ public static function get_site_id() { $is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM ); $site_id = $is_wpcom ? get_current_blog_id() : \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return new \WP_Error( 'unavailable_site_id', __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ), 403 ); } return (int) $site_id; } }