plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -0,0 +1,281 @@
<?php
/**
* Authorize_Json_Api handler class.
* Used to handle connections via JSON API.
* Ported from the Jetpack class.
*
* @since 2.7.6 Ported from the Jetpack class.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status\Host;
use Jetpack_Options;
/**
* Authorize_Json_Api handler class.
*/
class Authorize_Json_Api {
/**
* Verified data for JSON authorization request
*
* @since 2.7.6
*
* @var array
*/
public $json_api_authorization_request = array();
/**
* Verifies the request by checking the signature
*
* @since jetpack-4.6.0 Method was updated to use `$_REQUEST` instead of `$_GET` and `$_POST`. Method also updated to allow
* passing in an `$environment` argument that overrides `$_REQUEST`. This was useful for integrating with SSO.
* @since 2.7.6 Ported from Jetpack to the Connection package.
*
* @param null|array $environment Value to override $_REQUEST.
*
* @return void
*/
public function verify_json_api_authorization_request( $environment = null ) {
$environment = $environment === null
? $_REQUEST // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verification handled later in function and request data are 1) used to verify a cryptographic signature of the request data and 2) sanitized later in function.
: $environment;
if ( ! isset( $environment['token'] ) ) {
wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'jetpack-connection' ) );
}
list( $env_token,, $env_user_id ) = explode( ':', $environment['token'] );
$token = ( new Tokens() )->get_access_token( (int) $env_user_id, $env_token );
if ( ! $token || empty( $token->secret ) ) {
wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'jetpack-connection' ) );
}
$die_error = __( 'Someone may be trying to trick you into giving them access to your site. Or it could be you just encountered a bug :). Either way, please close this window.', 'jetpack-connection' );
// Host has encoded the request URL, probably as a result of a bad http => https redirect.
if (
preg_match( '/https?%3A%2F%2F/i', esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) > 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no site changes, we're erroring out.
) {
/**
* Jetpack authorisation request Error.
*
* @since jetpack-7.5.0
*/
do_action( 'jetpack_verify_api_authorization_request_error_double_encode' );
$die_error = sprintf(
/* translators: %s is a URL */
__( 'Your site is incorrectly double-encoding redirects from http to https. This is preventing Jetpack from authenticating your connection. Please visit our <a href="%s">support page</a> for details about how to resolve this.', 'jetpack-connection' ),
esc_url( Redirect::get_url( 'jetpack-support-double-encoding' ) )
);
}
$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) Jetpack_Options::get_option( 'time_diff' ) );
if ( isset( $environment['jetpack_json_api_original_query'] ) ) {
$signature = $jetpack_signature->sign_request(
$environment['token'],
$environment['timestamp'],
$environment['nonce'],
'',
'GET',
$environment['jetpack_json_api_original_query'],
null,
true
);
} else {
$signature = $jetpack_signature->sign_current_request(
array(
'body' => null,
'method' => 'GET',
)
);
}
if ( ! $signature ) {
wp_die(
wp_kses(
$die_error,
array(
'a' => array(
'href' => array(),
),
)
)
);
} elseif ( is_wp_error( $signature ) ) {
wp_die(
wp_kses(
$die_error,
array(
'a' => array(
'href' => array(),
),
)
)
);
} elseif ( ! hash_equals( $signature, $environment['signature'] ) ) {
if ( is_ssl() ) {
// If we signed an HTTP request on the Jetpack Servers, but got redirected to HTTPS by the local blog, check the HTTP signature as well.
$signature = $jetpack_signature->sign_current_request(
array(
'scheme' => 'http',
'body' => null,
'method' => 'GET',
)
);
if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $environment['signature'] ) ) {
wp_die(
wp_kses(
$die_error,
array(
'a' => array(
'href' => array(),
),
)
)
);
}
} else {
wp_die(
wp_kses(
$die_error,
array(
'a' => array(
'href' => array(),
),
)
)
);
}
}
$timestamp = (int) $environment['timestamp'];
$nonce = stripslashes( (string) $environment['nonce'] );
if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) {
// De-nonce the nonce, at least for 5 minutes.
// We have to reuse this nonce at least once (used the first time when the initial request is made, used a second time when the login form is POSTed).
$old_nonce_time = get_option( "jetpack_nonce_{$timestamp}_{$nonce}" );
if ( $old_nonce_time < time() - 300 ) {
wp_die( esc_html__( 'The authorization process expired. Please go back and try again.', 'jetpack-connection' ) );
}
}
$data = json_decode(
base64_decode( stripslashes( $environment['data'] ) ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
);
$data_filters = array(
'state' => 'opaque',
'client_id' => 'int',
'client_title' => 'string',
'client_image' => 'url',
);
foreach ( $data_filters as $key => $sanitation ) {
if ( ! isset( $data->$key ) ) {
wp_die(
wp_kses(
$die_error,
array(
'a' => array(
'href' => array(),
),
)
)
);
}
switch ( $sanitation ) {
case 'int':
$this->json_api_authorization_request[ $key ] = (int) $data->$key;
break;
case 'opaque':
$this->json_api_authorization_request[ $key ] = (string) $data->$key;
break;
case 'string':
$this->json_api_authorization_request[ $key ] = wp_kses( (string) $data->$key, array() );
break;
case 'url':
$this->json_api_authorization_request[ $key ] = esc_url_raw( (string) $data->$key );
break;
}
}
if ( empty( $this->json_api_authorization_request['client_id'] ) ) {
wp_die(
wp_kses(
$die_error,
array(
'a' => array(
'href' => array(),
),
)
)
);
}
}
/**
* Add the Access Code details to the public-api.wordpress.com redirect.
*
* @since 2.7.6 Ported from Jetpack to the Connection package.
*
* @param string $redirect_to URL.
* @param string $original_redirect_to URL.
* @param \WP_User $user WP_User for the redirect.
*
* @return string
*/
public function add_token_to_login_redirect_json_api_authorization( $redirect_to, $original_redirect_to, $user ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return add_query_arg(
urlencode_deep(
array(
'jetpack-code' => get_user_meta(
$user->ID,
'jetpack_json_api_' . $this->json_api_authorization_request['client_id'],
true
),
'jetpack-user-id' => (int) $user->ID,
'jetpack-state' => $this->json_api_authorization_request['state'],
)
),
$redirect_to
);
}
/**
* If someone logs in to approve API access, store the Access Code in usermeta.
*
* @since 2.7.6 Ported from Jetpack to the Connection package.
*
* @param string $user_login Unused.
* @param \WP_User $user User logged in.
*
* @return void
*/
public function store_json_api_authorization_token( $user_login, $user ) {
add_filter( 'login_redirect', array( $this, 'add_token_to_login_redirect_json_api_authorization' ), 10, 3 );
add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_public_api_domain' ) );
$token = wp_generate_password( 32, false );
update_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], $token );
}
/**
* HTML for the JSON API authorization notice.
*
* @since 2.7.6 Ported from Jetpack to the Connection package.
*
* @return string
*/
public function login_message_json_api_authorization() {
return '<p class="message">' . sprintf(
/* translators: Name/image of the client requesting authorization */
esc_html__( '%s wants to access your sites data. Log in to authorize that access.', 'jetpack-connection' ),
'<strong>' . esc_html( $this->json_api_authorization_request['client_title'] ) . '</strong>'
) . '<img src="' . esc_url( $this->json_api_authorization_request['client_image'] ) . '" /></p>';
}
}

View File

@@ -8,6 +8,10 @@
namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\Constants;
use WP_Error;
// `wp_remote_request` returns an array with a particular format.
'@phan-type _WP_Remote_Response_Array = array{headers:\WpOrg\Requests\Utility\CaseInsensitiveDictionary,body:string,response:array{code:int,message:string},cookies:\WP_HTTP_Cookie[],filename:?string,http_response:WP_HTTP_Requests_Response}';
/**
* The Client class that is used to connect to WordPress.com Jetpack API.
@@ -18,9 +22,10 @@ class Client {
/**
* Makes an authorized remote request using Jetpack_Signature
*
* @param array $args the arguments for the remote request.
* @param array|String $body the request body.
* @param array $args the arguments for the remote request.
* @param array|string|null $body the request body.
* @return array|WP_Error WP HTTP response on success
* @phan-return _WP_Remote_Response_Array|WP_Error
*/
public static function remote_request( $args, $body = null ) {
if ( isset( $args['url'] ) ) {
@@ -35,7 +40,7 @@ class Client {
}
$result = self::build_signed_request( $args, $body );
if ( ! $result || is_wp_error( $result ) ) {
if ( is_wp_error( $result ) ) {
return $result;
}
@@ -64,12 +69,12 @@ class Client {
/**
* Adds authorization signature to a remote request using Jetpack_Signature
*
* @param array $args the arguments for the remote request.
* @param array|String $body the request body.
* @return WP_Error|array {
* @param array $args the arguments for the remote request.
* @param array|string|null $body the request body.
* @return WP_Error|array{url:string,request:array,auth:array} {
* An array containing URL and request items.
*
* @type String $url The request URL.
* @type string $url The request URL.
* @type array $request Request arguments.
* @type array $auth Authorization data.
* }
@@ -88,6 +93,7 @@ class Client {
'blog_id' => 0,
'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ),
'method' => 'POST',
'format' => 'json',
'timeout' => 10,
'redirection' => 0,
'headers' => array(),
@@ -106,7 +112,7 @@ class Client {
$token = ( new Tokens() )->get_access_token( $args['user_id'] );
if ( ! $token ) {
return new \WP_Error( 'missing_token' );
return new WP_Error( 'missing_token' );
}
$method = strtoupper( $args['method'] );
@@ -121,8 +127,8 @@ class Client {
$request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
@list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
if ( empty( $token ) || empty( $secret ) ) {
return new \WP_Error( 'malformed_token' );
if ( ! $secret ) {
return new WP_Error( 'malformed_token' );
}
$token_key = sprintf(
@@ -140,7 +146,7 @@ class Client {
if ( function_exists( 'wp_generate_password' ) ) {
$nonce = wp_generate_password( 10, false );
} else {
$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
$nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
}
// Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing.
@@ -151,20 +157,22 @@ class Client {
// Allow arrays to be used in passing data.
$body_to_hash = $body;
if ( is_array( $body ) ) {
if ( $args['format'] === 'jsonl' ) {
parse_str( $body, $body_to_hash );
}
if ( is_array( $body_to_hash ) ) {
// We cast this to a new variable, because the array form of $body needs to be
// maintained so it can be passed into the request later on in the code.
if ( array() !== $body ) {
$body_to_hash = wp_json_encode( self::_stringify_data( $body ) );
if ( array() !== $body_to_hash ) {
$body_to_hash = wp_json_encode( self::_stringify_data( $body_to_hash ) );
} else {
$body_to_hash = '';
}
}
if ( ! is_string( $body_to_hash ) ) {
return new \WP_Error( 'invalid_body', 'Body is malformed.' );
return new WP_Error( 'invalid_body', 'Body is malformed.' );
}
$body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
@@ -192,7 +200,7 @@ class Client {
$signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
if ( ! $signature || is_wp_error( $signature ) ) {
if ( is_wp_error( $signature ) ) {
return $signature;
}
@@ -229,10 +237,11 @@ class Client {
*
* @internal
*
* @param String $url the request URL.
* @param string $url the request URL.
* @param array $args request arguments.
* @param Boolean $set_fallback whether to allow flagging this request to use a fallback certficate override.
* @param boolean $set_fallback whether to allow flagging this request to use a fallback certficate override.
* @return array|WP_Error WP HTTP response on success
* @phan-return _WP_Remote_Response_Array|WP_Error
*/
public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
$fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
@@ -312,8 +321,9 @@ class Client {
/**
* Sets the time difference for correct signature computation.
*
* @param HTTP_Response $response the response object.
* @param Boolean $force_set whether to force setting the time difference.
* @param array|WP_Error $response Response array from `wp_remote_request`, or WP_Error on error.
* @param bool $force_set whether to force setting the time difference.
* @phan-param _WP_Remote_Response_Array|WP_Error $response
*/
public static function set_time_diff( &$response, $force_set = false ) {
$code = wp_remote_retrieve_response_code( $response );
@@ -353,7 +363,7 @@ class Client {
* @param array $args Arguments to {@see WP_Http}. Default is `array()`.
* @param string $base_api_path REST API root. Default is `wpcom`.
*
* @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
* @return array Validated arguments.
*/
public static function validate_args_for_wpcom_json_api_request(
$path,
@@ -370,6 +380,7 @@ class Client {
array(
'headers' => 'array',
'method' => 'string',
'format' => 'string',
'timeout' => 'int',
'redirection' => 'int',
'stream' => 'boolean',
@@ -403,13 +414,14 @@ class Client {
/**
* Queries the WordPress.com REST API with a user token.
*
* @param string $path REST API path.
* @param string $version REST API version. Default is `2`.
* @param array $args Arguments to {@see WP_Http}. Default is `array()`.
* @param string $body Body passed to {@see WP_Http}. Default is `null`.
* @param string $base_api_path REST API root. Default is `wpcom`.
* @param string $path REST API path.
* @param string $version REST API version. Default is `2`.
* @param array $args Arguments to {@see WP_Http}. Default is `array()`.
* @param null|string|array $body Body passed to {@see WP_Http}. Default is `null`.
* @param string $base_api_path REST API root. Default is `wpcom`.
*
* @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
* @phan-return _WP_Remote_Response_Array|WP_Error
*/
public static function wpcom_json_api_request_as_user(
$path,
@@ -435,12 +447,13 @@ class Client {
/**
* Query the WordPress.com REST API using the blog token
*
* @param String $path The API endpoint relative path.
* @param String $version The API version.
* @param array $args Request arguments.
* @param String $body Request body.
* @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
* @param string $path The API endpoint relative path.
* @param string $version The API version.
* @param array $args Request arguments.
* @param array|string|null $body Request body.
* @param string $base_api_path (optional) the API base path override, defaults to 'rest'.
* @return array|WP_Error $response Data.
* @phan-return _WP_Remote_Response_Array|WP_Error
*/
public static function wpcom_json_api_request_as_blog(
$path,
@@ -467,7 +480,7 @@ class Client {
* make sure that body hashes are made ith the string version, which is what will be seen after a
* server pulls up the data in the $_POST array.
*
* @param array|Mixed $data the data that needs to be stringified.
* @param mixed $data the data that needs to be stringified.
*
* @return array|string
*/

View File

@@ -702,11 +702,15 @@ class Error_Handler {
return;
}
?>
<div class="notice notice-error is-dismissible jetpack-message jp-connect" style="display:block !important;">
<p><?php echo esc_html( $message ); ?></p>
</div>
<?php
wp_admin_notice(
esc_html( $message ),
array(
'type' => 'error',
'dismissible' => true,
'additional_classes' => array( 'jetpack-message', 'jp-connect' ),
'attributes' => array( 'style' => 'display:block !important;' ),
)
);
}
/**

View File

@@ -7,8 +7,13 @@
namespace Automattic\Jetpack;
use Automattic\Jetpack\Connection\Rest_Authentication;
use Automattic\Jetpack\Connection\REST_Connector;
use Jetpack_Options;
use WP_CLI;
use WP_Error;
use WP_REST_Request;
use WP_REST_Server;
/**
* Heartbeat sends a batch of stats to wp.com once a day
@@ -73,6 +78,8 @@ class Heartbeat {
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'jetpack-heartbeat', array( $this, 'cli_callback' ) );
}
add_action( 'rest_api_init', array( $this, 'initialize_rest_api' ) );
}
/**
@@ -249,4 +256,55 @@ class Heartbeat {
WP_CLI::line( sprintf( __( 'Last heartbeat sent at: %s', 'jetpack-connection' ), $last_date ) );
}
}
/**
* Initialize the heartbeat REST API.
*
* @return void
*/
public function initialize_rest_api() {
register_rest_route(
'jetpack/v4',
'/heartbeat/data',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'rest_heartbeat_data' ),
'permission_callback' => array( $this, 'rest_heartbeat_data_permission_check' ),
'args' => array(
'prefix' => array(
'description' => __( 'Prefix to add before the stats identifiers.', 'jetpack-connection' ),
'type' => 'string',
),
),
)
);
}
/**
* Endpoint to retrieve the heartbeat data.
*
* @param WP_REST_Request $request The request data.
*
* @since 2.7.0
*
* @return array
*/
public function rest_heartbeat_data( WP_REST_Request $request ) {
return static::generate_stats_array( $request->get_param( 'prefix' ) );
}
/**
* Check permissions for the `get_heartbeat_data` endpoint.
*
* @return true|WP_Error
*/
public function rest_heartbeat_data_permission_check() {
if ( current_user_can( 'jetpack_connect' ) ) {
return true;
}
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_heartbeat_data', REST_Connector::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
}

View File

@@ -10,13 +10,16 @@ namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\A8c_Mc_Stats;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Heartbeat;
use Automattic\Jetpack\Partner;
use Automattic\Jetpack\Roles;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use IXR_Error;
use Jetpack_IXR_Client;
use Jetpack_Options;
use Jetpack_XMLRPC_Server;
use WP_Error;
use WP_User;
@@ -70,6 +73,13 @@ class Manager {
*/
private static $extra_register_params = array();
/**
* We store ID's of users already disconnected to prevent multiple disconnect requests.
*
* @var array
*/
private static $disconnected_users = array();
/**
* Initialize the object.
* Make sure to call the "Configure" first.
@@ -101,7 +111,7 @@ class Manager {
);
$manager->setup_xmlrpc_handlers(
$_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
null,
$manager->has_connected_owner(),
$manager->verify_xml_rpc_signature()
);
@@ -126,6 +136,10 @@ class Manager {
Webhooks::init( $manager );
// Unlink user before deleting the user from WP.com.
add_action( 'deleted_user', array( $manager, 'disconnect_user_force' ), 9, 1 );
add_action( 'remove_user_from_blog', array( $manager, 'disconnect_user_force' ), 9, 1 );
// Set up package version hook.
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
@@ -138,38 +152,40 @@ class Manager {
// Initialize token locks.
new Tokens_Locks();
// Initial Partner management.
Partner::init();
}
/**
* Sets up the XMLRPC request handlers.
*
* @since 1.25.0 Deprecate $is_active param.
* @since 2.8.4 Deprecate $request_params param.
*
* @param array $request_params incoming request parameters.
* @param bool $has_connected_owner Whether the site has a connected owner.
* @param bool $is_signed whether the signature check has been successful.
* @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
* @param array|null $deprecated Deprecated. Not used.
* @param bool $has_connected_owner Whether the site has a connected owner.
* @param bool $is_signed whether the signature check has been successful.
* @param Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
*/
public function setup_xmlrpc_handlers(
$request_params,
$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 (
! isset( $request_params['for'] )
|| 'jetpack' !== $request_params['for']
) {
if ( $deprecated !== null ) {
_deprecated_argument( __METHOD__, '2.8.4' );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are using the 'for' request param to early return unless it's 'jetpack'.
if ( ! isset( $_GET['for'] ) || 'jetpack' !== $_GET['for'] ) {
return false;
}
// Alternate XML-RPC, via ?for=jetpack&jetpack=comms.
if (
isset( $request_params['jetpack'] )
&& 'comms' === $request_params['jetpack']
) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This just determines whether to handle the request as an XML-RPC request. The actual XML-RPC endpoints do the appropriate nonce checking where applicable. Plus we make sure to clear all cookies via require_jetpack_authentication called later in method.
if ( isset( $_GET['jetpack'] ) && 'comms' === $_GET['jetpack'] ) {
if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) {
// Use the real constant here for WordPress' sake.
define( 'XMLRPC_REQUEST', true );
@@ -183,13 +199,16 @@ class Manager {
if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) {
return false;
}
// Display errors can cause the XML to be not well formed.
// This only affects Jetpack XML-RPC endpoints received from WordPress.com servers.
// All other XML-RPC requests are unaffected.
@ini_set( 'display_errors', false ); // phpcs:ignore
if ( $xmlrpc_server ) {
$this->xmlrpc_server = $xmlrpc_server;
} else {
$this->xmlrpc_server = new \Jetpack_XMLRPC_Server();
$this->xmlrpc_server = new Jetpack_XMLRPC_Server();
}
$this->require_jetpack_authentication();
@@ -235,6 +254,8 @@ class Manager {
* from /xmlrpc.php so that we're replicating it as closely as possible.
*
* @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things.
*
* @return never
*/
public function alternate_xmlrpc() {
// Some browser-embedded clients send cookies. We don't want them.
@@ -274,7 +295,7 @@ class Manager {
$jetpack_methods = array();
foreach ( $methods as $method => $callback ) {
if ( 0 === strpos( $method, 'jetpack.' ) ) {
if ( str_starts_with( $method, 'jetpack.' ) ) {
$jetpack_methods[ $method ] = $callback;
}
}
@@ -436,12 +457,12 @@ class Manager {
}
$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) );
// phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Used to verify a cryptographic signature of the post data. Also a nonce is verified later in the function.
if ( isset( $_POST['_jetpack_is_multipart'] ) ) {
$post_data = $_POST;
$post_data = $_POST; // We need all of $_POST in order to verify a cryptographic signature of the post data.
$file_hashes = array();
foreach ( $post_data as $post_data_key => $post_data_value ) {
if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) {
if ( ! str_starts_with( $post_data_key, '_jetpack_file_hmac_' ) ) {
continue;
}
$post_data_key = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) );
@@ -857,6 +878,22 @@ class Manager {
exit();
}
/**
* Force user disconnect.
*
* @param int $user_id Local (external) user ID.
*
* @return bool
*/
public function disconnect_user_force( $user_id ) {
if ( ! (int) $user_id ) {
// Missing user ID.
return false;
}
return $this->disconnect_user( $user_id, true, true );
}
/**
* Unlinks the current user from the linked WordPress.com user.
*
@@ -878,6 +915,11 @@ class Manager {
return false;
}
if ( in_array( $user_id, self::$disconnected_users, true ) ) {
// The user is already disconnected.
return false;
}
// Attempt to disconnect the user from WordPress.com.
$is_disconnected_from_wpcom = $this->unlink_user_from_wpcom( $user_id );
@@ -907,6 +949,8 @@ class Manager {
}
}
self::$disconnected_users[] = $user_id;
return $is_disconnected_from_wpcom && $is_disconnected_locally;
}
@@ -1188,7 +1232,7 @@ class Manager {
$jetpack_public = false;
}
\Jetpack_Options::update_options(
Jetpack_Options::update_options(
array(
'id' => (int) $registration_details->jetpack_id,
'public' => $jetpack_public,
@@ -1199,6 +1243,13 @@ class Manager {
$this->get_tokens()->update_blog_token( (string) $registration_details->jetpack_secret );
if ( ! Jetpack_Options::get_option( 'id' ) || ! $this->get_tokens()->get_access_token() ) {
return new WP_Error(
'connection_data_save_failed',
'Failed to save connection data in the database'
);
}
$alternate_authorization_url = isset( $registration_details->alternate_authorization_url ) ? $registration_details->alternate_authorization_url : '';
add_filter(
@@ -1829,11 +1880,16 @@ class Manager {
/**
* 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.
* @since 2.7.6 Added optional $from and $raw parameters.
*
* @param WP_User|null $user (optional) defaults to the current logged in user.
* @param string|null $redirect (optional) a redirect URL to use instead of the default.
* @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
* @param bool $raw If true, URL will not be escaped.
*
* @return string Connect URL.
*/
public function get_authorization_url( $user = null, $redirect = null ) {
public function get_authorization_url( $user = null, $redirect = null, $from = false, $raw = false ) {
if ( empty( $user ) ) {
$user = wp_get_current_user();
}
@@ -1924,7 +1980,30 @@ class Manager {
$api_url = $this->api_url( 'authorize' );
return add_query_arg( $body, $api_url );
$url = add_query_arg( $body, $api_url );
if ( is_network_admin() ) {
$url = add_query_arg( 'is_multisite', network_admin_url( 'admin.php?page=jetpack-settings' ), $url );
}
if ( $from ) {
$url = add_query_arg( 'from', $from, $url );
}
if ( $raw ) {
$url = esc_url_raw( $url );
}
/**
* Filter the URL used when connecting a user to a WordPress.com account.
*
* @since 2.0.0
* @since 2.7.6 Added $raw parameter.
*
* @param string $url Connection URL.
* @param bool $raw If true, URL will not be escaped.
*/
return apply_filters( 'jetpack_build_authorize_url', $url, $raw );
}
/**
@@ -2047,7 +2126,7 @@ class Manager {
( new Nonce_Handler() )->clean_all();
/**
* Fires when a site is disconnected.
* Fires before a site is disconnected.
*
* @since 1.36.3
*/
@@ -2271,7 +2350,7 @@ class Manager {
* Handles a getOptions XMLRPC method call.
*
* @param array $args method call arguments.
* @return an amended XMLRPC server options array.
* @return array|IXR_Error An amended XMLRPC server options array.
*/
public function jetpack_get_options( $args ) {
global $wp_xmlrpc_server;
@@ -2545,17 +2624,21 @@ class Manager {
/**
* Get the WPCOM or self-hosted site ID.
*
* @return int|WP_Error
* @param bool $quiet Return null instead of an error.
*
* @return int|WP_Error|null
*/
public static function get_site_id() {
public static function get_site_id( $quiet = false ) {
$is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM );
$site_id = $is_wpcom ? get_current_blog_id() : \Jetpack_Options::get_option( 'id' );
if ( ! $site_id ) {
return new \WP_Error(
'unavailable_site_id',
__( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ),
403
);
return $quiet
? null
: new \WP_Error(
'unavailable_site_id',
__( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ),
403
);
}
return (int) $site_id;
}

View File

@@ -30,11 +30,31 @@ class Package_Version_Tracker {
*/
const CACHED_FAILED_REQUEST_EXPIRATION = 1 * HOUR_IN_SECONDS;
/**
* Transient key for rate limiting the package version requests;
*/
const RATE_LIMITER_KEY = 'jetpack_update_remote_package_last_query';
/**
* Only allow one versions check (and request) per minute.
*/
const RATE_LIMITER_TIMEOUT = MINUTE_IN_SECONDS;
/**
* Uses the jetpack_package_versions filter to obtain the package versions from packages that need
* version tracking. If the package versions have changed, updates the option and notifies WPCOM.
*/
public function maybe_update_package_versions() {
// Do not run too early or all the modules may not be loaded.
if ( ! did_action( 'init' ) ) {
return;
}
// The version check is being rate limited.
if ( $this->is_rate_limiting() ) {
return;
}
/**
* Obtains the package versions.
*
@@ -65,13 +85,43 @@ class Package_Version_Tracker {
}
/**
* Updates the package versions:
* Updates the package versions option.
*
* @param array $package_versions The package versions.
*/
protected function update_package_versions_option( $package_versions ) {
if ( ! $this->is_sync_enabled() ) {
$this->update_package_versions_via_remote_request( $package_versions );
return;
}
update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
}
/**
* Whether Jetpack Sync is enabled.
*
* @return boolean true if Sync is present and enabled, false otherwise
*/
protected function is_sync_enabled() {
if ( class_exists( 'Automattic\Jetpack\Sync\Settings' ) && \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {
return true;
}
return false;
}
/**
* Fallback for updating the package versions via a remote request when Sync is not present.
*
* Updates the package versions as follows:
* - Sends the updated package versions to wpcom.
* - Updates the 'jetpack_package_versions' option.
*
* @param array $package_versions The package versions.
*/
protected function update_package_versions_option( $package_versions ) {
protected function update_package_versions_via_remote_request( $package_versions ) {
$connection = new Manager();
if ( ! $connection->is_connected() ) {
return;
@@ -108,4 +158,19 @@ class Package_Version_Tracker {
set_transient( self::CACHED_FAILED_REQUEST_KEY, time(), self::CACHED_FAILED_REQUEST_EXPIRATION );
}
}
/**
* Check if version check is being rate limited, and update the rate limiting transient if needed.
*
* @return bool
*/
private function is_rate_limiting() {
if ( get_transient( static::RATE_LIMITER_KEY ) ) {
return true;
}
set_transient( static::RATE_LIMITER_KEY, time(), static::RATE_LIMITER_TIMEOUT );
return false;
}
}

View File

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

View File

@@ -0,0 +1,466 @@
<?php
/**
* Class for the Jetpack partner coupon logic.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack;
use Automattic\Jetpack\Connection\Client as Connection_Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack_Options;
/**
* Disable direct access.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Jetpack_Partner_Coupon
*
* @since partner-1.6.0
* @since 2.0.0
*/
class Partner_Coupon {
/**
* Name of the Jetpack_Option coupon option.
*
* @var string
*/
public static $coupon_option = 'partner_coupon';
/**
* Name of the Jetpack_Option added option.
*
* @var string
*/
public static $added_option = 'partner_coupon_added';
/**
* Name of "last availability check" transient.
*
* @var string
*/
public static $last_check_transient = 'jetpack_partner_coupon_last_check';
/**
* Callable that executes a blog-authenticated request.
*
* @var callable
*/
protected $request_as_blog;
/**
* Jetpack_Partner_Coupon
*
* @var Partner_Coupon|null
**/
private static $instance = null;
/**
* A list of supported partners.
*
* @var array
*/
private static $supported_partners = array(
'IONOS' => array(
'name' => 'IONOS',
'logo' => array(
'src' => '/images/ionos-logo.jpg',
'width' => 119,
'height' => 32,
),
),
);
/**
* A list of supported presets.
*
* @var array
*/
private static $supported_presets = array(
'IONA' => 'jetpack_backup_daily',
);
/**
* Get singleton instance of class.
*
* @return Partner_Coupon
*/
public static function get_instance() {
if ( self::$instance === null ) {
self::$instance = new Partner_Coupon( array( Connection_Client::class, 'wpcom_json_api_request_as_blog' ) );
}
return self::$instance;
}
/**
* Constructor.
*
* @param callable $request_as_blog Callable that executes a blog-authenticated request.
*/
public function __construct( $request_as_blog ) {
$this->request_as_blog = $request_as_blog;
}
/**
* Register hooks to catch and purge coupon.
*
* @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
* @param string $redirect_location The location we should redirect to after catching the coupon.
*/
public static function register_coupon_admin_hooks( $plugin_slug, $redirect_location ) {
$instance = self::get_instance();
// We have to use an anonymous function, so we can pass along relevant information
// and not have to hardcode values for a single plugin.
// This open up the opportunity for e.g. the "all-in-one" and backup plugins
// to both implement partner coupon logic.
add_action(
'admin_init',
function () use ( $plugin_slug, $redirect_location, $instance ) {
$instance->catch_coupon( $plugin_slug, $redirect_location );
$instance->maybe_purge_coupon( $plugin_slug );
}
);
}
/**
* Catch partner coupon and redirect to claim component.
*
* @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
* @param string $redirect_location The location we should redirect to after catching the coupon.
*/
public function catch_coupon( $plugin_slug, $redirect_location ) {
// Accept and store a partner coupon if present, and redirect to Jetpack connection screen.
$partner_coupon = isset( $_GET['jetpack-partner-coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack-partner-coupon'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $partner_coupon ) {
Jetpack_Options::update_options(
array(
self::$coupon_option => $partner_coupon,
self::$added_option => time(),
)
);
$connection = new Connection_Manager( $plugin_slug );
if ( $connection->is_connected() ) {
$redirect_location = add_query_arg( array( 'showCouponRedemption' => 1 ), $redirect_location );
wp_safe_redirect( $redirect_location );
} else {
wp_safe_redirect( $redirect_location );
}
}
}
/**
* Purge partner coupon.
*
* We try to remotely check if a coupon looks valid. We also automatically purge
* partner coupons after a certain amount of time to prevent unnecessary look-ups
* and/or promoting a product for months or years in the future due to unknown
* errors.
*
* @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
*/
public function maybe_purge_coupon( $plugin_slug ) {
// Only run coupon checks on Jetpack admin pages.
// The "admin-ui" package is responsible for registering the Jetpack admin
// page for all Jetpack plugins and has hardcoded the settings page to be
// "jetpack", so we shouldn't need to allow for dynamic/custom values.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] ) {
return;
}
if ( ( new Status() )->is_offline_mode() ) {
return;
}
$connection = new Connection_Manager( $plugin_slug );
if ( ! $connection->is_connected() ) {
return;
}
if ( $this->maybe_purge_coupon_by_added_date() ) {
return;
}
// Limit checks to happen once a minute at most.
if ( get_transient( self::$last_check_transient ) ) {
return;
}
set_transient( self::$last_check_transient, true, MINUTE_IN_SECONDS );
$this->maybe_purge_coupon_by_availability_check();
}
/**
* Purge coupon based on local added date.
*
* We automatically remove the coupon after a month to "self-heal" if
* something in the claim process has broken with the site.
*
* @return bool Return whether we should skip further purge checks.
*/
protected function maybe_purge_coupon_by_added_date() {
$date = Jetpack_Options::get_option( self::$added_option, '' );
if ( empty( $date ) ) {
return true;
}
$expire_date = strtotime( '+30 days', $date );
$today = time();
if ( $today >= $expire_date ) {
$this->delete_coupon_data();
return true;
}
return false;
}
/**
* Purge coupon based on availability check.
*
* @return bool Return whether we deleted coupon data.
*/
protected function maybe_purge_coupon_by_availability_check() {
$blog_id = Jetpack_Options::get_option( 'id', false );
if ( ! $blog_id ) {
return false;
}
$coupon = self::get_coupon();
if ( ! $coupon ) {
return false;
}
$response = call_user_func_array(
$this->request_as_blog,
array(
add_query_arg(
array( 'coupon_code' => $coupon['coupon_code'] ),
sprintf(
'/sites/%d/jetpack-partner/coupon/v1/site/coupon',
$blog_id
)
),
2,
array( 'method' => 'GET' ),
null,
'wpcom',
)
);
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if (
200 === wp_remote_retrieve_response_code( $response ) &&
is_array( $body ) &&
isset( $body['available'] ) &&
false === $body['available']
) {
$this->delete_coupon_data();
return true;
}
return false;
}
/**
* Delete all coupon data.
*/
protected function delete_coupon_data() {
Jetpack_Options::delete_option(
array(
self::$coupon_option,
self::$added_option,
)
);
}
/**
* Get partner coupon data.
*
* @return array|bool
*/
public static function get_coupon() {
$coupon_code = Jetpack_Options::get_option( self::$coupon_option, '' );
if ( ! is_string( $coupon_code ) || empty( $coupon_code ) ) {
return false;
}
$instance = self::get_instance();
$partner = $instance->get_coupon_partner( $coupon_code );
if ( ! $partner ) {
return false;
}
$preset = $instance->get_coupon_preset( $coupon_code );
if ( ! $preset ) {
return false;
}
$product = $instance->get_coupon_product( $preset );
if ( ! $product ) {
return false;
}
return array(
'coupon_code' => $coupon_code,
'partner' => $partner,
'preset' => $preset,
'product' => $product,
);
}
/**
* Get coupon partner.
*
* @param string $coupon_code Coupon code to go through.
* @return array|bool
*/
private function get_coupon_partner( $coupon_code ) {
if ( ! is_string( $coupon_code ) || false === strpos( $coupon_code, '_' ) ) {
return false;
}
$prefix = strtok( $coupon_code, '_' );
$supported_partners = $this->get_supported_partners();
if ( ! isset( $supported_partners[ $prefix ] ) ) {
return false;
}
return array(
'name' => $supported_partners[ $prefix ]['name'],
'prefix' => $prefix,
'logo' => isset( $supported_partners[ $prefix ]['logo'] ) ? $supported_partners[ $prefix ]['logo'] : null,
);
}
/**
* Get coupon product.
*
* @param string $coupon_preset The preset we wish to find a product for.
* @return array|bool
*/
private function get_coupon_product( $coupon_preset ) {
if ( ! is_string( $coupon_preset ) ) {
return false;
}
/**
* Allow for plugins to register supported products.
*
* @since 1.6.0
*
* @param array A list of product details.
* @return array
*/
$product_details = apply_filters( 'jetpack_partner_coupon_products', array() );
$product_slug = $this->get_supported_presets()[ $coupon_preset ];
foreach ( $product_details as $product ) {
if ( ! $this->array_keys_exist( array( 'title', 'slug', 'description', 'features' ), $product ) ) {
continue;
}
if ( $product_slug === $product['slug'] ) {
return $product;
}
}
return false;
}
/**
* Checks if multiple keys are present in an array.
*
* @param array $needles The keys we wish to check for.
* @param array $haystack The array we want to compare keys against.
*
* @return bool
*/
private function array_keys_exist( $needles, $haystack ) {
foreach ( $needles as $needle ) {
if ( ! isset( $haystack[ $needle ] ) ) {
return false;
}
}
return true;
}
/**
* Get coupon preset.
*
* @param string $coupon_code Coupon code to go through.
* @return string|bool
*/
private function get_coupon_preset( $coupon_code ) {
if ( ! is_string( $coupon_code ) ) {
return false;
}
$regex = '/^.*?_(?P<slug>.*?)_.+$/';
$matches = array();
if ( ! preg_match( $regex, $coupon_code, $matches ) ) {
return false;
}
return isset( $this->get_supported_presets()[ $matches['slug'] ] ) ? $matches['slug'] : false;
}
/**
* Get supported partners.
*
* @return array
*/
private function get_supported_partners() {
/**
* Allow external code to add additional supported partners.
*
* @since partner-1.6.0
* @since 2.0.0
*
* @param array $supported_partners A list of supported partners.
* @return array
*/
return apply_filters( 'jetpack_partner_coupon_supported_partners', self::$supported_partners );
}
/**
* Get supported presets.
*
* @return array
*/
private function get_supported_presets() {
/**
* Allow external code to add additional supported presets.
*
* @since partner-1.6.0
* @since 2.0.0
*
* @param array $supported_presets A list of supported presets.
* @return array
*/
return apply_filters( 'jetpack_partner_coupon_supported_presets', self::$supported_presets );
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* Jetpack Partner utilities.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack;
/**
* This class introduces functionality used by Jetpack hosting partners.
*
* @since partner-1.0.0
* @since 2.0.0
*/
class Partner {
/**
* Affiliate code.
*/
const AFFILIATE_CODE = 'affiliate';
/**
* Subsidiary id code.
*/
const SUBSIDIARY_CODE = 'subsidiary';
/**
* Singleton instance.
*
* @since partner-1.0.0
* @since 2.0.0
*
* @var Partner This class instance.
*/
private static $instance = null;
/**
* Partner constructor.
*/
private function __construct() {
}
/**
* Initializes the class or returns the singleton.
*
* @since partner-1.0.0
* @since 2.0.0
*
* @return Partner | false
*/
public static function init() {
if ( self::$instance === null ) {
self::$instance = new Partner();
add_filter( 'jetpack_build_authorize_url', array( self::$instance, 'add_subsidiary_id_as_query_arg' ) );
add_filter( 'jetpack_build_authorize_url', array( self::$instance, 'add_affiliate_code_as_query_arg' ) );
add_filter( 'jetpack_build_connection_url', array( self::$instance, 'add_subsidiary_id_as_query_arg' ) );
add_filter( 'jetpack_build_connection_url', array( self::$instance, 'add_affiliate_code_as_query_arg' ) );
add_filter( 'jetpack_register_request_body', array( self::$instance, 'add_subsidiary_id_to_params_array' ) );
add_filter( 'jetpack_register_request_body', array( self::$instance, 'add_affiliate_code_to_params_array' ) );
}
return self::$instance;
}
/**
* Adds the partner subsidiary code to the passed URL.
*
* @param string $url The URL.
*
* @return string
*/
public function add_subsidiary_id_as_query_arg( $url ) {
return $this->add_code_as_query_arg( self::SUBSIDIARY_CODE, $url );
}
/**
* Adds the affiliate code to the passed URL.
*
* @param string $url The URL.
*
* @return string
*/
public function add_affiliate_code_as_query_arg( $url ) {
return $this->add_code_as_query_arg( self::AFFILIATE_CODE, $url );
}
/**
* Adds the partner subsidiary code to the passed array.
*
* @since partner-1.5.0
* @since 2.0.0
*
* @param array $params The parameters array.
*
* @return array
*/
public function add_subsidiary_id_to_params_array( $params ) {
if ( ! is_array( $params ) ) {
return $params;
}
return array_merge( $params, $this->get_code_as_array( self::SUBSIDIARY_CODE ) );
}
/**
* Adds the affiliate code to the passed array.
*
* @since partner-1.5.0
* @since 2.0.0
*
* @param array $params The parameters array.
*
* @return array
*/
public function add_affiliate_code_to_params_array( $params ) {
if ( ! is_array( $params ) ) {
return $params;
}
return array_merge( $params, $this->get_code_as_array( self::AFFILIATE_CODE ) );
}
/**
* Returns the passed URL with the partner code added as a URL query arg.
*
* @since partner-1.0.0
* @since 2.0.0
*
* @param string $type The partner code.
* @param string $url The URL where the partner subsidiary id will be added.
*
* @return string The passed URL with the partner code added.
*/
public function add_code_as_query_arg( $type, $url ) {
return add_query_arg( $this->get_code_as_array( $type ), $url );
}
/**
* Gets the partner code in an associative array format
*
* @since partner-1.5.0
* @since 2.0.0
*
* @param string $type The partner code.
* @return array
*/
private function get_code_as_array( $type ) {
switch ( $type ) {
case self::AFFILIATE_CODE:
$query_arg_name = 'aff';
break;
case self::SUBSIDIARY_CODE:
$query_arg_name = 'subsidiaryId';
break;
default:
return array();
}
$code = $this->get_partner_code( $type );
if ( '' === $code ) {
return array();
}
return array( $query_arg_name => $code );
}
/**
* Returns a partner code.
*
* @since partner-1.0.0
* @since 2.0.0
*
* @param string $type This can be either 'affiliate' or 'subsidiary'. Returns empty string when code is unknown.
*
* @return string The partner code.
*/
public function get_partner_code( $type ) {
switch ( $type ) {
case self::AFFILIATE_CODE:
/**
* Allow to filter the affiliate code.
*
* @since partner-1.0.0
* @since-jetpack 6.9.0
* @since 2.0.0
*
* @param string $affiliate_code The affiliate code, blank by default.
*/
return apply_filters( 'jetpack_affiliate_code', get_option( 'jetpack_affiliate_code', '' ) );
case self::SUBSIDIARY_CODE:
/**
* Allow to filter the partner subsidiary id.
*
* @since partner-1.0.0
* @since 2.0.0
*
* @param string $subsidiary_id The partner subsidiary id, blank by default.
*/
return apply_filters(
'jetpack_partner_subsidiary_id',
get_option( 'jetpack_partner_subsidiary_id', '' )
);
default:
return '';
}
}
/**
* Resets the singleton for testing purposes.
*/
public static function reset() {
self::$instance = null;
}
}

View File

@@ -24,6 +24,11 @@ class Plugin_Storage {
*/
const PLUGINS_DISABLED_OPTION_NAME = 'jetpack_connection_disabled_plugins';
/**
* Transient name used as flag to indicate that the active connected plugins list needs refreshing.
*/
const ACTIVE_PLUGINS_REFRESH_FLAG = 'jetpack_connection_active_plugins_refresh';
/**
* Whether this class was configured for the first time or not.
*
@@ -31,13 +36,6 @@ class Plugin_Storage {
*/
private static $configured = false;
/**
* Refresh list of connected plugins upon intialization.
*
* @var boolean
*/
private static $refresh_connected_plugins = false;
/**
* Connected plugins.
*
@@ -65,11 +63,6 @@ class Plugin_Storage {
public static function upsert( $slug, array $args = array() ) {
self::$plugins[ $slug ] = $args;
// if plugin is not in the list of active plugins, refresh the list.
if ( ! array_key_exists( $slug, (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) ) ) {
self::$refresh_connected_plugins = true;
}
return true;
}
@@ -167,19 +160,58 @@ class Plugin_Storage {
return;
}
self::$configured = true;
add_action( 'update_option_active_plugins', array( __CLASS__, 'set_flag_to_refresh_active_connected_plugins' ) );
self::maybe_update_active_connected_plugins();
}
/**
* Set a flag to indicate that the active connected plugins list needs to be updated.
* This will happen when the `active_plugins` option is updated.
*
* @see configure
*/
public static function set_flag_to_refresh_active_connected_plugins() {
set_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG, time() );
}
/**
* Determine if we need to update the active connected plugins list.
*/
public static function maybe_update_active_connected_plugins() {
$maybe_error = self::ensure_configured();
if ( $maybe_error instanceof WP_Error ) {
return;
}
// Only attempt to update the option if the corresponding flag is set.
if ( ! get_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG ) ) {
return;
}
// Only attempt to update the option on POST requests.
// This will prevent the option from being updated multiple times due to concurrent requests.
if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) {
return;
}
delete_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG );
if ( is_multisite() ) {
self::$current_blog_id = get_current_blog_id();
}
// If a plugin was activated or deactivated.
// self::$plugins is populated in Config::ensure_options_connection().
$number_of_plugins_differ = count( self::$plugins ) !== count( (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) );
$configured_plugin_keys = array_keys( self::$plugins );
$stored_plugin_keys = array_keys( (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) );
sort( $configured_plugin_keys );
sort( $stored_plugin_keys );
if ( $number_of_plugins_differ || true === self::$refresh_connected_plugins ) {
if ( $configured_plugin_keys !== $stored_plugin_keys ) {
self::update_active_plugins_option();
}
self::$configured = true;
}
/**
@@ -188,7 +220,7 @@ class Plugin_Storage {
* @return void
*/
public static function update_active_plugins_option() {
// Note: Since this options is synced to wpcom, if you change its structure, you have to update the sanitizer at wpcom side.
// Note: Since this option is synced to wpcom, if you change its structure, you have to update the sanitizer at wpcom side.
update_option( self::ACTIVE_PLUGINS_OPTION_NAME, self::$plugins );
if ( ! class_exists( 'Automattic\Jetpack\Sync\Settings' ) || ! \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {

View File

@@ -7,6 +7,8 @@
namespace Automattic\Jetpack\Connection;
use WP_Error;
/**
* The Jetpack Connection Rest Authentication class.
*/
@@ -118,7 +120,7 @@ class Rest_Authentication {
}
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_request',
__( 'The request method is missing.', 'jetpack-connection' ),
array( 'status' => 400 )
@@ -131,7 +133,7 @@ class Rest_Authentication {
// can be passed to the WP REST API via the '?_method=' parameter if
// needed.
if ( 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_request',
__( 'This request method is not supported.', 'jetpack-connection' ),
array( 'status' => 400 )
@@ -139,7 +141,7 @@ class Rest_Authentication {
return null;
}
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) {
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_request',
__( 'This request method does not support body parameters.', 'jetpack-connection' ),
array( 'status' => 400 )
@@ -173,7 +175,7 @@ class Rest_Authentication {
}
// Something else went wrong. Probably a signature error.
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_signature',
__( 'The request is not signed correctly.', 'jetpack-connection' ),
array( 'status' => 400 )

View File

@@ -7,6 +7,7 @@
namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
@@ -84,6 +85,49 @@ class REST_Connector {
)
);
// Authorize a remote user.
register_rest_route(
'jetpack/v4',
'/remote_provision',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'remote_provision' ),
'permission_callback' => array( $this, 'remote_provision_permission_check' ),
)
);
register_rest_route(
'jetpack/v4',
'/remote_register',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'remote_register' ),
'permission_callback' => array( $this, 'remote_register_permission_check' ),
)
);
// Connect a remote user.
register_rest_route(
'jetpack/v4',
'/remote_connect',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'remote_connect' ),
'permission_callback' => array( $this, 'remote_connect_permission_check' ),
)
);
// The endpoint verifies blog connection and blog token validity.
register_rest_route(
'jetpack/v4',
'/connection/check',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'connection_check' ),
'permission_callback' => array( $this, 'connection_check_permission_check' ),
)
);
// Get current connection status of Jetpack.
register_rest_route(
'jetpack/v4',
@@ -274,7 +318,7 @@ class REST_Connector {
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array|wp-error
* @return array|WP_Error
*/
public static function remote_authorize( $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
@@ -287,6 +331,103 @@ class REST_Connector {
return $result;
}
/**
* Initiate the site provisioning process.
*
* @since 2.5.0
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return WP_Error|array
*/
public static function remote_provision( WP_REST_Request $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_provision( $request );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
}
return $result;
}
/**
* Connect a remote user.
*
* @since 2.6.0
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return WP_Error|array
*/
public static function remote_connect( WP_REST_Request $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_connect( $request );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
}
return $result;
}
/**
* Register the site so that a plan can be provisioned.
*
* @since 2.5.0
*
* @param WP_REST_Request $request The request object.
*
* @return WP_Error|array
*/
public function remote_register( WP_REST_Request $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_register( $request );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
}
return $result;
}
/**
* Remote provision endpoint permission check.
*
* @return true|WP_Error
*/
public function remote_provision_permission_check() {
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() ) );
}
/**
* Remote connect endpoint permission check.
*
* @return true|WP_Error
*/
public function remote_connect_permission_check() {
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* Remote register endpoint permission check.
*
* @return true|WP_Error
*/
public function remote_register_permission_check() {
if ( $this->connection->has_connected_owner() ) {
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
}
return true;
}
/**
* Get connection status for this Jetpack site.
*
@@ -303,7 +444,7 @@ class REST_Connector {
$connection_status = array(
'isActive' => $connection->has_connected_owner(), // TODO deprecate this.
'isStaging' => $status->is_staging_site(),
'isStaging' => $status->in_safe_mode(), // TODO deprecate this.
'isRegistered' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'hasConnectedOwner' => $connection->has_connected_owner(),
@@ -674,11 +815,7 @@ class REST_Connector {
$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
if ( class_exists( 'Jetpack' ) ) {
$authorize_url = \Jetpack::build_authorize_url( $redirect_uri );
} else {
$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
}
$authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri );
/**
* Filters the response of jetpack/v4/connection/register endpoint
@@ -798,7 +935,7 @@ class REST_Connector {
*
* @since 1.29.0
*
* @return bool|WP_Error.
* @return bool|WP_Error
*/
public static function update_user_token_permission_check() {
return Rest_Authentication::is_signed_with_blog_token()
@@ -847,4 +984,41 @@ class REST_Connector {
return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* The endpoint verifies blog connection and blog token validity.
*
* @since 2.7.0
*
* @return mixed|null
*/
public function connection_check() {
/**
* Filters the successful response of the REST API test_connection method
*
* @param string $response The response string.
*/
$status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
return rest_ensure_response(
array(
'status' => $status,
)
);
}
/**
* Remote connect endpoint permission check.
*
* @return true|WP_Error
*/
public function connection_check_permission_check() {
if ( current_user_can( 'jetpack_connect' ) ) {
return true;
}
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
}

View File

@@ -158,6 +158,7 @@ class Secrets {
*/
do_action( 'jetpack_verify_secrets_begin', $action, $user );
/** Closure to run the 'fail' action and return an error. */
$return_error = function ( WP_Error $error ) use ( $action, $user ) {
/**
* Verifying of the previously generated secret has failed.

View File

@@ -223,7 +223,7 @@ class Server_Sandbox {
*
* Attached to the `admin_bar_menu` action.
*
* @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
* @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
*/
public function admin_bar_add_sandbox_item( $wp_admin_bar ) {
if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) {

View File

@@ -38,8 +38,8 @@ class Tracking {
/**
* Creates the Tracking object.
*
* @param String $product_name the slug of the product that we are tracking.
* @param Automattic\Jetpack\Connection\Manager $connection the connection manager object.
* @param String $product_name the slug of the product that we are tracking.
* @param \Automattic\Jetpack\Connection\Manager $connection the connection manager object.
*/
public function __construct( $product_name = 'jetpack', $connection = null ) {
$this->product_name = $product_name;
@@ -159,7 +159,7 @@ class Tracking {
*
* @param string $event_type Type of the event.
* @param array $data Data to send with the event.
* @param mixed $user Username, user_id, or WP_user object.
* @param mixed $user Username, user_id, or WP_User object.
* @param bool $use_product_prefix Whether to use the object's product name as a prefix to the event type. If
* set to false, the prefix will be 'jetpack_'.
*/
@@ -189,7 +189,7 @@ class Tracking {
/**
* Record an event in Tracks - this is the preferred way to record events from PHP.
*
* @param mixed $user username, user_id, or WP_user object.
* @param mixed $user username, user_id, or WP_User object.
* @param string $event_name The name of the event.
* @param array $properties Custom properties to send with the event.
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
@@ -221,8 +221,8 @@ class Tracking {
/**
* Determines whether tracking should be enabled.
*
* @param Automattic\Jetpack\Terms_Of_Service $terms_of_service A Terms_Of_Service object.
* @param Automattic\Jetpack\Status $status A Status object.
* @param \Automattic\Jetpack\Terms_Of_Service $terms_of_service A Terms_Of_Service object.
* @param \Automattic\Jetpack\Status $status A Status object.
*
* @return boolean True if tracking should be enabled, else false.
*/
@@ -238,10 +238,10 @@ class Tracking {
* Procedurally build a Tracks Event Object.
* NOTE: Use this only when the simpler Automattic\Jetpack\Tracking->jetpack_tracks_record_event() function won't work for you.
*
* @param WP_user $user WP_user object.
* @param string $event_name The name of the event.
* @param array $properties Custom properties to send with the event.
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
* @param \WP_User $user WP_User object.
* @param string $event_name The name of the event.
* @param array $properties Custom properties to send with the event.
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
*
* @return \Jetpack_Tracks_Event|\WP_Error
*/
@@ -252,6 +252,7 @@ class Tracking {
$blog_details = array(
'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ),
'blog_id' => \Jetpack_Options::get_option( 'id' ),
);
$timestamp = ( false !== $event_timestamp_millis ) ? $event_timestamp_millis : round( microtime( true ) * 1000 );

View File

@@ -82,8 +82,8 @@ class Urls {
/**
* Return URL with a normalized protocol.
*
* @param callable $callable Function to retrieve URL option.
* @param string $new_value URL Protocol to set URLs to.
* @param string $callable Function name that was used to retrieve URL option.
* @param string $new_value URL Protocol to set URLs to.
* @return string Normalized URL.
*/
public static function get_protocol_normalized_url( $callable, $new_value ) {

View File

@@ -83,4 +83,53 @@ class Utils {
)
);
}
/**
* Generate a new user from a SSO attempt.
*
* @param object $user_data WordPress.com user information.
*/
public static function generate_user( $user_data ) {
$username = $user_data->login;
/**
* Determines how many times the SSO module can attempt to randomly generate a user.
*
* @module sso
*
* @since jetpack-4.3.2
*
* @param int 5 By default, SSO will attempt to random generate a user up to 5 times.
*/
$num_tries = (int) apply_filters( 'jetpack_sso_allowed_username_generate_retries', 5 );
$exists = username_exists( $username );
$tries = 0;
while ( $exists && $tries++ < $num_tries ) {
$username = $user_data->login . '_' . $user_data->ID . '_' . wp_rand();
$exists = username_exists( $username );
}
if ( $exists ) {
return false;
}
$user = (object) array();
$user->user_pass = wp_generate_password( 20 );
$user->user_login = wp_slash( $username );
$user->user_email = wp_slash( $user_data->email );
$user->display_name = $user_data->display_name;
$user->first_name = $user_data->first_name;
$user->last_name = $user_data->last_name;
$user->url = $user_data->url;
$user->description = $user_data->description;
if ( isset( $user_data->role ) && $user_data->role ) {
$user->role = $user_data->role;
}
$created_user_id = wp_insert_user( $user );
update_user_meta( $created_user_id, 'wpcom_user_id', $user_data->ID );
return get_userdata( $created_user_id );
}
}

View File

@@ -86,11 +86,11 @@ class Webhooks {
case 'authorize':
$this->handle_authorize();
$this->do_exit();
break;
break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns.
case 'authorize_redirect':
$this->handle_authorize_redirect();
$this->do_exit();
break;
break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns.
// Class Jetpack::admin_page_load() still handles other cases.
}
}
@@ -107,7 +107,7 @@ class Webhooks {
}
do_action( 'jetpack_client_authorize_processing' );
$data = stripslashes_deep( $_GET );
$data = stripslashes_deep( $_GET ); // We need all request data under the context of an authorization request.
$data['auth_type'] = 'client';
$roles = new Roles();
$role = $roles->translate_current_user_to_role();
@@ -159,6 +159,8 @@ class Webhooks {
/**
* The `exit` is wrapped into a method so we could mock it.
*
* @return never
*/
protected function do_exit() {
exit;

View File

@@ -60,6 +60,7 @@ class XMLRPC_Async_Call {
self::$clients[ $client_blog_id ][ $user_id ] = new Jetpack_IXR_ClientMulticall( array( 'user_id' => $user_id ) );
}
// https://plugins.trac.wordpress.org/ticket/2041
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}

View File

@@ -7,6 +7,8 @@
namespace Automattic\Jetpack\Connection;
use IXR_Error;
/**
* Registers the XML-RPC methods for Connections.
*/
@@ -69,10 +71,10 @@ class XMLRPC_Connector {
$code = -10520;
}
if ( ! class_exists( \IXR_Error::class ) ) {
if ( ! class_exists( IXR_Error::class ) ) {
require_once ABSPATH . WPINC . '/class-IXR.php';
}
return new \IXR_Error(
return new IXR_Error(
$code,
sprintf( 'Jetpack: [%s] %s', $data->get_error_code(), $data->get_error_message() )
);

View File

@@ -0,0 +1,30 @@
#wpadminbar #wp-admin-bar-jetpack-idc {
margin-right: 5px;
.jp-idc-admin-bar {
border-radius: 2px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #EFEFF0;
padding: 6px 8px;
}
&.hide {
display: none;
}
.dashicons {
font-family: 'dashicons';
margin-top: -6px;
&:before {
font-size: 18px;
}
}
.ab-item {
padding: 0;
background: #E68B28;
}
}

View File

@@ -0,0 +1,62 @@
import { IDCScreen } from '@automattic/jetpack-idc';
import * as WPElement from '@wordpress/element';
import React from 'react';
import './admin-bar.scss';
import './style.scss';
/**
* The initial renderer function.
*/
function render() {
if ( ! window.hasOwnProperty( 'JP_IDENTITY_CRISIS__INITIAL_STATE' ) ) {
return;
}
const container = document.getElementById(
window.JP_IDENTITY_CRISIS__INITIAL_STATE.containerID || 'jp-identity-crisis-container'
);
if ( null === container ) {
return;
}
const {
WP_API_root,
WP_API_nonce,
wpcomHomeUrl,
currentUrl,
redirectUri,
tracksUserData,
tracksEventData,
isSafeModeConfirmed,
consumerData,
isAdmin,
possibleDynamicSiteUrlDetected,
isDevelopmentSite,
} = window.JP_IDENTITY_CRISIS__INITIAL_STATE;
if ( ! isSafeModeConfirmed ) {
const component = (
<IDCScreen
wpcomHomeUrl={ wpcomHomeUrl }
currentUrl={ currentUrl }
apiRoot={ WP_API_root }
apiNonce={ WP_API_nonce }
redirectUri={ redirectUri }
tracksUserData={ tracksUserData || {} }
tracksEventData={ tracksEventData }
customContent={
consumerData.hasOwnProperty( 'customContent' ) ? consumerData.customContent : {}
}
isAdmin={ isAdmin }
logo={ consumerData.hasOwnProperty( 'logo' ) ? consumerData.logo : undefined }
possibleDynamicSiteUrlDetected={ possibleDynamicSiteUrlDetected }
isDevelopmentSite={ isDevelopmentSite }
/>
);
WPElement.createRoot( container ).render( component );
}
}
window.addEventListener( 'load', () => render() );

View File

@@ -0,0 +1,9 @@
#jp-identity-crisis-container .jp-idc__idc-screen {
margin-top: 40px;
margin-bottom: 40px;
}
#jp-identity-crisis-container.notice {
background: none;
border: none;
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Exception class for the Identity Crisis component.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\IdentityCrisis;
/**
* Exception class for the Identity Crisis component.
*/
class Exception extends \Exception {}

View File

@@ -0,0 +1,833 @@
<?php
/**
* Identity_Crisis class of the Connection package.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\IdentityCrisis\Exception;
use Automattic\Jetpack\IdentityCrisis\UI;
use Automattic\Jetpack\IdentityCrisis\URL_Secret;
use Jetpack_Options;
use WP_Error;
/**
* This class will handle everything involved with fixing an Identity Crisis.
*
* @since automattic/jetpack-identity-crisis:0.2.0
* @since-jetpack 4.4.0
* @since 2.9.0
*/
class Identity_Crisis {
/**
* Persistent WPCOM blog ID that stays in the options after disconnect.
*/
const PERSISTENT_BLOG_ID_OPTION_NAME = 'jetpack_persistent_blog_id';
/**
* Instance of the object.
*
* @var Identity_Crisis
**/
private static $instance = null;
/**
* The wpcom value of the home URL.
*
* @var string
*/
public static $wpcom_home_url;
/**
* Has safe mode been confirmed?
* Beware, it never contains `true` for non-admins, so doesn't always reflect the actual value.
*
* @var bool
*/
public static $is_safe_mode_confirmed;
/**
* The current screen, which is set if the current user is a non-admin and this is an admin page.
*
* @var \WP_Screen
*/
public static $current_screen;
/**
* Initializer.
*
* @return object
*/
public static function init() {
if ( self::$instance === null ) {
self::$instance = new Identity_Crisis();
}
return self::$instance;
}
/**
* Class constructor.
*
* @return void
*/
private function __construct() {
add_action( 'jetpack_sync_processed_actions', array( $this, 'maybe_clear_migrate_option' ) );
add_action( 'rest_api_init', array( 'Automattic\\Jetpack\\IdentityCrisis\\REST_Endpoints', 'initialize_rest_api' ) );
add_action( 'jetpack_idc_disconnect', array( __CLASS__, 'do_jetpack_idc_disconnect' ) );
add_action( 'jetpack_received_remote_request_response', array( $this, 'check_http_response_for_idc_detected' ) );
add_filter( 'jetpack_connection_disconnect_site_wpcom', array( __CLASS__, 'jetpack_connection_disconnect_site_wpcom_filter' ) );
add_filter( 'jetpack_remote_request_url', array( $this, 'add_idc_query_args_to_url' ) );
add_filter( 'jetpack_connection_validate_urls_for_idc_mitigation_response', array( static::class, 'add_secret_to_url_validation_response' ) );
add_filter( 'jetpack_connection_validate_urls_for_idc_mitigation_response', array( static::class, 'add_ip_requester_to_url_validation_response' ) );
add_filter( 'jetpack_options', array( static::class, 'reverse_wpcom_urls_for_idc' ) );
add_filter( 'jetpack_register_request_body', array( static::class, 'register_request_body' ) );
add_action( 'jetpack_site_registered', array( static::class, 'site_registered' ) );
$urls_in_crisis = self::check_identity_crisis();
if ( false === $urls_in_crisis ) {
return;
}
self::$wpcom_home_url = $urls_in_crisis['wpcom_home'];
add_action( 'init', array( $this, 'wordpress_init' ) );
}
/**
* Disconnect current connection and clear IDC options.
*/
public static function do_jetpack_idc_disconnect() {
$connection = new Connection_Manager();
// If the site is in an IDC because sync is not allowed,
// let's make sure to not disconnect the production site.
if ( ! self::validate_sync_error_idc_option() ) {
$connection->disconnect_site( true );
} else {
$connection->disconnect_site( false );
}
delete_option( static::PERSISTENT_BLOG_ID_OPTION_NAME );
// Clear IDC options.
self::clear_all_idc_options();
}
/**
* Filter to prevent site from disconnecting from WPCOM if it's in an IDC.
*
* @see jetpack_connection_disconnect_site_wpcom filter.
*
* @return bool False if the site is in IDC, true otherwise.
*/
public static function jetpack_connection_disconnect_site_wpcom_filter() {
return ! self::validate_sync_error_idc_option();
}
/**
* This method loops through the array of processed items from sync and checks if one of the items was the
* home_url or site_url callable. If so, then we delete the jetpack_migrate_for_idc option.
*
* @param array $processed_items Array of processed items that were synced to WordPress.com.
*/
public function maybe_clear_migrate_option( $processed_items ) {
foreach ( (array) $processed_items as $item ) {
// First, is this item a jetpack_sync_callable action? If so, then proceed.
$callable_args = ( is_array( $item ) && isset( $item[0] ) && isset( $item[1] ) && 'jetpack_sync_callable' === $item[0] )
? $item[1]
: null;
// Second, if $callable_args is set, check if the callable was home_url or site_url. If so,
// clear the migrate option.
if (
isset( $callable_args[0] )
&& ( 'home_url' === $callable_args[0] || 'site_url' === $callable_args[1] )
) {
Jetpack_Options::delete_option( 'migrate_for_idc' );
break;
}
}
}
/**
* WordPress init.
*
* @return void
*/
public function wordpress_init() {
if ( current_user_can( 'jetpack_disconnect' ) ) {
if (
isset( $_GET['jetpack_idc_clear_confirmation'] ) && isset( $_GET['_wpnonce'] ) &&
wp_verify_nonce( $_GET['_wpnonce'], 'jetpack_idc_clear_confirmation' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WordPress core doesn't unslash or verify nonces either.
) {
Jetpack_Options::delete_option( 'safe_mode_confirmed' );
self::$is_safe_mode_confirmed = false;
} else {
self::$is_safe_mode_confirmed = (bool) Jetpack_Options::get_option( 'safe_mode_confirmed' );
}
}
// 121 Priority so that it's the most inner Jetpack item in the admin bar.
add_action( 'admin_bar_menu', array( $this, 'display_admin_bar_button' ), 121 );
UI::init();
}
/**
* Add the idc query arguments to the url.
*
* @param string $url The remote request url.
*/
public function add_idc_query_args_to_url( $url ) {
$status = new Status();
if ( ! is_string( $url )
|| $status->is_offline_mode()
|| self::validate_sync_error_idc_option() ) {
return $url;
}
$home_url = Urls::home_url();
$site_url = Urls::site_url();
$hostname = wp_parse_url( $site_url, PHP_URL_HOST );
// If request is from an IP, make sure ip_requester option is set
if ( self::url_is_ip( $hostname ) ) {
self::maybe_update_ip_requester( $hostname );
}
$query_args = array(
'home' => $home_url,
'siteurl' => $site_url,
);
if ( self::should_handle_idc() ) {
$query_args['idc'] = true;
}
if ( \Jetpack_Options::get_option( 'migrate_for_idc', false ) ) {
$query_args['migrate_for_idc'] = true;
}
if ( is_multisite() ) {
$query_args['multisite'] = true;
}
return add_query_arg( $query_args, $url );
}
/**
* Renders the admin bar button.
*
* @return void
*/
public function display_admin_bar_button() {
global $wp_admin_bar;
$href = is_admin()
? add_query_arg( 'jetpack_idc_clear_confirmation', '1' )
: add_query_arg( 'jetpack_idc_clear_confirmation', '1', admin_url() );
$href = wp_nonce_url( $href, 'jetpack_idc_clear_confirmation' );
$consumer_data = UI::get_consumer_data();
$label = isset( $consumer_data['customContent']['adminBarSafeModeLabel'] )
? esc_html( $consumer_data['customContent']['adminBarSafeModeLabel'] )
: esc_html__( 'Jetpack Safe Mode', 'jetpack-connection' );
$title = sprintf(
'<span class="jp-idc-admin-bar">%s %s</span>',
'<span class="dashicons dashicons-info-outline"></span>',
$label
);
$menu = array(
'id' => 'jetpack-idc',
'title' => $title,
'href' => esc_url( $href ),
'parent' => 'top-secondary',
);
if ( ! self::$is_safe_mode_confirmed ) {
$menu['meta'] = array(
'class' => 'hide',
);
}
$wp_admin_bar->add_node( $menu );
}
/**
* Checks if the site is currently in an identity crisis.
*
* @return array|bool Array of options that are in a crisis, or false if everything is OK.
*/
public static function check_identity_crisis() {
$connection = new Connection_Manager( 'jetpack' );
if ( ! $connection->is_connected() || ( new Status() )->is_offline_mode() || ! self::validate_sync_error_idc_option() ) {
return false;
}
return Jetpack_Options::get_option( 'sync_error_idc' );
}
/**
* Checks the HTTP response body for the 'idc_detected' key. If the key exists,
* checks the idc_detected value for a valid idc error.
*
* @param array|WP_Error $http_response The HTTP response.
*
* @return bool Whether the site is in an identity crisis.
*/
public function check_http_response_for_idc_detected( $http_response ) {
if ( ! is_array( $http_response ) ) {
return false;
}
$response_body = json_decode( wp_remote_retrieve_body( $http_response ), true );
if ( isset( $response_body['idc_detected'] ) ) {
return $this->check_response_for_idc( $response_body['idc_detected'] );
}
if ( isset( $response_body['migrated_for_idc'] ) ) {
Jetpack_Options::delete_option( 'migrate_for_idc' );
}
return false;
}
/**
* Checks the WPCOM response to determine if the site is in an identity crisis. Updates the
* sync_error_idc option if it is.
*
* @param array $response The response data.
*
* @return bool Whether the site is in an identity crisis.
*/
public function check_response_for_idc( $response ) {
if ( is_array( $response ) && isset( $response['error_code'] ) ) {
$error_code = $response['error_code'];
$allowed_idc_error_codes = array(
'jetpack_url_mismatch',
'jetpack_home_url_mismatch',
'jetpack_site_url_mismatch',
);
if ( in_array( $error_code, $allowed_idc_error_codes, true ) ) {
Jetpack_Options::update_option(
'sync_error_idc',
self::get_sync_error_idc_option( $response )
);
}
return true;
}
return false;
}
/**
* Clears all IDC specific options. This method is used on disconnect and reconnect.
*
* @return void
*/
public static function clear_all_idc_options() {
// If the site is currently in IDC, let's also clear the VaultPress connection options.
// We have to check if the site is in IDC, otherwise we'd be clearing the VaultPress
// connection any time the Jetpack connection is cycled.
if ( self::validate_sync_error_idc_option() ) {
delete_option( 'vaultpress' );
delete_option( 'vaultpress_auto_register' );
}
Jetpack_Options::delete_option(
array(
'sync_error_idc',
'safe_mode_confirmed',
'migrate_for_idc',
)
);
delete_transient( 'jetpack_idc_possible_dynamic_site_url_detected' );
}
/**
* Checks whether the sync_error_idc option is valid or not, and if not, will do cleanup.
*
* @return bool
* @since-jetpack 5.4.0 Do not call get_sync_error_idc_option() unless site is in IDC
*
* @since 0.2.0
* @since-jetpack 4.4.0
*/
public static function validate_sync_error_idc_option() {
$is_valid = false;
// Is the site opted in and does the stored sync_error_idc option match what we now generate?
$sync_error = Jetpack_Options::get_option( 'sync_error_idc' );
if ( $sync_error && self::should_handle_idc() ) {
$local_options = self::get_sync_error_idc_option();
// Ensure all values are set.
if ( isset( $sync_error['home'] ) && isset( $local_options['home'] ) && isset( $sync_error['siteurl'] ) && isset( $local_options['siteurl'] ) ) {
// If the WP.com expected home and siteurl match local home and siteurl it is not valid IDC.
if (
isset( $sync_error['wpcom_home'] ) &&
isset( $sync_error['wpcom_siteurl'] ) &&
$sync_error['wpcom_home'] === $local_options['home'] &&
$sync_error['wpcom_siteurl'] === $local_options['siteurl']
) {
// Enable migrate_for_idc so that sync actions are accepted.
Jetpack_Options::update_option( 'migrate_for_idc', true );
} elseif ( $sync_error['home'] === $local_options['home'] && $sync_error['siteurl'] === $local_options['siteurl'] ) {
$is_valid = true;
}
}
}
/**
* Filters whether the sync_error_idc option is valid.
*
* @param bool $is_valid If the sync_error_idc is valid or not.
*
* @since 0.2.0
* @since-jetpack 4.4.0
*/
$is_valid = (bool) apply_filters( 'jetpack_sync_error_idc_validation', $is_valid );
if ( ! $is_valid && $sync_error ) {
// Since the option exists, and did not validate, delete it.
Jetpack_Options::delete_option( 'sync_error_idc' );
}
return $is_valid;
}
/**
* Reverses WP.com URLs stored in sync_error_idc option.
*
* @param array $sync_error error option containing reversed URLs.
* @return array
*/
public static function reverse_wpcom_urls_for_idc( $sync_error ) {
if ( isset( $sync_error['reversed_url'] ) ) {
if ( array_key_exists( 'wpcom_siteurl', $sync_error ) ) {
$sync_error['wpcom_siteurl'] = strrev( $sync_error['wpcom_siteurl'] );
}
if ( array_key_exists( 'wpcom_home', $sync_error ) ) {
$sync_error['wpcom_home'] = strrev( $sync_error['wpcom_home'] );
}
}
return $sync_error;
}
/**
* Normalizes a url by doing three things:
* - Strips protocol
* - Strips www
* - Adds a trailing slash
*
* @param string $url URL to parse.
*
* @return WP_Error|string
* @since 0.2.0
* @since-jetpack 4.4.0
*/
public static function normalize_url_protocol_agnostic( $url ) {
$parsed_url = wp_parse_url( trailingslashit( esc_url_raw( $url ) ) );
if ( ! $parsed_url || empty( $parsed_url['host'] ) || empty( $parsed_url['path'] ) ) {
return new WP_Error(
'cannot_parse_url',
sprintf(
/* translators: %s: URL to parse. */
esc_html__( 'Cannot parse URL %s', 'jetpack-connection' ),
$url
)
);
}
// Strip www and protocols.
$url = preg_replace( '/^www\./i', '', $parsed_url['host'] . $parsed_url['path'] );
return $url;
}
/**
* Gets the value that is to be saved in the jetpack_sync_error_idc option.
*
* @param array $response HTTP response.
*
* @return array Array of the local urls, wpcom urls, and error code.
* @since 0.2.0
* @since-jetpack 4.4.0
* @since-jetpack 5.4.0 Add transient since home/siteurl retrieved directly from DB.
*/
public static function get_sync_error_idc_option( $response = array() ) {
// Since the local options will hit the database directly, store the values
// in a transient to allow for autoloading and caching on subsequent views.
$local_options = get_transient( 'jetpack_idc_local' );
if ( false === $local_options ) {
$local_options = array(
'home' => Urls::home_url(),
'siteurl' => Urls::site_url(),
);
set_transient( 'jetpack_idc_local', $local_options, MINUTE_IN_SECONDS );
}
$options = array_merge( $local_options, $response );
$returned_values = array();
foreach ( $options as $key => $option ) {
if ( 'error_code' === $key ) {
$returned_values[ $key ] = $option;
continue;
}
$normalized_url = self::normalize_url_protocol_agnostic( $option );
if ( is_wp_error( $normalized_url ) ) {
continue;
}
$returned_values[ $key ] = $normalized_url;
}
// We need to protect WPCOM URLs from search & replace by reversing them. See https://wp.me/pf5801-3R
// Add 'reversed_url' key for backward compatibility
if ( array_key_exists( 'wpcom_home', $returned_values ) && array_key_exists( 'wpcom_siteurl', $returned_values ) ) {
$returned_values['reversed_url'] = true;
$returned_values = self::reverse_wpcom_urls_for_idc( $returned_values );
}
return $returned_values;
}
/**
* Returns the value of the jetpack_should_handle_idc filter or constant.
* If set to true, the site will be put into staging mode.
*
* This method uses both the current jetpack_should_handle_idc filter
* and constant to determine whether an IDC should be handled.
*
* @return bool
* @since 0.2.6
*/
public static function should_handle_idc() {
if ( Constants::is_defined( 'JETPACK_SHOULD_HANDLE_IDC' ) ) {
$default = Constants::get_constant( 'JETPACK_SHOULD_HANDLE_IDC' );
} else {
$default = ! Constants::is_defined( 'SUNRISE' ) && ! is_multisite();
}
/**
* Allows sites to opt in for IDC mitigation which blocks the site from syncing to WordPress.com when the home
* URL or site URL do not match what WordPress.com expects. The default value is either true, or the value of
* JETPACK_SHOULD_HANDLE_IDC constant if set.
*
* @param bool $default Whether the site is opted in to IDC mitigation.
*
* @since 0.2.6
*/
return (bool) apply_filters( 'jetpack_should_handle_idc', $default );
}
/**
* Whether the site is undergoing identity crisis.
*
* @return bool
*/
public static function has_identity_crisis() {
return false !== static::check_identity_crisis() && ! static::$is_safe_mode_confirmed;
}
/**
* Whether an admin has confirmed safe mode.
* Unlike `static::$is_safe_mode_confirmed` this function always returns the actual flag value.
*
* @return bool
*/
public static function safe_mode_is_confirmed() {
return Jetpack_Options::get_option( 'safe_mode_confirmed' );
}
/**
* Returns the mismatched URLs.
*
* @return array|bool The mismatched urls, or false if the site is not connected, offline, in safe mode, or the IDC error is not valid.
*/
public static function get_mismatched_urls() {
if ( ! static::has_identity_crisis() ) {
return false;
}
$data = static::check_identity_crisis();
if ( ! $data ||
! isset( $data['error_code'] ) ||
! isset( $data['wpcom_home'] ) ||
! isset( $data['home'] ) ||
! isset( $data['wpcom_siteurl'] ) ||
! isset( $data['siteurl'] )
) {
// The jetpack_sync_error_idc option is missing a key.
return false;
}
if ( 'jetpack_site_url_mismatch' === $data['error_code'] ) {
return array(
'wpcom_url' => $data['wpcom_siteurl'],
'current_url' => $data['siteurl'],
);
}
return array(
'wpcom_url' => $data['wpcom_home'],
'current_url' => $data['home'],
);
}
/**
* Try to detect $_SERVER['HTTP_HOST'] being used within WP_SITEURL or WP_HOME definitions inside of wp-config.
*
* If `HTTP_HOST` usage is found, it's possbile (though not certain) that site URLs are dynamic.
*
* When a site URL is dynamic, it can lead to a Jetpack IDC. If potentially dynamic usage is detected,
* helpful support info will be shown on the IDC UI about setting a static site/home URL.
*
* @return bool True if potentially dynamic site urls were detected in wp-config, false otherwise.
*/
public static function detect_possible_dynamic_site_url() {
$transient_key = 'jetpack_idc_possible_dynamic_site_url_detected';
$transient_val = get_transient( $transient_key );
if ( false !== $transient_val ) {
return (bool) $transient_val;
}
$path = self::locate_wp_config();
$wp_config = $path ? file_get_contents( $path ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
if ( $wp_config ) {
$matched = preg_match(
'/define ?\( ?[\'"](?:WP_SITEURL|WP_HOME).+(?:HTTP_HOST).+\);/',
$wp_config
);
if ( $matched ) {
set_transient( $transient_key, 1, HOUR_IN_SECONDS );
return true;
}
}
set_transient( $transient_key, 0, HOUR_IN_SECONDS );
return false;
}
/**
* Gets path to WordPress configuration.
* Source: https://github.com/wp-cli/wp-cli/blob/master/php/utils.php
*
* @return string
*/
public static function locate_wp_config() {
static $path;
if ( null === $path ) {
$path = false;
if ( getenv( 'WP_CONFIG_PATH' ) && file_exists( getenv( 'WP_CONFIG_PATH' ) ) ) {
$path = getenv( 'WP_CONFIG_PATH' );
} elseif ( file_exists( ABSPATH . 'wp-config.php' ) ) {
$path = ABSPATH . 'wp-config.php';
} elseif ( file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
$path = dirname( ABSPATH ) . '/wp-config.php';
}
if ( $path ) {
$path = realpath( $path );
}
}
return $path;
}
/**
* Adds `url_secret` to the `jetpack.idcUrlValidation` URL validation endpoint.
* Adds `url_secret_error` in case of an error.
*
* @param array $response The endpoint response that we're modifying.
*
* @return array
*
* phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag -- The exception is being caught, false positive.
*/
public static function add_secret_to_url_validation_response( array $response ) {
try {
$secret = new URL_Secret();
$secret->create();
if ( $secret->exists() ) {
$response['url_secret'] = $secret->get_secret();
}
} catch ( Exception $e ) {
$response['url_secret_error'] = new WP_Error( 'unable_to_create_url_secret', $e->getMessage() );
}
return $response;
}
/**
* Check if URL is an IP.
*
* @param string $hostname The hostname to check.
* @return bool
*/
public static function url_is_ip( $hostname = null ) {
if ( ! $hostname ) {
$hostname = wp_parse_url( Urls::site_url(), PHP_URL_HOST );
}
$is_ip = filter_var( $hostname, FILTER_VALIDATE_IP ) !== false ? $hostname : false;
return $is_ip;
}
/**
* Add IDC-related data to the registration query.
*
* @param array $params The existing query params.
*
* @return array
*/
public static function register_request_body( array $params ) {
$persistent_blog_id = get_option( static::PERSISTENT_BLOG_ID_OPTION_NAME );
if ( $persistent_blog_id ) {
$params['persistent_blog_id'] = $persistent_blog_id;
$params['url_secret'] = URL_Secret::create_secret( 'registration_request_url_secret_failed' );
}
return $params;
}
/**
* Set the necessary options when site gets registered.
*
* @param int $blog_id The blog ID.
*
* @return void
*/
public static function site_registered( $blog_id ) {
update_option( static::PERSISTENT_BLOG_ID_OPTION_NAME, (int) $blog_id, false );
}
/**
* Check if we need to update the ip_requester option.
*
* @param string $hostname The hostname to check.
*
* @return void
*/
public static function maybe_update_ip_requester( $hostname ) {
// Check if transient exists
$transient_key = ip2long( $hostname );
if ( $transient_key && ! get_transient( 'jetpack_idc_ip_requester_' . $transient_key ) ) {
self::set_ip_requester_for_idc( $hostname, $transient_key );
}
}
/**
* If URL is an IP, add the IP value to the ip_requester option with its expiry value.
*
* @param string $hostname The hostname to check.
* @param int $transient_key The transient key.
*/
public static function set_ip_requester_for_idc( $hostname, $transient_key ) {
// Check if option exists
$data = Jetpack_Options::get_option( 'identity_crisis_ip_requester' );
$ip_requester = array(
'ip' => $hostname,
'expires_at' => time() + 360,
);
// If not set, initialize it
if ( empty( $data ) ) {
$data = array( $ip_requester );
} else {
$updated_data = array();
$updated_value = false;
// Remove expired values and update existing IP
foreach ( $data as $item ) {
if ( time() > $item['expires_at'] ) {
continue; // Skip expired IP
}
if ( $item['ip'] === $hostname ) {
$item['expires_at'] = time() + 360;
$updated_value = true;
}
$updated_data[] = $item;
}
if ( ! $updated_value || empty( $updated_data ) ) {
$updated_data[] = $ip_requester;
}
$data = $updated_data;
}
self::update_ip_requester( $data, $transient_key );
}
/**
* Update the ip_requester option and set a transient to expire in 5 minutes.
*
* @param array $data The data to be updated.
* @param int $transient_key The transient key.
*
* @return void
*/
public static function update_ip_requester( $data, $transient_key ) {
// Update the option
$updated = Jetpack_Options::update_option( 'identity_crisis_ip_requester', $data );
// Set a transient to expire in 5 minutes
if ( $updated ) {
$transient_name = 'jetpack_idc_ip_requester_' . $transient_key;
set_transient( $transient_name, $data, 300 );
}
}
/**
* Adds `ip_requester` to the `jetpack.idcUrlValidation` URL validation endpoint.
*
* @param array $response The enpoint response that we're modifying.
*
* @return array
*/
public static function add_ip_requester_to_url_validation_response( array $response ) {
$requesters = Jetpack_Options::get_option( 'identity_crisis_ip_requester' );
if ( $requesters ) {
// Loop through the requesters and add the IP to the response if it's not expired
$i = 0;
foreach ( $requesters as $ip ) {
if ( $ip['expires_at'] > time() ) {
$response['ip_requester'][] = $ip['ip'];
}
// Limit the response to five IPs
$i = ++$i;
if ( $i === 5 ) {
break;
}
}
}
return $response;
}
}

View File

@@ -0,0 +1,322 @@
<?php
/**
* Identity_Crisis REST endpoints of the Connection package.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\IdentityCrisis;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Rest_Authentication;
use Jetpack_Options;
use Jetpack_XMLRPC_Server;
use WP_Error;
use WP_REST_Server;
/**
* This class will handle Identity Crisis Endpoints
*
* @since automattic/jetpack-identity-crisis:0.2.0
* @since 2.9.0
*/
class REST_Endpoints {
/**
* Initialize REST routes.
*/
public static function initialize_rest_api() {
// Confirm that a site in identity crisis should be in staging mode.
register_rest_route(
'jetpack/v4',
'/identity-crisis/confirm-safe-mode',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::confirm_safe_mode',
'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
)
);
// Handles the request to migrate stats and subscribers during an identity crisis.
register_rest_route(
'jetpack/v4',
'identity-crisis/migrate',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::migrate_stats_and_subscribers',
'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
)
);
// IDC resolve: create an entirely new shadow site for this URL.
register_rest_route(
'jetpack/v4',
'/identity-crisis/start-fresh',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::start_fresh_connection',
'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
'args' => array(
'redirect_uri' => array(
'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
'type' => 'string',
),
),
)
);
// Fetch URL and secret for IDC check.
register_rest_route(
'jetpack/v4',
'/identity-crisis/idc-url-validation',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( static::class, 'validate_urls_and_set_secret' ),
'permission_callback' => array( static::class, 'url_secret_permission_check' ),
)
);
// Fetch URL verification secret.
register_rest_route(
'jetpack/v4',
'/identity-crisis/url-secret',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( static::class, 'fetch_url_secret' ),
'permission_callback' => array( static::class, 'url_secret_permission_check' ),
)
);
// Fetch URL verification secret.
register_rest_route(
'jetpack/v4',
'/identity-crisis/compare-url-secret',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( static::class, 'compare_url_secret' ),
'permission_callback' => array( static::class, 'compare_url_secret_permission_check' ),
'args' => array(
'secret' => array(
'description' => __( 'URL secret to compare to the ones stored in the database.', 'jetpack-connection' ),
'type' => 'string',
'required' => true,
),
),
)
);
}
/**
* Handles identity crisis mitigation, confirming safe mode for this site.
*
* @since 0.2.0
* @since-jetpack 4.4.0
*
* @return bool | WP_Error True if option is properly set.
*/
public static function confirm_safe_mode() {
$updated = Jetpack_Options::update_option( 'safe_mode_confirmed', true );
if ( $updated ) {
return rest_ensure_response(
array(
'code' => 'success',
)
);
}
return new WP_Error(
'error_setting_jetpack_safe_mode',
esc_html__( 'Could not confirm safe mode.', 'jetpack-connection' ),
array( 'status' => 500 )
);
}
/**
* Handles identity crisis mitigation, migrating stats and subscribers from old url to this, new url.
*
* @since 0.2.0
* @since-jetpack 4.4.0
*
* @return bool | WP_Error True if option is properly set.
*/
public static function migrate_stats_and_subscribers() {
if ( Jetpack_Options::get_option( 'sync_error_idc' ) && ! Jetpack_Options::delete_option( 'sync_error_idc' ) ) {
return new WP_Error(
'error_deleting_sync_error_idc',
esc_html__( 'Could not delete sync error option.', 'jetpack-connection' ),
array( 'status' => 500 )
);
}
if ( Jetpack_Options::get_option( 'migrate_for_idc' ) || Jetpack_Options::update_option( 'migrate_for_idc', true ) ) {
return rest_ensure_response(
array(
'code' => 'success',
)
);
}
return new WP_Error(
'error_setting_jetpack_migrate',
esc_html__( 'Could not confirm migration.', 'jetpack-connection' ),
array( 'status' => 500 )
);
}
/**
* This IDC resolution will disconnect the site and re-connect to a completely new
* and separate shadow site than the original.
*
* It will first will disconnect the site without phoning home as to not disturb the production site.
* It then builds a fresh connection URL and sends it back along with the response.
*
* @since 0.2.0
* @since-jetpack 4.4.0
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response|WP_Error
*/
public static function start_fresh_connection( $request ) {
/**
* Fires when Users have requested through Identity Crisis for the connection to be reset.
* Should be used to disconnect any connections and reset options.
*
* @since 0.2.0
*/
do_action( 'jetpack_idc_disconnect' );
$connection = new Connection_Manager();
$result = $connection->try_registration( true );
// early return if site registration fails.
if ( ! $result || is_wp_error( $result ) ) {
return rest_ensure_response( $result );
}
$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
/**
* Filters the connection url that users should be redirected to for re-establishing their connection.
*
* @since 0.2.0
*
* @param \WP_REST_Response|WP_Error $connection_url Connection URL user should be redirected to.
*/
return apply_filters( 'jetpack_idc_authorization_url', rest_ensure_response( $connection->get_authorization_url( null, $redirect_uri ) ) );
}
/**
* Verify that user can mitigate an identity crisis.
*
* @since 0.2.0
* @since-jetpack 4.4.0
*
* @return true|WP_Error True if the user has capability 'jetpack_disconnect', an error object otherwise.
*/
public static function identity_crisis_mitigation_permission_check() {
if ( current_user_can( 'jetpack_disconnect' ) ) {
return true;
}
$error_msg = esc_html__(
'You do not have the correct user permissions to perform this action.
Please contact your site admin if you think this is a mistake.',
'jetpack-connection'
);
return new WP_Error( 'invalid_user_permission_identity_crisis', $error_msg, array( 'status' => rest_authorization_required_code() ) );
}
/**
* Endpoint for URL validation and creating a secret.
*
* @since 0.18.0
*
* @return array
*/
public static function validate_urls_and_set_secret() {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->validate_urls_for_idc_mitigation();
return $result;
}
/**
* Endpoint for fetching the existing secret.
*
* @return WP_Error|\WP_REST_Response
*/
public static function fetch_url_secret() {
$secret = new URL_Secret();
if ( ! $secret->exists() ) {
return new WP_Error( 'missing_url_secret', esc_html__( 'URL secret does not exist.', 'jetpack-connection' ) );
}
return rest_ensure_response(
array(
'code' => 'success',
'data' => array(
'secret' => $secret->get_secret(),
'expires_at' => $secret->get_expires_at(),
),
)
);
}
/**
* Endpoint for comparing the existing secret.
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return WP_Error|\WP_REST_Response
*/
public static function compare_url_secret( $request ) {
$match = false;
$storage = new URL_Secret();
if ( $storage->exists() ) {
$remote_secret = $request->get_param( 'secret' );
$match = $remote_secret && hash_equals( $storage->get_secret(), $remote_secret );
}
return rest_ensure_response(
array(
'code' => 'success',
'match' => $match,
)
);
}
/**
* Verify url_secret create/fetch permissions (valid blog token authentication).
*
* @return true|WP_Error
*/
public static function url_secret_permission_check() {
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error(
'invalid_user_permission_identity_crisis',
esc_html__( 'You do not have the correct user permissions to perform this action.', 'jetpack-connection' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* The endpoint is only available on non-connected sites.
* use `/identity-crisis/url-secret` for connected sites.
*
* @return true|WP_Error
*/
public static function compare_url_secret_permission_check() {
return ( new Connection_Manager() )->is_connected()
? new WP_Error(
'invalid_connection_status',
esc_html__( 'The endpoint is not available on connected sites.', 'jetpack-connection' ),
array( 'status' => 403 )
)
: true;
}
}

View File

@@ -0,0 +1,204 @@
<?php
/**
* Identity_Crisis UI class of the Connection package.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\IdentityCrisis;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Identity_Crisis;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Tracking;
use Jetpack_Options;
use Jetpack_Tracks_Client;
/**
* The Identity Crisis UI handling.
*/
class UI {
/**
* Temporary storage for consumer data.
*
* @var array
*/
private static $consumers;
/**
* Initialization.
*/
public static function init() {
if ( did_action( 'jetpack_identity_crisis_ui_init' ) ) {
return;
}
/**
* Action called after initializing Identity Crisis UI.
*
* @since 0.6.0
*/
do_action( 'jetpack_identity_crisis_ui_init' );
$idc_data = Identity_Crisis::check_identity_crisis();
if ( false === $idc_data ) {
return;
}
add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_scripts' ) );
Tracking::register_tracks_functions_scripts( true );
}
/**
* Enqueue scripts!
*/
public static function enqueue_scripts() {
if ( is_admin() ) {
Assets::register_script(
'jp_identity_crisis_banner',
'../../dist/identity-crisis.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-connection',
)
);
Assets::enqueue_script( 'jp_identity_crisis_banner' );
wp_add_inline_script( 'jp_identity_crisis_banner', static::get_initial_state(), 'before' );
add_action( 'admin_notices', array( static::class, 'render_container' ) );
}
}
/**
* Create the container element for the IDC banner.
*/
public static function render_container() {
?>
<div id="jp-identity-crisis-container" class="notice"></div>
<?php
}
/**
* Return the rendered initial state JavaScript code.
*
* @return string
*/
private static function get_initial_state() {
return 'var JP_IDENTITY_CRISIS__INITIAL_STATE=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( static::get_initial_state_data() ) ) . '"));';
}
/**
* Get the initial state data.
*
* @return array
*/
private static function get_initial_state_data() {
$idc_urls = Identity_Crisis::get_mismatched_urls();
$current_screen = get_current_screen();
$is_admin = current_user_can( 'jetpack_disconnect' );
$possible_dynamic_site_url_detected = (bool) Identity_Crisis::detect_possible_dynamic_site_url();
$is_development_site = (bool) Status::is_development_site();
return array(
'WP_API_root' => esc_url_raw( rest_url() ),
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
'wpcomHomeUrl' => ( is_array( $idc_urls ) && array_key_exists( 'wpcom_url', $idc_urls ) ) ? $idc_urls['wpcom_url'] : null,
'currentUrl' => ( is_array( $idc_urls ) && array_key_exists( 'current_url', $idc_urls ) ) ? $idc_urls['current_url'] : null,
'redirectUri' => isset( $_SERVER['REQUEST_URI'] ) ? str_replace( '/wp-admin/', '/', filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : '',
'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
'tracksEventData' => array(
'isAdmin' => $is_admin,
'currentScreen' => $current_screen ? $current_screen->id : false,
'blogID' => Jetpack_Options::get_option( 'id' ),
'platform' => static::get_platform(),
),
'isSafeModeConfirmed' => Identity_Crisis::$is_safe_mode_confirmed,
'consumerData' => static::get_consumer_data(),
'isAdmin' => $is_admin,
'possibleDynamicSiteUrlDetected' => $possible_dynamic_site_url_detected,
'isDevelopmentSite' => $is_development_site,
/**
* Use the filter to provide custom HTML elecontainer ID.
*
* @since 0.10.0
*
* @param string|null $containerID The container ID.
*/
'containerID' => apply_filters( 'identity_crisis_container_id', null ),
);
}
/**
* Get the package consumer data.
*
* @return array
*/
public static function get_consumer_data() {
if ( null !== static::$consumers ) {
return static::$consumers;
}
$consumers = apply_filters( 'jetpack_idc_consumers', array() );
if ( ! $consumers ) {
return array();
}
usort(
$consumers,
function ( $c1, $c2 ) {
$priority1 = ( array_key_exists( 'priority', $c1 ) && (int) $c1['priority'] ) ? (int) $c1['priority'] : 10;
$priority2 = ( array_key_exists( 'priority', $c2 ) && (int) $c2['priority'] ) ? (int) $c2['priority'] : 10;
return $priority1 <=> $priority2;
}
);
$consumer_chosen = null;
$consumer_url_length = 0;
foreach ( $consumers as $consumer ) {
if ( empty( $consumer['admin_page'] ) || ! is_string( $consumer['admin_page'] ) ) {
continue;
}
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'] );
}
}
static::$consumers = $consumer_chosen ? $consumer_chosen : array_shift( $consumers );
return static::$consumers;
}
/**
* Get the site platform.
*
* @return string
*/
private static function get_platform() {
$host = new Host();
if ( $host->is_woa_site() ) {
return 'woa';
}
if ( $host->is_vip_site() ) {
return 'vip';
}
if ( $host->is_newspack_site() ) {
return 'newspack';
}
return 'self-hosted';
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* IDC URL secret functionality.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\IdentityCrisis;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\Tracking;
use Jetpack_Options;
/**
* IDC URL secret functionality.
* A short-lived secret used to verify whether an IDC is coming from the same vs a different Jetpack site.
*/
class URL_Secret {
/**
* The options key used to store the secret.
*/
const OPTION_KEY = 'identity_crisis_url_secret';
/**
* Secret lifespan (5 minutes)
*/
const LIFESPAN = 300;
/**
* The URL secret string.
*
* @var string|null
*/
private $secret = null;
/**
* The URL secret expiration date in unix timestamp.
*
* @var string|null
*/
private $expires_at = null;
/**
* Initialize the class.
*/
public function __construct() {
$secret_data = $this->fetch();
if ( $secret_data !== null ) {
$this->secret = $secret_data['secret'];
$this->expires_at = $secret_data['expires_at'];
}
}
/**
* Fetch the URL secret from the database.
*
* @return array|null
*/
private function fetch() {
$data = Jetpack_Options::get_option( static::OPTION_KEY );
if ( $data === false || empty( $data['secret'] ) || empty( $data['expires_at'] ) ) {
return null;
}
if ( time() > $data['expires_at'] ) {
Jetpack_Options::delete_option( static::OPTION_KEY );
return null;
}
return $data;
}
/**
* Create new secret and save it in the options.
*
* @throws Exception Thrown if unable to save the new secret.
*
* @return bool
*/
public function create() {
$secret_data = array(
'secret' => $this->generate_secret(),
'expires_at' => strval( time() + static::LIFESPAN ),
);
$result = Jetpack_Options::update_option( static::OPTION_KEY, $secret_data );
if ( ! $result ) {
throw new Exception( esc_html__( 'Unable to save new URL secret', 'jetpack-connection' ) );
}
$this->secret = $secret_data['secret'];
$this->expires_at = $secret_data['expires_at'];
return true;
}
/**
* Get the URL secret.
*
* @return string|null
*/
public function get_secret() {
return $this->secret;
}
/**
* Get the URL secret expiration date.
*
* @return string|null
*/
public function get_expires_at() {
return $this->expires_at;
}
/**
* Check if the secret exists.
*
* @return bool
*/
public function exists() {
return $this->secret && $this->expires_at;
}
/**
* Generate the secret string.
*
* @return string
*/
private function generate_secret() {
return wp_generate_password( 12, false );
}
/**
* Generate secret for response.
*
* @param string $flow used to tell which flow generated the exception.
* @return string|null
*/
public static function create_secret( $flow = 'generating_secret_failed' ) {
$secret_value = null;
try {
$secret = new self();
$secret->create();
if ( $secret->exists() ) {
$secret_value = $secret->get_secret();
}
} catch ( Exception $e ) {
// Track the error and proceed.
( new Tracking() )->record_user_event( $flow, array( 'current_url' => Urls::site_url() ) );
}
return $secret_value;
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* Force Jetpack 2FA Functionality
*
* Ported from original repo at https://github.com/automattic/jetpack-force-2fa
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection\SSO;
use Automattic\Jetpack\Connection\SSO;
use Automattic\Jetpack\Modules;
use WP_Error;
/**
* Force users to use two factor authentication.
*/
class Force_2FA {
/**
* The role to force 2FA for.
*
* Defaults to manage_options via the plugins_loaded function.
* Can be modified with the jetpack_force_2fa_cap filter.
*
* @var string
*/
private $role;
/**
* Constructor.
*/
public function __construct() {
add_action( 'after_setup_theme', array( $this, 'plugins_loaded' ) );
}
/**
* Load the plugin via the plugins_loaded hook.
*/
public function plugins_loaded() {
/**
* Filter the role to force 2FA for.
* Defaults to manage_options.
*
* @param string $role The role to force 2FA for.
* @return string
* @since jetpack-12.7
* @module SSO
*/
$this->role = apply_filters( 'jetpack_force_2fa_cap', 'manage_options' );
// Bail if Jetpack SSO is not active
if ( ! ( new Modules() )->is_active( 'sso' ) ) {
add_action( 'admin_notices', array( $this, 'admin_notice' ) );
return;
}
$this->force_2fa();
}
/**
* Display an admin notice if Jetpack SSO is not active.
*/
public function admin_notice() {
/**
* Filter if an admin notice is deplayed when Force 2FA is required, but SSO is not enabled.
* Defaults to true.
*
* @param bool $display_notice Whether to display the notice.
* @return bool
* @since jetpack-12.7
* @module SSO
*/
if ( apply_filters( 'jetpack_force_2fa_dependency_notice', true ) && current_user_can( $this->role ) ) {
wp_admin_notice(
esc_html__( 'Jetpack Force 2FA requires Jetpacks SSO feature.', 'jetpack-connection' ),
array(
'type' => 'warning',
)
);
}
}
/**
* Force 2FA when using Jetpack SSO and force Jetpack SSO.
*
* @return void
*/
private function force_2fa() {
// Allows WP.com login to a local account if it matches the local account.
add_filter( 'jetpack_sso_match_by_email', '__return_true', 9999 );
// multisite
if ( is_multisite() ) {
// Hide the login form
add_filter( 'jetpack_remove_login_form', '__return_true', 9999 );
add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true', 9999 );
add_filter( 'jetpack_sso_display_disclaimer', '__return_false', 9999 );
add_filter(
'wp_authenticate_user',
function () {
return new WP_Error( 'wpcom-required', $this->get_login_error_message() ); },
9999
);
add_filter( 'jetpack_sso_require_two_step', '__return_true' );
add_filter( 'allow_password_reset', '__return_false' );
} else {
// Not multisite.
// Completely disable the standard login form for admins.
add_filter(
'wp_authenticate_user',
function ( $user ) {
if ( is_wp_error( $user ) ) {
return $user;
}
if ( $user->has_cap( $this->role ) ) {
return new WP_Error( 'wpcom-required', $this->get_login_error_message(), $user->user_login );
}
return $user;
},
9999
);
add_filter(
'allow_password_reset',
function ( $allow, $user_id ) {
if ( user_can( $user_id, $this->role ) ) {
return false;
}
return $allow; },
9999,
2
);
add_action( 'jetpack_sso_pre_handle_login', array( $this, 'jetpack_set_two_step' ) );
}
}
/**
* Specifically set the two step filter for Jetpack SSO.
*
* @param Object $user_data The user data from WordPress.com.
*
* @return void
*/
public function jetpack_set_two_step( $user_data ) {
$user = SSO::get_user_by_wpcom_id( $user_data->ID );
// Borrowed from Jetpack. Ignores the match_by_email setting.
if ( empty( $user ) ) {
$user = get_user_by( 'email', $user_data->email );
}
if ( $user && $user->has_cap( $this->role ) ) {
add_filter( 'jetpack_sso_require_two_step', '__return_true' );
}
}
/**
* Get the login error message.
*
* @return string
*/
private function get_login_error_message() {
/**
* Filter the login error message.
* Defaults to a message that explains the user must use a WordPress.com account with 2FA enabled.
*
* @param string $message The login error message.
* @return string
* @since jetpack-12.7
* @module SSO
*/
return apply_filters(
'jetpack_force_2fa_login_error_message',
sprintf( 'For added security, please log in using your WordPress.com account.<br /><br />Note: Your account must have <a href="%1$s" target="_blank">Two Step Authentication</a> enabled, which can be configured from <a href="%2$s" target="_blank">Security Settings</a>.', 'https://support.wordpress.com/security/two-step-authentication/', 'https://wordpress.com/me/security/two-step' )
);
}
}

View File

@@ -0,0 +1,387 @@
<?php
/**
* A collection of helper functions used in the SSO module.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection\SSO;
use Automattic\Jetpack\Constants;
use Jetpack_IXR_Client;
/**
* A collection of helper functions used in the SSO module.
*
* @since jetpack-4.1.0
*/
class Helpers {
/**
* Determine if the login form should be hidden or not
*
* @return bool
**/
public static function should_hide_login_form() {
/**
* Remove the default log in form, only leave the WordPress.com log in button.
*
* @module sso
*
* @since jetpack-3.1.0
*
* @param bool get_option( 'jetpack_sso_remove_login_form', false ) Should the default log in form be removed. Default to false.
*/
return (bool) apply_filters( 'jetpack_remove_login_form', get_option( 'jetpack_sso_remove_login_form', false ) );
}
/**
* Returns a boolean value for whether logging in by matching the WordPress.com user email to a
* Jetpack site user's email is allowed.
*
* @return bool
*/
public static function match_by_email() {
$match_by_email = defined( 'WPCC_MATCH_BY_EMAIL' ) ? \WPCC_MATCH_BY_EMAIL : (bool) get_option( 'jetpack_sso_match_by_email', true );
/**
* Link the local account to an account on WordPress.com using the same email address.
*
* @module sso
*
* @since jetpack-2.6.0
*
* @param bool $match_by_email Should we link the local account to an account on WordPress.com using the same email address. Default to false.
*/
return (bool) apply_filters( 'jetpack_sso_match_by_email', $match_by_email );
}
/**
* Returns a boolean for whether users are allowed to register on the Jetpack site with SSO,
* even though the site disallows normal registrations.
*
* @param object|null $user_data WordPress.com user information.
* @return bool|string
*/
public static function new_user_override( $user_data = null ) {
$new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? \WPCC_NEW_USER_OVERRIDE : false;
/**
* Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations.
* If you return a string that corresponds to a user role, the user will be given that role.
*
* @module sso
*
* @since jetpack-2.6.0
* @since jetpack-4.6 $user_data object is now passed to the jetpack_sso_new_user_override filter
*
* @param bool|string $new_user_override Allow users to register on your site with a WordPress.com account. Default to false.
* @param object|null $user_data An object containing the user data returned from WordPress.com.
*/
$role = apply_filters( 'jetpack_sso_new_user_override', $new_user_override, $user_data );
if ( $role ) {
if ( is_string( $role ) && get_role( $role ) ) {
return $role;
} else {
return get_option( 'default_role' );
}
}
return false;
}
/**
* Returns a boolean value for whether two-step authentication is required for SSO.
*
* @since jetpack-4.1.0
*
* @return bool
*/
public static function is_two_step_required() {
/**
* Is it required to have 2-step authentication enabled on WordPress.com to use SSO?
*
* @module sso
*
* @since jetpack-2.8.0
*
* @param bool get_option( 'jetpack_sso_require_two_step' ) Does SSO require 2-step authentication?
*/
return (bool) apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step', false ) );
}
/**
* Returns a boolean for whether a user that is attempting to log in will be automatically
* redirected to WordPress.com to begin the SSO flow.
*
* @return bool
*/
public static function bypass_login_forward_wpcom() {
/**
* Redirect the site's log in form to WordPress.com's log in form.
*
* @module sso
*
* @since jetpack-3.1.0
*
* @param bool false Should the site's log in form be automatically forwarded to WordPress.com's log in form.
*/
return (bool) apply_filters( 'jetpack_sso_bypass_login_forward_wpcom', false );
}
/**
* Returns a boolean for whether the SSO login form should be displayed as the default
* when both the default and SSO login form allowed.
*
* @since jetpack-4.1.0
*
* @return bool
*/
public static function show_sso_login() {
if ( self::should_hide_login_form() ) {
return true;
}
/**
* Display the SSO login form as the default when both the default and SSO login forms are enabled.
*
* @module sso
*
* @since jetpack-4.1.0
*
* @param bool true Should the SSO login form be displayed by default when the default login form is also enabled?
*/
return (bool) apply_filters( 'jetpack_sso_default_to_sso_login', true );
}
/**
* Returns a boolean for whether the two step required checkbox, displayed on the Jetpack admin page, should be disabled.
*
* @since jetpack-4.1.0
*
* @return bool
*/
public static function is_require_two_step_checkbox_disabled() {
return (bool) has_filter( 'jetpack_sso_require_two_step' );
}
/**
* Returns a boolean for whether the match by email checkbox, displayed on the Jetpack admin page, should be disabled.
*
* @since jetpack-4.1.0
*
* @return bool
*/
public static function is_match_by_email_checkbox_disabled() {
return defined( 'WPCC_MATCH_BY_EMAIL' ) || has_filter( 'jetpack_sso_match_by_email' );
}
/**
* Returns an array of hosts that SSO will redirect to.
*
* Instead of accessing JETPACK__API_BASE within the method directly, we set it as the
* default for $api_base due to restrictions with testing constants in our tests.
*
* @since jetpack-4.3.0
* @since jetpack-4.6.0 Added public-api.wordpress.com as an allowed redirect
*
* @param array $hosts Allowed redirect hosts.
* @param string $api_base Base API URL.
*
* @return array
*/
public static function allowed_redirect_hosts( $hosts, $api_base = '' ) {
if ( empty( $api_base ) ) {
$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
}
if ( empty( $hosts ) ) {
$hosts = array();
}
$hosts[] = 'wordpress.com';
$hosts[] = 'jetpack.wordpress.com';
$hosts[] = 'public-api.wordpress.com';
$hosts[] = 'jetpack.com';
if ( ! str_contains( $api_base, 'jetpack.wordpress.com/jetpack' ) ) {
$base_url_parts = wp_parse_url( esc_url_raw( $api_base ) );
if ( $base_url_parts && ! empty( $base_url_parts['host'] ) ) {
$hosts[] = $base_url_parts['host'];
}
}
return array_unique( $hosts );
}
/**
* Determines how long the auth cookie is valid for when a user logs in with SSO.
*
* @return int result of the jetpack_sso_auth_cookie_expiration filter.
*/
public static function extend_auth_cookie_expiration_for_sso() {
/**
* Determines how long the auth cookie is valid for when a user logs in with SSO.
*
* @module sso
*
* @since jetpack-4.4.0
* @since jetpack-6.1.0 Fixed a typo. Filter was previously jetpack_sso_auth_cookie_expirtation.
*
* @param int YEAR_IN_SECONDS
*/
return (int) apply_filters( 'jetpack_sso_auth_cookie_expiration', YEAR_IN_SECONDS );
}
/**
* Determines if the SSO form should be displayed for the current action.
*
* @since jetpack-4.6.0
*
* @param string $action SSO action being performed.
*
* @return bool Is SSO allowed for the current action?
*/
public static function display_sso_form_for_action( $action ) {
/**
* Allows plugins the ability to overwrite actions where the SSO form is allowed to be used.
*
* @module sso
*
* @since jetpack-4.6.0
*
* @param array $allowed_actions_for_sso
*/
$allowed_actions_for_sso = (array) apply_filters(
'jetpack_sso_allowed_actions',
array(
'login',
'jetpack-sso',
'jetpack_json_api_authorization',
)
);
return in_array( $action, $allowed_actions_for_sso, true );
}
/**
* This method returns an environment array that is meant to simulate `$_REQUEST` when the initial
* JSON API auth request was made.
*
* @since jetpack-4.6.0
*
* @return array|bool
*/
public static function get_json_api_auth_environment() {
if ( empty( $_COOKIE['jetpack_sso_original_request'] ) ) {
return false;
}
$original_request = esc_url_raw( wp_unslash( $_COOKIE['jetpack_sso_original_request'] ) );
$parsed_url = wp_parse_url( $original_request );
if ( empty( $parsed_url ) || empty( $parsed_url['query'] ) ) {
return false;
}
$args = array();
wp_parse_str( $parsed_url['query'], $args );
if ( empty( $args ) || empty( $args['action'] ) ) {
return false;
}
if ( 'jetpack_json_api_authorization' !== $args['action'] ) {
return false;
}
return array_merge(
$args,
array( 'jetpack_json_api_original_query' => $original_request )
);
}
/**
* Check if the site has a custom login page URL, and return it.
* If default login page URL is used (`wp-login.php`), `null` will be returned.
*
* @return string|null
*/
public static function get_custom_login_url() {
$login_url = wp_login_url();
if ( str_ends_with( $login_url, 'wp-login.php' ) ) {
// No custom URL found.
return null;
}
$site_url = trailingslashit( site_url() );
if ( ! str_starts_with( $login_url, $site_url ) ) {
// Something went wrong, we can't properly extract the custom URL.
return null;
}
// Extracting the "path" part of the URL, because we don't need the `site_url` part.
return str_ireplace( $site_url, '', $login_url );
}
/**
* Clear the cookies that store the profile information for the last
* WPCOM user to connect.
*/
public static function clear_wpcom_profile_cookies() {
if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
setcookie(
'jetpack_sso_wpcom_name_' . COOKIEHASH,
' ',
time() - YEAR_IN_SECONDS,
COOKIEPATH,
COOKIE_DOMAIN,
is_ssl(),
true
);
}
if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
setcookie(
'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
' ',
time() - YEAR_IN_SECONDS,
COOKIEPATH,
COOKIE_DOMAIN,
is_ssl(),
true
);
}
}
/**
* Remove an SSO connection for a user.
*
* @param int $user_id The local user id.
*/
public static function delete_connection_for_user( $user_id ) {
$wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true );
if ( ! $wpcom_user_id ) {
return;
}
$xml = new Jetpack_IXR_Client(
array(
'wpcom_user_id' => $user_id,
)
);
$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
if ( $xml->isError() ) {
return false;
}
// Clean up local data stored for SSO.
delete_user_meta( $user_id, 'wpcom_user_id' );
delete_user_meta( $user_id, 'wpcom_user_data' );
self::clear_wpcom_profile_cookies();
return $xml->getResponse();
}
}

View File

@@ -0,0 +1,272 @@
<?php
/**
* A collection of helper functions used in the SSO module.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection\SSO;
use Automattic\Jetpack\Redirect;
use WP_Error;
use WP_User;
/**
* A collection of helper functions used in the SSO module.
*
* @since jetpack-4.4.0
*/
class Notices {
/**
* Error message displayed on the login form when two step is required and
* the user's account on WordPress.com does not have two step enabled.
*
* @since jetpack-2.7
* @param string $message Error message.
* @return string
**/
public static function error_msg_enable_two_step( $message ) {
$error = sprintf(
wp_kses(
/* translators: URL to settings page */
__(
'Two-Step Authentication is required to access this site. Please visit your <a href="%1$s" rel="noopener noreferrer" target="_blank">Security Settings</a> to configure <a href="%2$s" rel="noopener noreferrer" target="_blank">Two-step Authentication</a> for your account.',
'jetpack-connection'
),
array( 'a' => array( 'href' => array() ) )
),
Redirect::get_url( 'calypso-me-security-two-step' ),
Redirect::get_url( 'wpcom-support-security-two-step-authentication' )
);
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
return $message;
}
/**
* Error message displayed when the user tries to SSO, but match by email
* is off and they already have an account with their email address on
* this site.
*
* @param string $message Error message.
* @return string
*/
public static function error_msg_email_already_exists( $message ) {
$error = sprintf(
wp_kses(
/* translators: login URL */
__(
'You already have an account on this site. Please <a href="%1$s">sign in</a> with your username and password and then connect to WordPress.com.',
'jetpack-connection'
),
array( 'a' => array( 'href' => array() ) )
),
esc_url_raw( add_query_arg( 'jetpack-sso-show-default-form', '1', wp_login_url() ) )
);
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
return $message;
}
/**
* Error message that is displayed when the current site is in an identity crisis and SSO can not be used.
*
* @since jetpack-4.3.2
*
* @param string $message Error Message.
*
* @return string
*/
public static function error_msg_identity_crisis( $message ) {
$error = esc_html__( 'Logging in with WordPress.com is not currently available because this site is experiencing connection problems.', 'jetpack-connection' );
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
return $message;
}
/**
* Error message that is displayed when we are not able to verify the SSO nonce due to an XML error or
* failed validation. In either case, we prompt the user to try again or log in with username and password.
*
* @since jetpack-4.3.2
*
* @param string $message Error message.
*
* @return string
*/
public static function error_invalid_response_data( $message ) {
$error = esc_html__(
'There was an error logging you in via WordPress.com, please try again or try logging in with your username and password.',
'jetpack-connection'
);
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
return $message;
}
/**
* Error message that is displayed when we were not able to automatically create an account for a user
* after a user has logged in via SSO. By default, this message is triggered after trying to create an account 5 times.
*
* @since jetpack-4.3.2
*
* @param string $message Error message.
*
* @return string
*/
public static function error_unable_to_create_user( $message ) {
$error = esc_html__(
'There was an error creating a user for you. Please contact the administrator of your site.',
'jetpack-connection'
);
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
return $message;
}
/**
* When the default login form is hidden, this method is called on the 'authenticate' filter with a priority of 30.
* This method disables the ability to submit the default login form.
*
* @param WP_User|WP_Error $user Either the user attempting to login or an existing authentication failure.
*
* @return WP_Error
*/
public static function disable_default_login_form( $user ) {
if ( is_wp_error( $user ) ) {
return $user;
}
/**
* Since we're returning an error that will be shown as a red notice, let's remove the
* informational "blue" notice.
*/
remove_filter( 'login_message', array( static::class, 'msg_login_by_jetpack' ) );
return new WP_Error( 'jetpack_sso_required', self::get_sso_required_message() );
}
/**
* Message displayed when the site admin has disabled the default WordPress
* login form in Settings > General > Secure Sign On
*
* @since jetpack-2.7
* @param string $message Error message.
*
* @return string
**/
public static function msg_login_by_jetpack( $message ) {
$message .= sprintf( '<p class="message">%s</p>', self::get_sso_required_message() );
return $message;
}
/**
* Get the message for SSO required.
*
* @return string
*/
public static function get_sso_required_message() {
$msg = esc_html__(
'A WordPress.com account is required to access this site. Click the button below to sign in or create a free WordPress.com account.',
'jetpack-connection'
);
/**
* Filter the message displayed when the default WordPress login form is disabled.
*
* @module sso
*
* @since jetpack-2.8.0
*
* @param string $msg Disclaimer when default WordPress login form is disabled.
*/
return apply_filters( 'jetpack_sso_disclaimer_message', $msg );
}
/**
* Message displayed when the user can not be found after approving the SSO process on WordPress.com
*
* @param string $message Error message.
*
* @return string
*/
public static function cant_find_user( $message ) {
$error = __(
"We couldn't find your account. If you already have an account, make sure you have connected to WordPress.com.",
'jetpack-connection'
);
/**
* Filters the "couldn't find your account" notice after an attempted SSO.
*
* @module sso
*
* @since jetpack-10.5.0
*
* @param string $error Error text.
*/
$error = apply_filters( 'jetpack_sso_unknown_user_notice', $error );
$message .= sprintf( '<p class="message" id="login_error">%s</p>', esc_html( $error ) );
return $message;
}
/**
* Error message that is displayed when the current site is in an identity crisis and SSO can not be used.
*
* @since jetpack-4.4.0
* @deprecated since 2.10.0
*
* @param string $message Error message.
*
* @return string
*/
public static function sso_not_allowed_in_staging( $message ) {
_deprecated_function( __FUNCTION__, '2.10.0', 'sso_not_allowed_in_safe_mode' );
$error = __(
'Logging in with WordPress.com is disabled for sites that are in staging mode.',
'jetpack-connection'
);
/**
* Filters the disallowed notice for staging sites attempting SSO.
*
* @module sso
*
* @since jetpack-10.5.0
*
* @param string $error Error text.
*/
$error = apply_filters_deprecated( 'jetpack_sso_disallowed_staging_notice', array( $error ), '2.9.1', 'jetpack_sso_disallowed_safe_mode_notice' );
$message .= sprintf( '<p class="message">%s</p>', esc_html( $error ) );
return $message;
}
/**
* Error message that is displayed when the current site is in an identity crisis and SSO can not be used.
*
* @since 2.10.0
*
* @param string $message Error message.
*
* @return string
*/
public static function sso_not_allowed_in_safe_mode( $message ) {
$error = __(
'Logging in with WordPress.com is disabled for sites that are in safe mode.',
'jetpack-connection'
);
/**
* Filters the disallowed notice for sites in safe mode attempting SSO.
*
* @module sso
*
* @since 2.10.0
*
* @param string $error Error text.
*/
$error = apply_filters( 'jetpack_sso_disallowed_safe_mode_notice', $error );
$message .= sprintf( '<p class="message">%s</p>', esc_html( $error ) );
return $message;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
.jetpack-sso-admin-create-user-invite-message {
width: 550px;
}
.jetpack-sso-admin-create-user-invite-message-link-sso {
text-decoration: none;
}
#createuser .form-field textarea {
width: 25em;
}
#createuser .form-field [type=checkbox] {
width: 1rem;
}
#custom_email_message_description {
max-width: 25rem;
color: #646970;
font-size: 12px;
}

View File

@@ -0,0 +1,45 @@
document.addEventListener( 'DOMContentLoaded', function () {
const sendUserNotificationCheckbox = document.getElementById( 'send_user_notification' );
const userExternalContractorCheckbox = document.getElementById( 'user_external_contractor' );
const inviteUserWpcomCheckbox = document.getElementById( 'invite_user_wpcom' );
const customEmailMessageBlock = document.getElementById( 'custom_email_message_block' );
if ( inviteUserWpcomCheckbox && sendUserNotificationCheckbox && customEmailMessageBlock ) {
// Toggle Send User Notification checkbox enabled/disabled based on Invite User checkbox
// Enable External Contractor checkbox if Invite User checkbox is checked
// Show/hide the external email message field.
inviteUserWpcomCheckbox.addEventListener( 'change', function () {
sendUserNotificationCheckbox.disabled = inviteUserWpcomCheckbox.checked;
if ( inviteUserWpcomCheckbox.checked ) {
sendUserNotificationCheckbox.checked = false;
if ( userExternalContractorCheckbox ) {
userExternalContractorCheckbox.disabled = false;
}
customEmailMessageBlock.style.display = 'table';
} else {
if ( userExternalContractorCheckbox ) {
userExternalContractorCheckbox.disabled = true;
userExternalContractorCheckbox.checked = false;
}
customEmailMessageBlock.style.display = 'none';
}
} );
// On load, disable Send User Notification checkbox
// and show the custom email message if Invite User checkbox is checked
if ( inviteUserWpcomCheckbox.checked ) {
sendUserNotificationCheckbox.disabled = true;
sendUserNotificationCheckbox.checked = false;
customEmailMessageBlock.style.display = 'table';
}
// On load, disable External Contractor checkbox
// and hide the custom email message if Invite User checkbox is unchecked
if ( ! inviteUserWpcomCheckbox.checked ) {
if ( userExternalContractorCheckbox ) {
userExternalContractorCheckbox.disabled = true;
}
customEmailMessageBlock.style.display = 'none';
}
}
} );

View File

@@ -0,0 +1,164 @@
#loginform {
/* We set !important because sometimes static is added inline */
position: relative !important;
padding-bottom: 92px;
}
.jetpack-sso-repositioned #loginform {
padding-bottom: 26px;
}
#loginform #jetpack-sso-wrap,
#loginform #jetpack-sso-wrap * {
box-sizing: border-box;
}
#jetpack-sso-wrap__action,
#jetpack-sso-wrap__user {
display: none;
}
.jetpack-sso-form-display #jetpack-sso-wrap__action,
.jetpack-sso-form-display #jetpack-sso-wrap__user {
display: block;
}
#jetpack-sso-wrap {
position: absolute;
bottom: 20px;
padding: 0 24px;
margin-left: -24px;
margin-right: -24px;
width: 100%;
}
.jetpack-sso-repositioned #jetpack-sso-wrap {
position: relative;
bottom: auto;
padding: 0;
margin-top: 16px;
margin-left: 0;
margin-right: 0;
}
.jetpack-sso-form-display #jetpack-sso-wrap {
position: relative;
bottom: auto;
padding: 0;
margin-top: 0;
margin-left: 0;
margin-right: 0;
}
#loginform #jetpack-sso-wrap p {
color: #777777;
margin-bottom: 16px;
}
#jetpack-sso-wrap a {
display: block;
width: 100%;
text-align: center;
text-decoration: none;
}
#jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
display: none;
}
.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
display: block;
}
.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default {
display: none;
}
.jetpack-sso-form-display #loginform>p,
.jetpack-sso-form-display #loginform>div {
display: none;
}
.jetpack-sso-form-display #loginform #jetpack-sso-wrap {
display: block;
}
.jetpack-sso-form-display #loginform {
padding: 26px 24px;
}
.jetpack-sso-or {
margin-bottom: 16px;
position: relative;
text-align: center;
}
.jetpack-sso-or:before {
background: #dcdcde;
content: '';
height: 1px;
position: absolute;
left: 0;
top: 50%;
width: 100%;
}
.jetpack-sso-or span {
background: #fff;
color: #777;
position: relative;
padding: 0 8px;
text-transform: uppercase
}
#jetpack-sso-wrap .button {
display: flex;
justify-content: center;
align-items: center;
height: 36px;
margin-bottom: 16px;
width: 100%;
}
#jetpack-sso-wrap .button .genericon-wordpress {
font-size: 24px;
margin-right: 4px;
}
#jetpack-sso-wrap__user img {
border-radius: 50%;
display: block;
margin: 0 auto 16px;
}
#jetpack-sso-wrap__user h2 {
font-size: 21px;
font-weight: 300;
margin-bottom: 16px;
text-align: center;
}
#jetpack-sso-wrap__user h2 span {
font-weight: bold;
}
.jetpack-sso-wrap__reauth {
margin-bottom: 16px;
}
.jetpack-sso-form-display #nav {
display: none;
}
.jetpack-sso-form-display #backtoblog {
margin: 24px 0 0;
}
.jetpack-sso-clear:after {
content: "";
display: table;
clear: both;
}

View File

@@ -0,0 +1,27 @@
document.addEventListener( 'DOMContentLoaded', () => {
const body = document.querySelector( 'body' ),
toggleSSO = document.querySelector( '.jetpack-sso-toggle' ),
userLogin = document.getElementById( 'user_login' ),
userPassword = document.getElementById( 'user_pass' ),
ssoWrap = document.getElementById( 'jetpack-sso-wrap' ),
loginForm = document.getElementById( 'loginform' ),
overflow = document.createElement( 'div' );
overflow.className = 'jetpack-sso-clear';
loginForm.appendChild( overflow );
overflow.appendChild( document.querySelector( 'p.forgetmenot' ) );
overflow.appendChild( document.querySelector( 'p.submit' ) );
loginForm.appendChild( ssoWrap );
body.classList.add( 'jetpack-sso-repositioned' );
toggleSSO.addEventListener( 'click', e => {
e.preventDefault();
body.classList.toggle( 'jetpack-sso-form-display' );
if ( ! body.classList.contains( 'jetpack-sso-form-display' ) ) {
userLogin.focus();
userPassword.disabled = false;
}
} );
} );

View File

@@ -0,0 +1,64 @@
document.addEventListener( 'DOMContentLoaded', function () {
document
.querySelectorAll( '.jetpack-sso-invitation-tooltip-icon:not(.sso-disconnected-user)' )
.forEach( function ( tooltip ) {
tooltip.innerHTML += ' [?]';
const tooltipTextbox = document.createElement( 'span' );
tooltipTextbox.classList.add( 'jetpack-sso-invitation-tooltip', 'jetpack-sso-th-tooltip' );
const tooltipString = window.Jetpack_SSOTooltip.tooltipString;
tooltipTextbox.innerHTML += tooltipString;
tooltip.addEventListener( 'mouseenter', appendTooltip );
tooltip.addEventListener( 'focus', appendTooltip );
tooltip.addEventListener( 'mouseleave', removeTooltip );
tooltip.addEventListener( 'blur', removeTooltip );
/**
* Display the tooltip textbox.
*/
function appendTooltip() {
tooltip.appendChild( tooltipTextbox );
tooltipTextbox.style.display = 'block';
}
/**
* Remove the tooltip textbox.
*/
function removeTooltip() {
// Only remove tooltip if the element isn't currently active.
if ( document.activeElement === tooltip ) {
return;
}
tooltip.removeChild( tooltipTextbox );
}
} );
document
.querySelectorAll( '.jetpack-sso-invitation-tooltip-icon:not(.jetpack-sso-status-column)' )
.forEach( function ( tooltip ) {
tooltip.addEventListener( 'mouseenter', appendSSOInvitationTooltip );
tooltip.addEventListener( 'focus', appendSSOInvitationTooltip );
tooltip.addEventListener( 'mouseleave', removeSSOInvitationTooltip );
tooltip.addEventListener( 'blur', removeSSOInvitationTooltip );
} );
/**
* Display the SSO invitation tooltip textbox.
*/
function appendSSOInvitationTooltip() {
this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'block';
}
/**
* Remove the SSO invitation tooltip textbox.
*
* @param {Event} event - Triggering event.
*/
function removeSSOInvitationTooltip( event ) {
if ( document.activeElement === event.target ) {
return;
}
this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'none';
}
} );

View File

@@ -10,6 +10,7 @@ namespace Automattic\Jetpack\Connection\Webhooks;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Tracking;
use GP_Locales;
use Jetpack_Network;
@@ -18,11 +19,17 @@ use Jetpack_Network;
* Authorize_Redirect Webhook handler class.
*/
class Authorize_Redirect {
/**
* The Connection Manager object.
*
* @var \Automattic\Jetpack\Connection\Manager
*/
private $connection;
/**
* Constructs the object
*
* @param Automattic\Jetpack\Connection\Manager $connection The Connection Manager object.
* @param \Automattic\Jetpack\Connection\Manager $connection The Connection Manager object.
*/
public function __construct( $connection ) {
$this->connection = $connection;
@@ -32,6 +39,8 @@ class Authorize_Redirect {
* Handle the webhook
*
* This method implements what's in Jetpack::admin_page_load when the Jetpack plugin is not present
*
* @return never
*/
public function handle() {
@@ -89,46 +98,57 @@ class Authorize_Redirect {
}
/**
* Create the Jetpack authorization URL. Copied from Jetpack class.
* Create the Jetpack authorization URL.
*
* @since 2.7.6 Added optional $from and $raw 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.
*
* @todo Update default value for redirect since the called function expects a string.
*
* @return mixed|void
*/
public function build_authorize_url( $redirect = false ) {
public function build_authorize_url( $redirect = false, $from = false, $raw = false ) {
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 );
$url = $this->connection->get_authorization_url( wp_get_current_user(), $redirect, $from, $raw );
remove_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) );
remove_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) );
/**
* This filter is documented in plugins/jetpack/class-jetpack.php
* Filter the URL used when authorizing a user to a WordPress.com account.
*
* @since jetpack-8.9.0
* @since 2.7.6 Added $raw parameter.
*
* @param string $url Connection URL.
* @param bool $raw If true, URL will not be escaped.
*/
return apply_filters( 'jetpack_build_authorize_url', $url );
return apply_filters( 'jetpack_build_authorize_url', $url, $raw );
}
/**
* Filters the redirection URL that is used for connect requests. The redirect
* URL should return the user back to the Jetpack console.
* Copied from Jetpack class.
* URL should return the user back to the My Jetpack page.
*
* @param String $redirect the default redirect URL used by the package.
* @return String the modified URL.
* @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' ) );
$jetpack_admin_page = esc_url_raw( admin_url( 'admin.php?page=my-jetpack' ) );
$redirect = $redirect
? wp_validate_redirect( esc_url_raw( $redirect ), $jetpack_admin_page )
: $jetpack_admin_page;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_REQUEST['is_multisite'] ) ) {
if (
class_exists( 'Jetpack_Network' )
&& isset( $_REQUEST['is_multisite'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
) {
$redirect = Jetpack_Network::init()->get_url( 'network_admin_page' );
}
@@ -137,7 +157,6 @@ class Authorize_Redirect {
/**
* Filters the connection URL parameter array.
* Copied from Jetpack class.
*
* @param array $args default URL parameters used by the package.
* @return array the modified URL arguments array.
@@ -164,7 +183,7 @@ class Authorize_Redirect {
)
);
$calypso_env = self::get_calypso_env();
$calypso_env = ( new Host() )->get_calypso_env();
if ( ! empty( $calypso_env ) ) {
$args['calypso_env'] = $calypso_env;
@@ -178,25 +197,15 @@ class Authorize_Redirect {
* it with different Calypso enrionments, such as localhost.
* Copied from Jetpack class.
*
* @deprecated 2.7.6
*
* @since 1.37.1
*
* @return string Calypso environment
*/
public static function get_calypso_env() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['calypso_env'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return sanitize_key( $_GET['calypso_env'] );
}
_deprecated_function( __METHOD__, '2.7.6', 'Automattic\\Jetpack\\Status\\Host::get_calypso_env' );
if ( getenv( 'CALYPSO_ENV' ) ) {
return sanitize_key( getenv( 'CALYPSO_ENV' ) );
}
if ( defined( 'CALYPSO_ENV' ) && CALYPSO_ENV ) {
return sanitize_key( CALYPSO_ENV );
}
return '';
return ( new Host() )->get_calypso_env();
}
}