updated plugin Jetpack Protect version 4.0.0

This commit is contained in:
2025-04-29 21:19:56 +00:00
committed by Gitium
parent eb9181b250
commit ebd40ef928
265 changed files with 11864 additions and 3987 deletions

View File

@ -25,6 +25,9 @@ class Connection_Assets {
/**
* Register assets.
*
* NOTICE: Please think twice before including Connection scripts in the frontend.
* Those scripts are intended to be used in WP admin area.
*/
public static function register_assets() {

View File

@ -40,7 +40,7 @@ class Connection_Notice {
* @return void
*/
public function initialize_notices( $screen ) {
if ( ! in_array(
if ( in_array(
$screen->id,
array(
'jetpack_page_akismet-key-config',
@ -48,6 +48,19 @@ class Connection_Notice {
),
true
) ) {
return;
}
/*
* phpcs:disable WordPress.Security.NonceVerification.Recommended
*
* This function is firing within wp-admin and checks (below) if it is in the midst of a deletion on the users
* page. Nonce will be already checked by WordPress, so we do not need to check ourselves.
*/
if ( isset( $screen->base ) && 'users' === $screen->base
&& isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action']
) {
add_action( 'admin_notices', array( $this, 'delete_user_update_connection_owner_notice' ) );
}
}
@ -57,23 +70,6 @@ class Connection_Notice {
* the connection owner.
*/
public function delete_user_update_connection_owner_notice() {
global $current_screen;
/*
* phpcs:disable WordPress.Security.NonceVerification.Recommended
*
* This function is firing within wp-admin and checks (below) if it is in the midst of a deletion on the users
* page. Nonce will be already checked by WordPress, so we do not need to check ourselves.
*/
if ( ! isset( $current_screen->base ) || 'users' !== $current_screen->base ) {
return;
}
if ( ! isset( $_REQUEST['action'] ) || 'delete' !== $_REQUEST['action'] ) {
return;
}
// Get connection owner or bail.
$connection_manager = new Manager();
$connection_owner_id = $connection_manager->get_connection_owner_id();

View File

@ -691,7 +691,7 @@ class Error_Handler {
/**
* Fires inside the admin_notices hook just before displaying the error message for a broken connection.
*
* If you want to disable the default message from being displayed, return an emtpy value in the jetpack_connection_error_notice_message filter.
* If you want to disable the default message from being displayed, return an empty value in the jetpack_connection_error_notice_message filter.
*
* @since 8.9.0
*

View File

@ -80,6 +80,20 @@ class Manager {
*/
private static $disconnected_users = array();
/**
* Cached connection status.
*
* @var bool|null True if the site is connected, false if not, null if not determined yet.
*/
private static $is_connected = null;
/**
* Tracks whether connection status invalidation hooks have been added.
*
* @var bool
*/
private static $connection_invalidators_added = false;
/**
* Initialize the object.
* Make sure to call the "Configure" first.
@ -123,7 +137,9 @@ class Manager {
add_filter( 'shutdown', array( new Package_Version_Tracker(), 'maybe_update_package_versions' ) );
}
add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ) );
// This runs on priority 11 - at least one api method in the connection package is set to override a previously
// existing method from the Jetpack plugin. Running later than Jetpack's api init ensures the override is successful.
add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ), 11 );
( new Nonce_Handler() )->init_schedule();
@ -140,6 +156,8 @@ class Manager {
add_action( 'deleted_user', array( $manager, 'disconnect_user_force' ), 9, 1 );
add_action( 'remove_user_from_blog', array( $manager, 'disconnect_user_force' ), 9, 1 );
$manager->add_connection_status_invalidation_hooks();
// Set up package version hook.
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
@ -157,6 +175,28 @@ class Manager {
Partner::init();
}
/**
* Adds hooks to invalidate the memoized connection status.
*/
private function add_connection_status_invalidation_hooks() {
if ( self::$connection_invalidators_added ) {
return;
}
// Force is_connected() to recompute after important actions.
add_action( 'jetpack_site_registered', array( $this, 'reset_connection_status' ) );
add_action( 'jetpack_site_disconnected', array( $this, 'reset_connection_status' ) );
add_action( 'jetpack_sync_register_user', array( $this, 'reset_connection_status' ) );
add_action( 'pre_update_jetpack_option_id', array( $this, 'reset_connection_status' ) );
add_action( 'pre_update_jetpack_option_blog_token', array( $this, 'reset_connection_status' ) );
add_action( 'pre_update_jetpack_option_user_token', array( $this, 'reset_connection_status' ) );
add_action( 'pre_update_jetpack_option_user_tokens', array( $this, 'reset_connection_status' ) );
// phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff.
add_action( 'switch_blog', array( $this, 'reset_connection_status' ) );
self::$connection_invalidators_added = true;
}
/**
* Sets up the XMLRPC request handlers.
*
@ -172,7 +212,7 @@ class Manager {
$deprecated,
$has_connected_owner,
$is_signed,
Jetpack_XMLRPC_Server $xmlrpc_server = null
?Jetpack_XMLRPC_Server $xmlrpc_server = null
) {
add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 );
if ( $deprecated !== null ) {
@ -280,7 +320,7 @@ class Manager {
nocache_headers();
$wp_xmlrpc_server->serve_request();
exit;
exit( 0 );
}
/**
@ -415,8 +455,9 @@ class Manager {
if (
empty( $token_key )
||
empty( $version ) || (string) $jetpack_api_version !== $version ) {
|| empty( $version )
|| (string) $jetpack_api_version !== $version
) {
return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details', 'error_type' ) );
}
@ -596,9 +637,31 @@ class Manager {
* @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;
if ( self::$is_connected === null ) {
if ( ! self::$connection_invalidators_added ) {
$this->add_connection_status_invalidation_hooks();
}
$has_blog_id = (bool) \Jetpack_Options::get_option( 'id' );
if ( $has_blog_id ) {
$has_blog_token = (bool) $this->get_tokens()->get_access_token();
self::$is_connected = ( $has_blog_id && $has_blog_token );
} else {
// Short-circuit, no need to check for tokens if there's no blog ID.
self::$is_connected = false;
}
}
return self::$is_connected;
}
/**
* Resets the memoized connection status.
* This will force the connection status to be recomputed on the next check.
*
* @since 5.0.0
*/
public function reset_connection_status() {
self::$is_connected = null;
}
/**
@ -875,25 +938,54 @@ class Manager {
// Using wp_redirect intentionally because we're redirecting outside.
wp_redirect( $this->get_authorization_url( $user, $redirect_url ) ); // phpcs:ignore WordPress.Security.SafeRedirect
exit();
exit( 0 );
}
/**
* Force user disconnect.
*
* @param int $user_id Local (external) user ID.
* @param int $user_id Local (external) user ID.
* @param bool $disconnect_all_users Whether to disconnect all users before disconnecting the primary user.
*
* @return bool
*/
public function disconnect_user_force( $user_id ) {
public function disconnect_user_force( $user_id, $disconnect_all_users = false ) {
if ( ! (int) $user_id ) {
// Missing user ID.
return false;
}
// If we are disconnecting the primary user we may need to disconnect all other users first
if ( $user_id === $this->get_connection_owner_id() && $disconnect_all_users && ! $this->disconnect_all_users_except_primary() ) {
return false;
}
return $this->disconnect_user( $user_id, true, true );
}
/**
* Disconnects all users except the primary user.
*
* @return bool
*/
public function disconnect_all_users_except_primary() {
$all_connected_users = $this->get_connected_users();
foreach ( $all_connected_users as $user ) {
// Skip the primary.
if ( $user->ID === $this->get_connection_owner_id() ) {
continue;
}
$disconnected = $this->disconnect_user( $user->ID, false, true );
// If we fail to disconnect any user, we should not proceed with disconnecting the primary user.
if ( ! $disconnected ) {
return false;
}
}
return true;
}
/**
* Unlinks the current user from the linked WordPress.com user.
*
@ -1505,6 +1597,16 @@ class Manager {
// 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;
case 'jetpack_unlink_user':
$is_offline_mode = ( new Status() )->is_offline_mode();
if ( $is_offline_mode ) {
$caps = array( 'do_not_allow' );
break;
}
// Non-admins can always disconnect
$caps = array( 'read' );
break;
}
return $caps;
}
@ -1561,12 +1663,17 @@ class Manager {
return $cached_date;
}
/**
* We don't use the 'ID' field, but need it to overcome a WP caching bug: https://core.trac.wordpress.org/ticket/62003
*
* @todo Remote the 'ID' field from users fetching when the issue is fixed and Jetpack-supported WP versions move beyond it.
*/
$earliest_registered_users = get_users(
array(
'role' => 'administrator',
'orderby' => 'user_registered',
'order' => 'ASC',
'fields' => array( 'user_registered' ),
'fields' => array( 'ID', 'user_registered' ),
'number' => 1,
)
);
@ -2125,6 +2232,8 @@ class Manager {
( new Nonce_Handler() )->clean_all();
Heartbeat::init()->deactivate();
/**
* Fires before a site is disconnected.
*

View File

@ -12,7 +12,7 @@ namespace Automattic\Jetpack\Connection;
*/
class Package_Version {
const PACKAGE_VERSION = '4.0.1';
const PACKAGE_VERSION = '6.8.1';
const PACKAGE_SLUG = 'connection';

View File

@ -15,7 +15,7 @@ use Jetpack_Options;
* Disable direct access.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
exit( 0 );
}
/**

View File

@ -17,14 +17,6 @@ class Plugin_Storage {
const ACTIVE_PLUGINS_OPTION_NAME = 'jetpack_connection_active_plugins';
/**
* Options where disabled plugins were stored
*
* @deprecated since 1.39.0.
* @var string
*/
const PLUGINS_DISABLED_OPTION_NAME = 'jetpack_connection_disabled_plugins';
/**
* Transient name used as flag to indicate that the active connected plugins list needs refreshing.
*/
@ -93,13 +85,9 @@ class Plugin_Storage {
* 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.
*
* @since 1.39.0 deprecated the $connected_only argument.
*
* @param null $deprecated null plugins that were explicitly disconnected. Deprecated, there's no such a thing as disconnecting only specific plugins anymore.
*
* @return array|WP_Error
*/
public static function get_all( $deprecated = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public static function get_all() {
$maybe_error = self::ensure_configured();
if ( $maybe_error instanceof WP_Error ) {
@ -144,7 +132,10 @@ class Plugin_Storage {
}
if ( is_multisite() && get_current_blog_id() !== self::$current_blog_id ) {
self::$plugins = (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() );
if ( self::$current_blog_id ) {
// If blog ID got changed, pull the list of active plugins for that blog from the database.
self::$plugins = (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() );
}
self::$current_blog_id = get_current_blog_id();
}
@ -234,43 +225,6 @@ class Plugin_Storage {
}
}
/**
* Add the plugin to the set of disconnected ones.
*
* @deprecated since 1.39.0.
*
* @param string $slug Plugin slug.
*
* @return bool
*/
public static function disable_plugin( $slug ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return true;
}
/**
* Remove the plugin from the set of disconnected ones.
*
* @deprecated since 1.39.0.
*
* @param string $slug Plugin slug.
*
* @return bool
*/
public static function enable_plugin( $slug ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return true;
}
/**
* Get all plugins that were disconnected by user.
*
* @deprecated since 1.39.0.
*
* @return array
*/
public static function get_all_disabled_plugins() { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return array();
}
/**
* Update active plugins option with current list of active plugins on WPCOM.
* This is a fallback to ensure this option is always up to date on WPCOM in case

View File

@ -31,6 +31,13 @@ class Plugin {
*/
private $slug;
/**
* Users Connection Admin instance.
*
* @var Users_Connection_Admin
*/
private $users_connection_admin;
/**
* Initialize the plugin manager.
*
@ -38,6 +45,9 @@ class Plugin {
*/
public function __construct( $slug ) {
$this->slug = $slug;
// Initialize Users_Connection_Admin
$this->users_connection_admin = new Users_Connection_Admin();
}
/**
@ -87,36 +97,4 @@ class Plugin {
return ! $plugins || ( array_key_exists( $this->slug, $plugins ) && 1 === count( $plugins ) );
}
/**
* Add the plugin to the set of disconnected ones.
*
* @deprecated since 1.39.0.
*
* @return bool
*/
public function disable() {
return true;
}
/**
* Remove the plugin from the set of disconnected ones.
*
* @deprecated since 1.39.0.
*
* @return bool
*/
public function enable() {
return true;
}
/**
* Whether this plugin is allowed to use the connection.
*
* @deprecated since 11.0
* @return bool
*/
public function is_enabled() {
return true;
}
}

View File

@ -219,4 +219,17 @@ class Rest_Authentication {
return true === $instance->rest_authentication_status && 'blog' === $instance->rest_authentication_type;
}
/**
* Whether the request was signed with a user token.
*
* @since 6.7.0
*
* @return bool True if the request was signed with a valid user token, false otherwise.
*/
public static function is_signed_with_user_token() {
$instance = self::init();
return true === $instance->rest_authentication_status && 'user' === $instance->rest_authentication_type;
}
}

View File

@ -167,6 +167,20 @@ class REST_Connector {
)
);
// Disconnect/unlink user from WordPress.com servers.
// this endpoint is set to override the older endpoint that was previously in the Jetpack plugin
// Override is here in case an older version of the Jetpack plugin is installed alongside an updated standalone
register_rest_route(
'jetpack/v4',
'/connection/user',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::unlink_user',
'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
),
true // override other implementations
);
// We are only registering this route if Jetpack-the-plugin is not active or it's version is ge 10.0-alpha.
// The reason for doing so is to avoid conflicts between the Connection package and
// older versions of Jetpack, registering the same route twice.
@ -214,20 +228,15 @@ class REST_Connector {
'callback' => array( $this, 'connection_register' ),
'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
'args' => array(
'from' => array(
'from' => array(
'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack-connection' ),
'type' => 'string',
),
'registration_nonce' => array(
'description' => __( 'The registration nonce', 'jetpack-connection' ),
'type' => 'string',
'required' => true,
),
'redirect_uri' => array(
'redirect_uri' => array(
'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
'type' => 'string',
),
'plugin_slug' => array(
'plugin_slug' => array(
'description' => __( 'Indicates from what plugin the request is coming from', 'jetpack-connection' ),
'type' => 'string',
),
@ -252,6 +261,34 @@ class REST_Connector {
)
);
// Provider-specific authorization URL endpoint
register_rest_route(
'jetpack/v4',
'/connection/authorize_url/(?P<provider>[a-zA-Z]+)',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'connection_authorize_url_provider' ),
'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
'args' => array(
'provider' => array(
'description' => __( 'Authentication provider (google, github, apple, link)', 'jetpack-connection' ),
'type' => 'string',
'required' => true,
'enum' => array( 'google', 'github', 'apple', 'link' ),
),
'redirect_uri' => array(
'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
'type' => 'string',
),
'email_address' => array(
'description' => __( 'Email address for magic link authentication', 'jetpack-connection' ),
'type' => 'string',
'format' => 'email',
),
),
)
);
register_rest_route(
'jetpack/v4',
'/user-token',
@ -340,9 +377,15 @@ class REST_Connector {
*
* @return WP_Error|array
*/
public static function remote_provision( WP_REST_Request $request ) {
public function remote_provision( WP_REST_Request $request ) {
$request_data = $request->get_params();
if ( current_user_can( 'jetpack_connect_user' ) ) {
$request_data['local_user'] = get_current_user_id();
}
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_provision( $request );
$result = $xmlrpc_server->remote_provision( $request_data );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
@ -394,9 +437,15 @@ class REST_Connector {
/**
* Remote provision endpoint permission check.
*
* @param WP_REST_Request $request The request object.
*
* @return true|WP_Error
*/
public function remote_provision_permission_check() {
public function remote_provision_permission_check( WP_REST_Request $request ) {
if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) {
return true;
}
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
@ -545,15 +594,36 @@ class REST_Connector {
*
* @since 1.30.1
*
* @return bool|WP_Error True if user is able to disconnect the site.
* @since 5.1.0 Modified the permission check to accept requests signed with blog tokens.
*
* @return bool|WP_Error True if user is able to disconnect the site or the request is signed with a blog token (aka a direct request from WPCOM).
*/
public static function disconnect_site_permission_check() {
if ( current_user_can( 'jetpack_disconnect' ) ) {
return true;
}
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
*
* @since 6.3.3
*
* @return bool|WP_Error True if user is able to unlink.
*/
public static function unlink_user_permission_callback() {
// This is a mapped capability
// phpcs:ignore WordPress.WP.Capabilities.Unknown
if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) {
return true;
}
return new WP_Error(
'invalid_user_permission_jetpack_disconnect',
'invalid_user_permission_unlink_user',
self::get_user_permissions_error_msg(),
array( 'status' => rest_authorization_required_code() )
);
@ -607,11 +677,15 @@ class REST_Connector {
'id' => $current_user->ID,
'blogId' => $blog_id,
'wpcomUser' => $wpcom_user_data,
'gravatar' => get_avatar_url( $current_user->ID, 64, 'mm', '', array( 'force_display' => true ) ),
'gravatar' => get_avatar_url( $current_user->ID ),
'permissions' => array(
'connect' => current_user_can( 'jetpack_connect' ),
'connect_user' => current_user_can( 'jetpack_connect_user' ),
'disconnect' => current_user_can( 'jetpack_disconnect' ),
'connect' => current_user_can( 'jetpack_connect' ),
'connect_user' => current_user_can( 'jetpack_connect_user' ),
// This is a mapped capability
// phpcs:ignore WordPress.WP.Capabilities.Unknown
'unlink_user' => current_user_can( 'jetpack_unlink_user' ),
'disconnect' => current_user_can( 'jetpack_disconnect' ),
'manage_options' => current_user_can( 'manage_options' ),
),
);
@ -627,6 +701,7 @@ class REST_Connector {
$response = array(
'currentUser' => $current_user_connection_data,
'connectionOwner' => $owner_display_name,
'isRegistered' => $connection->is_connected(),
);
if ( $rest_response ) {
@ -781,9 +856,10 @@ class REST_Connector {
}
/**
* The endpoint tried to partially or fully reconnect the website to WP.com.
* The endpoint tried to connect Jetpack site to WPCOM.
*
* @since 1.7.0
* @since 6.7.0 No longer needs `registration_nonce`.
* @since-jetpack 7.7.0
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
@ -791,10 +867,6 @@ class REST_Connector {
* @return \WP_REST_Response|WP_Error
*/
public function connection_register( $request ) {
if ( ! wp_verify_nonce( $request->get_param( 'registration_nonce' ), 'jetpack-registration-nonce' ) ) {
return new WP_Error( 'invalid_nonce', __( 'Unable to verify your request.', 'jetpack-connection' ), array( 'status' => 403 ) );
}
if ( isset( $request['from'] ) ) {
$this->connection->add_register_request_param( 'from', (string) $request['from'] );
}
@ -930,6 +1002,51 @@ class REST_Connector {
);
}
/**
* Unlinks current user from the WordPress.com Servers.
*
* @since 6.3.3
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return bool|WP_Error True if user successfully unlinked.
*/
public static function unlink_user( $request ) {
if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
}
// If the user is also connection owner, we need to disconnect all users. Since disconnecting all users is a destructive action, we need to pass a parameter to confirm the action.
$disconnect_all_users = false;
if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
$disconnect_all_users = true;
} else {
return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
}
}
// Allow admins to force a disconnect by passing the "force" parameter
// This allows an admin to disconnect themselves
if ( isset( $request['force'] ) && false !== $request['force'] && current_user_can( 'manage_options' ) && ( new Manager( 'jetpack' ) )->disconnect_user_force( get_current_user_id(), $disconnect_all_users ) ) {
return rest_ensure_response(
array(
'code' => 'success',
)
);
} elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
return rest_ensure_response(
array(
'code' => 'success',
)
);
}
return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
}
/**
* Verify that the API client is allowed to replace user token.
*
@ -1021,4 +1138,53 @@ class REST_Connector {
? true
: new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* Provider-specific authorization URL endpoint
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response|WP_Error
*/
public function connection_authorize_url_provider( $request ) {
$provider = $request['provider'];
$redirect_uri = $request['redirect_uri'] ?? '';
// Validate magic link parameters if provider is 'link'
if ( 'link' === $provider ) {
if ( empty( $request['email_address'] ) ) {
return new WP_Error(
'missing_email',
__( 'Email address is required for magic link authentication.', 'jetpack-connection' ),
array( 'status' => 400 )
);
}
// Sanitize email address
$email = sanitize_email( $request['email_address'] );
if ( ! is_email( $email ) ) {
return new WP_Error(
'invalid_email',
__( 'Invalid email address format.', 'jetpack-connection' ),
array( 'status' => 400 )
);
}
}
$authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url(
$redirect_uri,
false,
false,
$provider,
array(
'email_address' => $email ?? '',
)
);
return rest_ensure_response(
array(
'authorizeUrl' => $authorize_url,
)
);
}
}

View File

@ -280,24 +280,28 @@ class Tracking {
*/
public function tracks_get_identity( $user_id ) {
// Meta is set, and user is still connected. Use WPCOM ID.
// Meta is set, and user is still connected. Use WPCOM ID.
$wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
if ( $wpcom_id && $this->connection->is_user_connected( $user_id ) ) {
if ( $wpcom_id && is_string( $wpcom_id ) && $this->connection->is_user_connected( $user_id ) ) {
return array(
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_id,
);
}
// User is connected, but no meta is set yet. Use WPCOM ID and set meta.
// User is connected, but no meta is set yet. Use WPCOM ID and set meta.
if ( $this->connection->is_user_connected( $user_id ) ) {
$wpcom_user_data = $this->connection->get_connected_user_data( $user_id );
update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] );
$wpcom_id = $wpcom_user_data['ID'] ?? null;
return array(
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_user_data['ID'],
);
if ( is_string( $wpcom_id ) ) {
update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_id );
return array(
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_id,
);
}
}
// User isn't linked at all. Fall back to anonymous ID.

View File

@ -0,0 +1,175 @@
<?php
/**
* Handles the WordPress.com account column in the users list table.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Status\Host;
/**
* Class Users_Connection_Admin
*/
class Users_Connection_Admin {
/**
* The column ID used for the WordPress.com account column.
*
* @var string
*/
const COLUMN_ID = 'user_jetpack';
/**
* Constructor.
*/
public function __construct() {
// Only set up hooks if we're in the admin area and user has proper permissions
add_action( 'init', array( $this, 'init' ) );
}
/**
* Initialize the admin functionality if conditions are met.
*/
public function init() {
if ( ! is_admin() || ! current_user_can( 'manage_options' ) || ( new Host() )->is_wpcom_simple() ) {
return;
}
add_filter( 'manage_users_columns', array( $this, 'add_connection_column' ) );
add_filter( 'manage_users_custom_column', array( $this, 'render_connection_column' ), 9, 3 ); // Priority 9 to run before SSO
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'admin_print_styles-users.php', array( $this, 'add_connection_column_styles' ) );
}
/**
* Add the connection column to the users list table.
*
* @param array $columns The current columns.
* @return array Modified columns.
*/
public function add_connection_column( $columns ) {
$columns[ self::COLUMN_ID ] = sprintf(
'<span class="jetpack-connection-tooltip-icon" role="tooltip" tabindex="0" aria-label="%2$s: %1$s">
%1$s
<span class="jetpack-connection-tooltip"></span>
</span>',
esc_html__( 'WordPress.com account', 'jetpack-connection' ),
esc_attr__( 'Tooltip', 'jetpack-connection' )
);
return $columns;
}
/**
* Render the connection column content.
*
* @param string $output Custom column output.
* @param string $column_name Column name.
* @param int $user_id ID of the currently-listed user.
* @return string
*/
public function render_connection_column( $output, $column_name, $user_id ) {
if ( self::COLUMN_ID !== $column_name ) {
return $output;
}
if ( ( new Manager() )->is_user_connected( $user_id ) ) {
return sprintf(
'<span title="%1$s" class="jetpack-connection-status">%2$s</span>',
esc_attr__( 'This user has connected their WordPress.com account.', 'jetpack-connection' ),
esc_html__( 'Connected', 'jetpack-connection' )
);
}
return $output;
}
/**
* Enqueue scripts and styles.
*
* @param string $hook The current admin page.
*/
public function enqueue_scripts( $hook ) {
if ( 'users.php' !== $hook ) {
return;
}
Assets::register_script(
'jetpack-users-connection',
'../dist/jetpack-users-connection.js',
__FILE__,
array(
'strategy' => 'defer',
'in_footer' => true,
'enqueue' => true,
'version' => Package_Version::PACKAGE_VERSION,
'deps' => array( 'wp-i18n' ),
)
);
wp_localize_script(
'jetpack-users-connection',
'jetpackConnectionTooltips',
array(
'columnTooltip' => esc_html__( 'Connecting a WordPress.com account unlocks Jetpacks full suite of features including secure logins.', 'jetpack-connection' ),
)
);
}
/**
* Add styles for the connection column.
*/
public function add_connection_column_styles() {
?>
<style>
.jetpack-connection-tooltip-icon {
position: relative;
cursor: pointer;
}
/* Add [?] icon using pseudo-element, only in column header */
th.manage-column .jetpack-connection-tooltip-icon::after {
content: '[?]';
color: #3c434a;
font-size: 1em;
margin-left: 4px;
}
.jetpack-connection-tooltip {
position: absolute;
background: #f6f7f7;
top: -85px;
width: 250px;
padding: 7px;
color: #3c434a;
font-size: .75rem;
line-height: 17px;
text-align: left;
margin: 0;
display: none;
border-radius: 4px;
font-family: sans-serif;
box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.1);
left: -170px;
}
.column-user_jetpack {
width: 140px;
}
/* Show tooltip on hover and focus */
.jetpack-connection-tooltip-icon:hover .jetpack-connection-tooltip,
.jetpack-connection-tooltip-icon:focus-within .jetpack-connection-tooltip {
display: block;
}
</style>
<?php
}
/**
* Get the column ID. Allows other classes to reference the same column.
*
* @return string
*/
public static function get_column_id() {
return self::COLUMN_ID;
}
}

View File

@ -163,7 +163,7 @@ class Webhooks {
* @return never
*/
protected function do_exit() {
exit;
exit( 0 );
}
/**
@ -199,6 +199,10 @@ class Webhooks {
wp_safe_redirect( $redirect );
$this->do_exit();
} else {
if ( 'connect-after-checkout' === $from && $redirect ) {
wp_safe_redirect( $redirect );
$this->do_exit();
}
$connect_url = add_query_arg(
array(
'from' => $from,

View File

@ -162,17 +162,21 @@ class UI {
$consumer_chosen = null;
$consumer_url_length = 0;
foreach ( $consumers as $consumer ) {
foreach ( $consumers as &$consumer ) {
if ( empty( $consumer['admin_page'] ) || ! is_string( $consumer['admin_page'] ) ) {
continue;
}
if ( isset( $consumer['customContent'] ) && is_callable( $consumer['customContent'] ) ) {
$consumer['customContent'] = call_user_func( $consumer['customContent'] );
}
if ( isset( $_SERVER['REQUEST_URI'] ) && str_starts_with( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ), $consumer['admin_page'] ) && strlen( $consumer['admin_page'] ) > $consumer_url_length ) {
$consumer_chosen = $consumer;
$consumer_url_length = strlen( $consumer['admin_page'] );
}
}
unset( $consumer );
static::$consumers = $consumer_chosen ? $consumer_chosen : array_shift( $consumers );

View File

@ -191,7 +191,7 @@ class SSO {
Helpers::delete_connection_for_user( $current_user->ID );
wp_logout();
wp_safe_redirect( wp_login_url() );
exit;
exit( 0 );
}
}
@ -491,7 +491,7 @@ class SSO {
$tracking->record_user_event( 'sso_login_redirect_success' );
wp_safe_redirect( $sso_url );
exit;
exit( 0 );
}
} elseif ( Helpers::display_sso_form_for_action( $action ) ) {
@ -509,7 +509,7 @@ class SSO {
$sso_url = $this->get_sso_url_or_die( $reauth );
$tracking->record_user_event( 'sso_login_redirect_bypass_success' );
wp_safe_redirect( $sso_url );
exit;
exit( 0 );
}
$this->display_sso_login_form();
@ -622,7 +622,7 @@ class SSO {
<?php if ( $display_name && $gravatar ) : ?>
<a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>">
<?php esc_html_e( 'Log in as a different WordPress.com user', 'jetpack-connection' ); ?>
<?php esc_html_e( 'Log in with another WordPress.com account', 'jetpack-connection' ); ?>
</a>
<?php else : ?>
<p>
@ -969,7 +969,7 @@ class SSO {
admin_url()
)
);
exit;
exit( 0 );
}
add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) );
@ -977,7 +977,7 @@ class SSO {
/** This filter is documented in core/src/wp-login.php */
apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
);
exit;
exit( 0 );
}
add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
@ -1207,7 +1207,7 @@ class SSO {
add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) );
wp_safe_redirect( $connect_url );
exit;
exit( 0 );
}
/**

View File

@ -11,6 +11,7 @@ use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Connection\Package_Version;
use Automattic\Jetpack\Connection\Users_Connection_Admin as Base_Admin;
use Automattic\Jetpack\Roles;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Tracking;
@ -21,7 +22,7 @@ use WP_User_Query;
/**
* Jetpack sso user admin class.
*/
class User_Admin {
class User_Admin extends Base_Admin {
/**
* Instance of WP_User_Query.
*
@ -56,16 +57,20 @@ class User_Admin {
add_action( 'user_new_form', array( $this, 'render_custom_email_message_form_field' ), 1 );
add_action( 'delete_user_form', array( $this, 'render_invitations_notices_for_deleted_users' ) );
add_action( 'delete_user', array( $this, 'revoke_user_invite' ) );
add_filter( 'manage_users_columns', array( $this, 'jetpack_user_connected_th' ) );
add_filter( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 );
add_action( 'user_row_actions', array( $this, 'jetpack_user_table_row_actions' ), 10, 2 );
add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) );
if ( isset( $_GET['jetpack-sso-invite-user'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) );
}
add_action( 'admin_post_jetpack_invite_user_to_wpcom', array( $this, 'invite_user_to_wpcom' ) );
add_action( 'admin_post_jetpack_revoke_invite_user_to_wpcom', array( $this, 'handle_request_revoke_invite' ) );
add_action( 'admin_post_jetpack_resend_invite_user_to_wpcom', array( $this, 'handle_request_resend_invite' ) );
add_action( 'admin_print_styles-users.php', array( $this, 'jetpack_user_table_styles' ) );
add_filter( 'users_list_table_query_args', array( $this, 'set_user_query' ), 100, 1 );
add_action( 'admin_print_styles-user-new.php', array( $this, 'jetpack_new_users_styles' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
self::$tracking = new Tracking();
}
@ -102,6 +107,7 @@ class User_Admin {
* Revokes WordPress.com invitation.
*
* @param int $user_id The user ID.
* @return mixed Response from the API call or false on failure.
*/
public function revoke_user_invite( $user_id ) {
try {
@ -970,39 +976,14 @@ class User_Admin {
}
/**
* Adds a column in the user admin table to display user connection status and actions.
* Deprecated method. Adds a column in the user admin table to display user connection status and actions.
*
* @param array $columns User list table columns.
*
* @return array
* @deprecated 6.5.0
*/
public function jetpack_user_connected_th( $columns ) {
Assets::register_script(
'jetpack-sso-users',
'../../dist/jetpack-sso-users.js',
__FILE__,
array(
'strategy' => 'defer',
'in_footer' => true,
'enqueue' => true,
'version' => Package_Version::PACKAGE_VERSION,
)
);
$tooltip_string = esc_attr__( 'Jetpack SSO allows a seamless and secure experience on WordPress.com. Join millions of WordPress users who trust us to keep their accounts safe.', 'jetpack-connection' );
wp_add_inline_script(
'jetpack-sso-users',
"var Jetpack_SSOTooltip = { 'tooltipString': '{$tooltip_string}' }",
'before'
);
$columns['user_jetpack'] = sprintf(
'<span class="jetpack-sso-invitation-tooltip-icon jetpack-sso-status-column" role="tooltip" aria-label="%3$s: %1$s" tabindex="0">%2$s</span>',
$tooltip_string,
esc_html__( 'SSO Status', 'jetpack-connection' ),
esc_attr__( 'Tooltip', 'jetpack-connection' )
);
_deprecated_function( __METHOD__, 'package-6.5.0' );
return $columns;
}
@ -1175,59 +1156,58 @@ class User_Admin {
* @param string $val HTML for the column.
* @param string $col User list table column.
* @param int $user_id User ID.
*
* @return string
* @return string Modified column content.
*/
public function jetpack_show_connection_status( $val, $col, $user_id ) {
if ( 'user_jetpack' === $col ) {
if ( ( new Manager() )->is_user_connected( $user_id ) ) {
$connection_html = sprintf(
'<span title="%1$s" class="jetpack-sso-invitation">%2$s</span>',
esc_attr__( 'This user is connected and can log-in to this site.', 'jetpack-connection' ),
esc_html__( 'Connected', 'jetpack-connection' )
);
return $connection_html;
} else {
$has_pending_invite = self::has_pending_wpcom_invite( $user_id );
if ( $has_pending_invite ) {
$connection_html = sprintf(
'<span title="%1$s" class="jetpack-sso-invitation sso-pending-invite">%2$s</span>',
esc_attr__( 'This user didn&#8217;t accept the invitation to join this site yet.', 'jetpack-connection' ),
esc_html__( 'Pending invite', 'jetpack-connection' )
);
return $connection_html;
}
$nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
$connection_html = sprintf(
// Using formmethod and formaction because we can't nest forms and have to submit using the main form.
'<span tabindex="0" role="tooltip" aria-label="%4$s: %3$s" class="jetpack-sso-invitation-tooltip-icon sso-disconnected-user">
<a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a>
<span class="sso-disconnected-user-icon dashicons dashicons-warning">
<span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip">%3$s</span>
</span>
</span>',
add_query_arg(
array(
'user_id' => $user_id,
'invite_nonce' => $nonce,
'action' => 'jetpack_invite_user_to_wpcom',
),
admin_url( 'admin-post.php' )
),
esc_html__( 'Send invite', 'jetpack-connection' ),
esc_attr__( 'This user doesn&#8217;t have an SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack-connection' ),
esc_attr__( 'Tooltip', 'jetpack-connection' )
);
return $connection_html;
}
if ( 'user_jetpack' !== $col ) {
return $val;
}
return $val;
// Get base connection status from parent
$connection_status = parent::render_connection_column( '', $col, $user_id );
// If user is not connected, check for pending invite
if ( ! $connection_status ) {
$has_pending_invite = self::has_pending_wpcom_invite( $user_id );
if ( $has_pending_invite ) {
return sprintf(
'<span title="%1$s" class="jetpack-sso-invitation sso-pending-invite">%2$s</span>',
esc_attr__( 'This user didn&#8217;t accept the invitation to join this site yet.', 'jetpack-connection' ),
esc_html__( 'Pending invite', 'jetpack-connection' )
);
}
// Show invite button for non-connected users
$nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
return sprintf(
'<span tabindex="0" role="tooltip" aria-label="%4$s: %3$s" class="jetpack-sso-invitation-tooltip-icon sso-disconnected-user">
<a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a>
<span class="sso-disconnected-user-icon dashicons dashicons-warning">
<span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip">%3$s</span>
</span>
</span>',
add_query_arg(
array(
'user_id' => $user_id,
'invite_nonce' => $nonce,
'action' => 'jetpack_invite_user_to_wpcom',
),
admin_url( 'admin-post.php' )
),
esc_html__( 'Send invite', 'jetpack-connection' ),
esc_attr__( 'This user doesn&#8217;t have a Jetpack SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack-connection' ),
esc_attr__( 'Tooltip', 'jetpack-connection' )
);
}
return $connection_status;
}
/**
* Creates error notices and redirects the user to the previous page.
*
* @param array $query_params - query parameters added to redirection URL.
* @phan-suppress PhanPluginNeverReturnMethod
*/
public function create_error_notice_and_redirect( $query_params ) {
$ref = wp_get_referer();
@ -1239,7 +1219,8 @@ class User_Admin {
$query_params,
$ref
);
return wp_safe_redirect( $url );
wp_safe_redirect( $url );
exit;
}
/**
@ -1254,9 +1235,6 @@ class User_Admin {
#the-list tr:has(.sso-pending-invite) {
background: #E9F0F5;
}
.fixed .column-user_jetpack {
width: 100px;
}
.jetpack-sso-invitation {
background: none;
border: none;
@ -1293,9 +1271,6 @@ class User_Admin {
position: relative;
cursor: pointer;
}
.jetpack-sso-th-tooltip {
left: -170px;
}
.jetpack-sso-td-tooltip {
left: -256px;
}
@ -1319,4 +1294,29 @@ class User_Admin {
</style>
<?php
}
/**
* Enqueue SSO-specific scripts.
*
* @param string $hook The current admin page.
*/
public function enqueue_scripts( $hook ) {
if ( 'users.php' !== $hook ) {
return;
}
parent::enqueue_scripts( $hook );
// Enqueue the SSO users script.
Assets::register_script(
'jetpack-sso-users',
'../../dist/jetpack-sso-users.js',
__FILE__,
array(
'strategy' => 'defer',
'in_footer' => true,
'enqueue' => true,
'version' => Package_Version::PACKAGE_VERSION,
)
);
}
}

View File

@ -5,7 +5,7 @@ document.addEventListener( 'DOMContentLoaded', function () {
tooltip.innerHTML += ' [?]';
const tooltipTextbox = document.createElement( 'span' );
tooltipTextbox.classList.add( 'jetpack-sso-invitation-tooltip', 'jetpack-sso-th-tooltip' );
tooltipTextbox.classList.add( 'jetpack-sso-invitation-tooltip' );
const tooltipString = window.Jetpack_SSOTooltip.tooltipString;
tooltipTextbox.innerHTML += tooltipString;
@ -28,7 +28,7 @@ document.addEventListener( 'DOMContentLoaded', function () {
*/
function removeTooltip() {
// Only remove tooltip if the element isn't currently active.
if ( document.activeElement === tooltip ) {
if ( tooltip.ownerDocument.activeElement === tooltip ) {
return;
}
tooltip.removeChild( tooltipTextbox );
@ -56,7 +56,7 @@ document.addEventListener( 'DOMContentLoaded', function () {
* @param {Event} event - Triggering event.
*/
function removeSSOInvitationTooltip( event ) {
if ( document.activeElement === event.target ) {
if ( event.target.ownerDocument.activeElement === event.target ) {
return;
}
this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'none';

View File

@ -0,0 +1,151 @@
<?php
/**
* Trait WPCOM_REST_API_Proxy_Request
*
* Used to proxy requests to wpcom servers.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection\Traits;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Status\Visitor;
use WP_Error;
use WP_REST_Request;
trait WPCOM_REST_API_Proxy_Request {
/**
* Base path for the API.
*
* @var string
*/
protected $base_api_path;
/**
* Version of the API.
*
* @var string
*/
protected $version;
/**
* The base of the controller's route.
*
* @var string
*/
protected $rest_base;
/**
* Proxy request to wpcom servers on behalf of a user or using the Site-level Connection (blog token).
*
* @param WP_REST_Request $request Request to proxy.
* @param string $path Path to append to the rest base.
* @param string $context Whether the request should be proxied on behalf of the current user or using the Site-level Connection, aka 'blog' token. Can be Either 'user' or 'blog'. Defaults to 'user'.
* @param bool $allow_fallback_to_blog If the $context is 'user', whether we should fallback to using the Site-level Connection in case the current user is not connected.
* @param array $request_options Request options to pass to wp_remote_request.
*
* @return mixed|WP_Error Response from wpcom servers or an error.
*/
public function proxy_request_to_wpcom( $request, $path = '', $context = 'user', $allow_fallback_to_blog = false, $request_options = array() ) {
$blog_id = \Jetpack_Options::get_option( 'id' );
$path = '/sites/' . rawurldecode( $blog_id ) . '/' . rawurldecode( ltrim( $this->rest_base, '/' ) ) . ( $path ? '/' . rawurldecode( ltrim( $path, '/' ) ) : '' );
$query_params = $request->get_query_params();
$manager = new Manager();
/*
* A rest_route parameter can be added when using plain permalinks.
* It is not necessary to pass them to WordPress.com,
* and may even cause issues with some endpoints.
* Let's remove it.
*/
if ( isset( $query_params['rest_route'] ) ) {
unset( $query_params['rest_route'] );
}
$api_url = add_query_arg( $query_params, $path );
$request_options = array_replace_recursive(
array(
'headers' => array(
'Content-Type' => 'application/json',
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
),
'method' => $request->get_method(),
),
$request_options
);
// If no body is present, passing it as $request->get_body() will cause an error.
$body = $request->get_body() ? $request->get_body() : null;
$response = new WP_Error(
'rest_unauthorized',
__( 'Please connect your user account to WordPress.com', 'jetpack-connection' ),
array( 'status' => rest_authorization_required_code() )
);
if ( 'user' === $context ) {
if ( ! $manager->is_user_connected() ) {
if ( false === $allow_fallback_to_blog ) {
return $response;
}
$context = 'blog';
} else {
$response = Client::wpcom_json_api_request_as_user( $api_url, $this->version, $request_options, $body, $this->base_api_path );
}
}
if ( 'blog' === $context ) {
if ( ! $manager->is_connected() ) {
return $response;
}
$response = Client::wpcom_json_api_request_as_blog( $api_url, $this->version, $request_options, $body, $this->base_api_path );
}
if ( is_wp_error( $response ) ) {
return $response;
}
$response_status = wp_remote_retrieve_response_code( $response );
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( $response_status >= 400 ) {
$code = $response_body['code'] ?? 'unknown_error';
$message = $response_body['message'] ?? __( 'An unknown error occurred.', 'jetpack-connection' );
return new WP_Error( $code, $message, array( 'status' => $response_status ) );
}
return $response_body;
}
/**
* Proxy request to wpcom servers on behalf of a user.
*
* @param WP_REST_Request $request Request to proxy.
* @param string $path Path to append to the rest base.
* @param array $request_options Request options to pass to wp_remote_request.
*
* @return mixed|WP_Error Response from wpcom servers or an error.
*/
public function proxy_request_to_wpcom_as_user( $request, $path = '', $request_options = array() ) {
return $this->proxy_request_to_wpcom( $request, $path, 'user', false, $request_options );
}
/**
* Proxy request to wpcom servers using the Site-level Connection (blog token).
*
* @param WP_REST_Request $request Request to proxy.
* @param string $path Path to append to the rest base.
* @param array $request_options Request options to pass to wp_remote_request.
*
* @return mixed|WP_Error Response from wpcom servers or an error.
*/
public function proxy_request_to_wpcom_as_blog( $request, $path = '', $request_options = array() ) {
return $this->proxy_request_to_wpcom( $request, $path, 'blog', false, $request_options );
}
}

View File

@ -63,7 +63,7 @@ class Authorize_Redirect {
if ( ! $dest_url || ( 0 === stripos( $dest_url, 'https://jetpack.com/' ) && 0 === stripos( $dest_url, 'https://wordpress.com/' ) ) ) {
// The destination URL is missing or invalid, nothing to do here.
exit;
exit( 0 );
}
// The user is either already connected, or finished the connection process.
@ -73,12 +73,12 @@ class Authorize_Redirect {
}
wp_safe_redirect( $dest_url );
exit;
exit( 0 );
} elseif ( ! empty( $_GET['done'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// The user decided not to proceed with setting up the connection.
wp_safe_redirect( Admin_Menu::get_top_level_menu_item_url() );
exit;
exit( 0 );
}
$redirect_args = array(
@ -94,29 +94,66 @@ class Authorize_Redirect {
}
wp_safe_redirect( $this->build_authorize_url( add_query_arg( $redirect_args, admin_url( 'admin.php' ) ) ) );
exit;
exit( 0 );
}
/**
* Create the Jetpack authorization URL.
*
* @since 2.7.6 Added optional $from and $raw parameters.
* @since 6.8.0 Added optional $provider and $provider_args parameters.
*
* @param bool|string $redirect URL to redirect to.
* @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.
* @param string|null $provider The authentication provider (google, github, apple, link).
* @param array|null $provider_args Additional provider-specific arguments.
*
* @todo Update default value for redirect since the called function expects a string.
*
* @return mixed|void
*/
public function build_authorize_url( $redirect = false, $from = false, $raw = false ) {
public function build_authorize_url( $redirect = false, $from = false, $raw = false, $provider = null, $provider_args = null ) {
add_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) );
add_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) );
$url = $this->connection->get_authorization_url( wp_get_current_user(), $redirect, $from, $raw );
// If a provider is specified, modify the URL to use the provider-specific endpoint
if ( $provider && in_array( $provider, array( 'google', 'github', 'apple', 'link' ), true ) ) {
// Parse the URL to modify it safely
$url_parts = wp_parse_url( $url );
if ( ! empty( $url_parts['host'] ) && ! empty( $url_parts['path'] ) ) {
// Build the new URL using wordpress.com as the host
$url_parts['host'] = 'wordpress.com';
$url_parts['path'] = '/log-in/jetpack/' . $provider;
// Preserve the query parameters
$query_params = array();
if ( ! empty( $url_parts['query'] ) ) {
parse_str( $url_parts['query'], $query_params );
}
// Add magic link specific parameters if provider is 'link'
if ( 'link' === $provider && is_array( $provider_args ) && ! empty( $provider_args['email_address'] ) ) {
$query_params['email_address'] = $provider_args['email_address'];
// Add flag to trigger magic link flow
$query_params['auto_trigger'] = '1';
}
// URL encode all parameter values
$query_params = array_map( 'rawurlencode', $query_params );
// Rebuild the URL
$url = 'https://' . $url_parts['host'] . $url_parts['path'];
if ( ! empty( $query_params ) ) {
$url = add_query_arg( $query_params, $url );
}
}
}
remove_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) );
remove_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) );
@ -125,11 +162,14 @@ class Authorize_Redirect {
*
* @since jetpack-8.9.0
* @since 2.7.6 Added $raw parameter.
* @since 6.8.0 Added $provider and $provider_args parameters.
*
* @param string $url Connection URL.
* @param bool $raw If true, URL will not be escaped.
* @param string $url Connection URL.
* @param bool $raw If true, URL will not be escaped.
* @param string|null $provider The authentication provider if specified.
* @param array|null $provider_args Additional provider-specific arguments.
*/
return apply_filters( 'jetpack_build_authorize_url', $url, $raw );
return apply_filters( 'jetpack_build_authorize_url', $url, $raw, $provider, $provider_args );
}
/**