diff --git a/class.jetpack.php b/class.jetpack.php index d49d2f8e25813..8706d4b9f210c 100644 --- a/class.jetpack.php +++ b/class.jetpack.php @@ -4636,86 +4636,116 @@ function build_connect_url( $raw = false, $redirect = false, $from = false, $reg } public static function build_authorize_url( $redirect = false, $iframe = false ) { - if ( defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && include_once JETPACK__GLOTPRESS_LOCALES_PATH ) { - $gp_locale = GP_Locales::by_field( 'wp_locale', get_locale() ); - } - $roles = new Roles(); - $role = $roles->translate_current_user_to_role(); - $signed_role = self::connection()->sign_role( $role ); + add_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) ); + add_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) ); + add_filter( 'jetpack_connect_processing_url', array( __CLASS__, 'filter_connect_processing_url' ) ); - $user = wp_get_current_user(); + if ( $iframe ) { + add_filter( 'jetpack_api_url', array( __CLASS__, 'filter_connect_api_iframe_url' ), 10, 2 ); + } - $jetpack_admin_page = esc_url_raw( admin_url( 'admin.php?page=jetpack' ) ); - $redirect = $redirect - ? wp_validate_redirect( esc_url_raw( $redirect ), $jetpack_admin_page ) - : $jetpack_admin_page; + $c8n = self::connection(); + $url = $c8n->get_authorization_url( wp_get_current_user(), $redirect ); - if ( isset( $_REQUEST['is_multisite'] ) ) { - $redirect = Jetpack_Network::init()->get_url( 'network_admin_page' ); + remove_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) ); + remove_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) ); + remove_filter( 'jetpack_connect_processing_url', array( __CLASS__, 'filter_connect_processing_url' ) ); + + if ( $iframe ) { + remove_filter( 'jetpack_api_url', array( __CLASS__, 'filter_connect_api_iframe_url' ) ); } - $secrets = self::generate_secrets( 'authorize', false, 2 * HOUR_IN_SECONDS ); + return $url; + } - /** - * 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 connection URL parameter array. + * + * @param Array $args default URL parameters used by the package. + * @return Array the modified URL arguments array. + */ + public static function filter_connect_request_body( $args ) { + if ( + Constants::is_defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) + && include_once Constants::get_constant( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) + ) { + $gp_locale = GP_Locales::by_field( 'wp_locale', get_locale() ); + $args['locale'] = isset( $gp_locale ) && isset( $gp_locale->slug ) + ? $gp_locale->slug + : ''; + } - $tracks = new Tracking(); - $tracks_identity = $tracks->tracks_get_identity( get_current_user_id() ); + $tracking = new Tracking(); + $tracks_identity = $tracking->tracks_get_identity( $args['state'] ); - $args = urlencode_deep( + $args = array_merge( + $args, array( - 'response_type' => 'code', - 'client_id' => Jetpack_Options::get_option( 'id' ), - 'redirect_uri' => add_query_arg( - array( - 'action' => 'authorize', - '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), - 'redirect' => urlencode( $redirect ), - ), - esc_url( admin_url( 'admin.php?page=jetpack' ) ) - ), - 'state' => $user->ID, - 'scope' => $signed_role, - 'user_email' => $user->user_email, - 'user_login' => $user->user_login, - 'is_active' => self::is_active(), - 'jp_version' => JETPACK__VERSION, - 'auth_type' => $auth_type, - 'secret' => $secrets['secret_1'], - 'locale' => ( isset( $gp_locale ) && isset( $gp_locale->slug ) ) ? $gp_locale->slug : '', - 'blogname' => get_option( 'blogname' ), - 'site_url' => site_url(), - 'home_url' => home_url(), - 'site_icon' => get_site_icon_url(), - 'site_lang' => get_locale(), - '_ui' => $tracks_identity['_ui'], - '_ut' => $tracks_identity['_ut'], - 'site_created' => self::connection()->get_assumed_site_creation_date(), + '_ui' => $tracks_identity['_ui'], + '_ut' => $tracks_identity['_ut'], ) ); - self::apply_activation_source_to_args( $args ); - - $connection = self::connection(); - $calypso_env = self::get_calypso_env(); if ( ! empty( $calypso_env ) ) { $args['calypso_env'] = $calypso_env; } - $api_url = $iframe ? $connection->api_url( 'authorize_iframe' ) : $connection->api_url( 'authorize' ); + return $args; + } + + /** + * Filters the URL that will process the connection data. It can be different from the URL + * that we send the user to after everything is done. + * + * @param String $processing_url the default redirect URL used by the package. + * @return String the modified URL. + */ + public static function filter_connect_processing_url( $processing_url ) { + $processing_url = admin_url( 'admin.php?page=jetpack' ); // Making PHPCS happy. + return $processing_url; + } + + /** + * Filters the redirection URL that is used for connect requests. The redirect + * URL should return the user back to the Jetpack console. + * + * @param String $redirect the default redirect URL used by the package. + * @return String the modified URL. + */ + public static function filter_connect_redirect_url( $redirect ) { + $jetpack_admin_page = esc_url_raw( admin_url( 'admin.php?page=jetpack' ) ); + $redirect = $redirect + ? wp_validate_redirect( esc_url_raw( $redirect ), $jetpack_admin_page ) + : $jetpack_admin_page; + + if ( isset( $_REQUEST['is_multisite'] ) ) { + $redirect = Jetpack_Network::init()->get_url( 'network_admin_page' ); + } + + return $redirect; + } + + /** + * Filters the API URL that is used for connect requests. The method + * intercepts only the authorize URL and replaces it with another if needed. + * + * @param String $api_url the default redirect API URL used by the package. + * @param String $relative_url the path of the URL that's being used. + * @return String the modified URL. + */ + public static function filter_connect_api_iframe_url( $api_url, $relative_url ) { + + // Short-circuit on anything that is not related to connect requests. + if ( 'authorize' !== $relative_url ) { + return $api_url; + } + + $c8n = self::connection(); - return add_query_arg( $args, $api_url ); + return $c8n->api_url( 'authorize_iframe' ); } /** diff --git a/packages/connection/src/class-manager.php b/packages/connection/src/class-manager.php index 9d984f16262e6..a39658195cc4c 100644 --- a/packages/connection/src/class-manager.php +++ b/packages/connection/src/class-manager.php @@ -628,6 +628,38 @@ public function is_connection_owner( $user_id = false ) { return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id; } + /** + * Connects the user with a specified ID to a WordPress.com user using the + * remote login flow. + * + * @access public + * + * @param Integer $user_id (optional) the user identifier, defaults to current user. + * @param String $redirect_url the URL to redirect the user to for processing, defaults to + * admin_url(). + * @return WP_Error only in case of a failed user lookup. + */ + public function connect_user( $user_id = null, $redirect_url = null ) { + $user = null; + if ( null === $user_id ) { + $user = wp_get_current_user(); + } else { + $user = get_user_by( 'ID', $user_id ); + } + + if ( empty( $user ) ) { + return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' ); + } + + if ( null === $redirect_url ) { + $redirect_url = admin_url(); + } + + // Using wp_redirect intentionally because we're redirecting outside. + wp_redirect( $this->get_authorization_url( $user ) ); // phpcs:ignore WordPress.Security.SafeRedirect + exit(); + } + /** * Unlinks the current user from the linked WordPress.com user. * @@ -1383,18 +1415,96 @@ public function handle_authorization() { /** * Builds a URL to the Jetpack connection auth page. - * This needs rethinking. - * - * @param bool $raw If true, URL will not be escaped. - * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection. - * If string, will be a custom redirect. - * @param bool|string $from If not false, adds 'from=$from' param to the connect URL. - * @param bool $register If true, will generate a register URL regardless of the existing token, since 4.9.0. * - * @return string Connect URL + * @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 build_connect_url( $raw, $redirect, $from, $register ) { - return array( $raw, $redirect, $from, $register ); + public function get_authorization_url( $user = null, $redirect = null ) { + + if ( empty( $user ) ) { + $user = wp_get_current_user(); + } + + $roles = new Roles(); + $role = $roles->translate_user_to_role( $user ); + $signed_role = $this->sign_role( $role ); + + /** + * Filter the URL of the first time the user gets redirected back to your site for connection + * data processing. + * + * @since 8.0.0 + * + * @param string $redirect_url Defaults to the site admin URL. + */ + $processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) ); + + /** + * Filter the URL to redirect the user back to when the authorization process + * is complete. + * + * @since 8.0.0 + * + * @param string $redirect_url Defaults to the site URL. + */ + $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); + + $secrets = $this->generate_secrets( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS ); + + /** + * Filter the type of authorization. + * 'calypso' completes authorization on wordpress.com/jetpack/connect + * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com. + * + * @since 4.3.3 + * + * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'. + */ + $auth_type = apply_filters( 'jetpack_auth_type', 'calypso' ); + + /** + * Filters the user connection request data for additional property addition. + * + * @since 8.0.0 + * + * @param Array $request_data request data. + */ + $body = apply_filters( + 'jetpack_connect_request_body', + array( + 'response_type' => 'code', + 'client_id' => \Jetpack_Options::get_option( 'id' ), + 'redirect_uri' => add_query_arg( + array( + 'action' => 'authorize', + '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), + 'redirect' => rawurlencode( $redirect ), + ), + esc_url( $processing_url ) + ), + 'state' => $user->ID, + 'scope' => $signed_role, + 'user_email' => $user->user_email, + 'user_login' => $user->user_login, + 'is_active' => $this->is_active(), + 'jp_version' => Constants::get_constant( 'JETPACK__VERSION' ), + 'auth_type' => $auth_type, + 'secret' => $secrets['secret_1'], + 'blogname' => get_option( 'blogname' ), + 'site_url' => site_url(), + 'home_url' => home_url(), + 'site_icon' => get_site_icon_url(), + 'site_lang' => get_locale(), + 'site_created' => $this->get_assumed_site_creation_date(), + ) + ); + + $body = $this->apply_activation_source_to_args( urlencode_deep( $body ) ); + + $api_url = $this->api_url( 'authorize' ); + + return add_query_arg( $body, $api_url ); } /** diff --git a/packages/connection/tests/php/test_Manager.php b/packages/connection/tests/php/test_Manager.php index 96c3899bf45ff..d4dfde9d220b6 100644 --- a/packages/connection/tests/php/test_Manager.php +++ b/packages/connection/tests/php/test_Manager.php @@ -27,6 +27,18 @@ function( $filter_name, $return_value ) { ); $this->apply_filters = $builder->build(); + + $builder = new MockBuilder(); + $builder->setNamespace( __NAMESPACE__ ) + ->setName( 'wp_redirect' ) + ->setFunction( + function( $url ) { + $this->arguments_stack['wp_redirect'] [] = [ $url ]; + return true; + } + ); + + $this->wp_redirect = $builder->build(); } public function tearDown() {