updated plugin Jetpack Protect version 3.0.2

This commit is contained in:
2024-10-09 12:44:31 +00:00
committed by Gitium
parent a35dc419bc
commit f970470c59
283 changed files with 6970 additions and 2338 deletions

View File

@ -0,0 +1,3 @@
module.exports = {
extends: [ require.resolve( 'jetpack-js-tools/eslintrc/react' ) ],
};

View File

@ -5,6 +5,113 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.0.1] - 2024-09-06
### Removed
- Removed throwing of warning if a given Jetpack options does not exist [#39270]
## [4.0.0] - 2024-09-05
### Deprecated
- Deprecated Jetpack Onboarding system. [#39229]
## [3.0.0] - 2024-09-05
### Changed
- Jetpack Connection: Restrict handling verified errors on admin pages only [#39233]
- Updated connection js to load its bundle via connection package [#38877]
- Updated package dependencies. [#39176]
### Removed
- Removed registering of Jetpack option edit_links_calypso_redirect [#39171]
### Fixed
- Fixed connection assets for wpcom simple sites [#39201]
## [2.12.5] - 2024-08-29
### Changed
- Sync: Remove the checksum for active plugins if present when sync is not active, so it gets recalculated when sync gets activated [#39098]
- Updated package dependencies. [#39111]
## [2.12.4] - 2024-08-23
### Changed
- Updated package dependencies. [#39004]
### Removed
- SSO: Removed the ability to skip the automatic login if site uses the WP.com classic interface [#38996]
## [2.12.3] - 2024-08-21
### Changed
- Internal updates.
## [2.12.2] - 2024-08-19
### Changed
- `Jetpack_Options::update_option()` now documents `$autoload` as `bool|null` to match the similar change in WordPress 6.6. String values are still accepted for as long as core's `update_option()` accepts them. [#38822]
## [2.12.1] - 2024-08-15
### Changed
- Updated package dependencies. [#38662]
## [2.12.0] - 2024-08-13
### Added
- Updated the connection initial state to fallback on the new consolidated Jetpack script data [#38825]
## [2.11.4] - 2024-08-09
### Fixed
- Fix type for tracking product string [#38748]
## [2.11.3] - 2024-08-01
### Added
- Added support for 'recommendations_evaluation' Jetpack option" [#38534]
## [2.11.2] - 2024-07-22
### Fixed
- Fixed textdomain on i18n messages imported from the IDC package. [#38412]
## [2.11.1] - 2024-07-03
### Changed
- Updated package dependencies. [#38132]
## [2.11.0] - 2024-06-26
### Added
- Add blog_id to tracks data [#37902]
## [2.10.2] - 2024-06-25
### Changed
- Internal updates.
## [2.10.1] - 2024-06-12
### Changed
- Updated package dependencies. [#37796]
## [2.10.0] - 2024-06-10
### Added
- Staging: deprecating staging mode and separating the logic into is_development_site and in_safe_mode [#37023]
### Fixed
- Jetpack Connection: Add stricter check before updating 'jetpack_connection_active_plugins' option [#37755]
## [2.9.3] - 2024-06-06
### Added
- Add mechanism to track previously working plugins [#37537]
## [2.9.2] - 2024-06-05
### Changed
- Updated package dependencies. [#37669]
## [2.9.1] - 2024-06-03
### Fixed
- Remove tabindex from tooltip modal. [#37663]
## [2.9.0] - 2024-05-29
### Added
- Move Identity Crisis handling functionality into the package. [#36968]
## [2.8.6] - 2024-05-28
### Changed
- Internal updates.
## [2.8.5] - 2024-05-27
### Fixed
- SSO: Use filter instead of action for user custom column to prevent interference with other custom columns. [#37575]
## [2.8.4] - 2024-05-22
### Deprecated
- Jetpack Connection Manager: Deprecate `request_params` arg in setup_xmlrpc_handlers method. [#37445]
@ -1080,6 +1187,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Separate the connection library into its own package.
[4.0.1]: https://github.com/Automattic/jetpack-connection/compare/v4.0.0...v4.0.1
[4.0.0]: https://github.com/Automattic/jetpack-connection/compare/v3.0.0...v4.0.0
[3.0.0]: https://github.com/Automattic/jetpack-connection/compare/v2.12.5...v3.0.0
[2.12.5]: https://github.com/Automattic/jetpack-connection/compare/v2.12.4...v2.12.5
[2.12.4]: https://github.com/Automattic/jetpack-connection/compare/v2.12.3...v2.12.4
[2.12.3]: https://github.com/Automattic/jetpack-connection/compare/v2.12.2...v2.12.3
[2.12.2]: https://github.com/Automattic/jetpack-connection/compare/v2.12.1...v2.12.2
[2.12.1]: https://github.com/Automattic/jetpack-connection/compare/v2.12.0...v2.12.1
[2.12.0]: https://github.com/Automattic/jetpack-connection/compare/v2.11.4...v2.12.0
[2.11.4]: https://github.com/Automattic/jetpack-connection/compare/v2.11.3...v2.11.4
[2.11.3]: https://github.com/Automattic/jetpack-connection/compare/v2.11.2...v2.11.3
[2.11.2]: https://github.com/Automattic/jetpack-connection/compare/v2.11.1...v2.11.2
[2.11.1]: https://github.com/Automattic/jetpack-connection/compare/v2.11.0...v2.11.1
[2.11.0]: https://github.com/Automattic/jetpack-connection/compare/v2.10.2...v2.11.0
[2.10.2]: https://github.com/Automattic/jetpack-connection/compare/v2.10.1...v2.10.2
[2.10.1]: https://github.com/Automattic/jetpack-connection/compare/v2.10.0...v2.10.1
[2.10.0]: https://github.com/Automattic/jetpack-connection/compare/v2.9.3...v2.10.0
[2.9.3]: https://github.com/Automattic/jetpack-connection/compare/v2.9.2...v2.9.3
[2.9.2]: https://github.com/Automattic/jetpack-connection/compare/v2.9.1...v2.9.2
[2.9.1]: https://github.com/Automattic/jetpack-connection/compare/v2.9.0...v2.9.1
[2.9.0]: https://github.com/Automattic/jetpack-connection/compare/v2.8.6...v2.9.0
[2.8.6]: https://github.com/Automattic/jetpack-connection/compare/v2.8.5...v2.8.6
[2.8.5]: https://github.com/Automattic/jetpack-connection/compare/v2.8.4...v2.8.5
[2.8.4]: https://github.com/Automattic/jetpack-connection/compare/v2.8.3...v2.8.4
[2.8.3]: https://github.com/Automattic/jetpack-connection/compare/v2.8.2...v2.8.3
[2.8.2]: https://github.com/Automattic/jetpack-connection/compare/v2.8.1...v2.8.2

View File

@ -0,0 +1,23 @@
<?php
/**
* Action Hooks for Jetpack connection assets.
*
* @package automattic/jetpack-connection
*/
// If WordPress's plugin API is available already, use it. If not,
// drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
if ( function_exists( 'add_action' ) ) {
add_action(
'plugins_loaded',
array( Automattic\Jetpack\Connection\Connection_Assets::class, 'configure' ),
1
);
} else {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter['plugins_loaded'][1][] = array(
'accepted_args' => 0,
'function' => array( Automattic\Jetpack\Connection\Connection_Assets::class, 'configure' ),
);
}

View File

@ -5,28 +5,32 @@
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.0",
"automattic/jetpack-a8c-mc-stats": "^2.0.1",
"automattic/jetpack-admin-ui": "^0.4.2",
"automattic/jetpack-assets": "^2.1.11",
"automattic/jetpack-constants": "^2.0.2",
"automattic/jetpack-roles": "^2.0.2",
"automattic/jetpack-status": "^3.2.0",
"automattic/jetpack-redirect": "^2.0.2"
"automattic/jetpack-a8c-mc-stats": "^2.0.2",
"automattic/jetpack-admin-ui": "^0.4.5",
"automattic/jetpack-assets": "^2.3.7",
"automattic/jetpack-constants": "^2.0.4",
"automattic/jetpack-roles": "^2.0.3",
"automattic/jetpack-status": "^4.0.1",
"automattic/jetpack-redirect": "^2.0.4"
},
"require-dev": {
"automattic/wordbless": "@dev",
"yoast/phpunit-polyfills": "1.1.0",
"yoast/phpunit-polyfills": "^1.1.1",
"brain/monkey": "2.6.1",
"automattic/jetpack-changelogger": "^4.2.4"
"automattic/jetpack-changelogger": "^4.2.6"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"autoload": {
"files": [
"actions.php"
],
"classmap": [
"legacy",
"src/",
"src/webhooks"
"src/webhooks",
"src/identity-crisis"
]
},
"scripts": {
@ -58,7 +62,7 @@
"link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "2.8.x-dev"
"dev-trunk": "4.0.x-dev"
},
"dependencies": {
"test-only": [

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'f6bce0e6b8e0527839ee');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-script-data', 'react', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '633f5b84c0735e749fc1');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
/*
* Exposes number format capability
*
* @copyright Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) and Contributors (http://phpjs.org/authors).
* @license See CREDITS.md
* @see https://github.com/kvz/phpjs/blob/ffe1356af23a6f2512c84c954dd4e828e92579fa/functions/strings/number_format.js
*/

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => 'd9dbf909a3d10fb26f39');
<?php return array('dependencies' => array(), 'version' => 'a8b23de97e9658b5993f');

View File

@ -1 +1 @@
(()=>{var e={775:e=>{let n;window._tkq=window._tkq||[];const t=console.error;const o={initialize:function(e,n){o.setUser(e,n),o.identifyUser()},mc:{bumpStat:function(e,n){const t=function(e,n){let t="";if("object"==typeof e)for(const n in e)t+="&x_"+encodeURIComponent(n)+"="+encodeURIComponent(e[n]);else t="&x_"+encodeURIComponent(e)+"="+encodeURIComponent(n);return t}(e,n);(new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom-no-pv"+t+"&t="+Math.random()}},tracks:{recordEvent:function(e,n){n=n||{},0===e.indexOf("jetpack_")?window._tkq.push(["recordEvent",e,n]):t('- Event name must be prefixed by "jetpack_"')},recordPageView:function(e){o.tracks.recordEvent("jetpack_page_view",{path:e})}},setUser:function(e,t){n={ID:e,username:t}},identifyUser:function(){n&&window._tkq.push(["identifyUser",n.ID,n.username])},clearedIdentity:function(){window._tkq.push(["clearIdentity"])}};e.exports=o}},n={};var t=function t(o){var r=n[o];if(void 0!==r)return r.exports;var i=n[o]={exports:{}};return e[o](i,i.exports,t),i.exports}(775);window.analytics=t})();
(()=>{var e={7961:e=>{let n;window._tkq=window._tkq||[];const t=console.error;const o={initialize:function(e,n){o.setUser(e,n),o.identifyUser()},mc:{bumpStat:function(e,n){const t=function(e,n){let t="";if("object"==typeof e)for(const n in e)t+="&x_"+encodeURIComponent(n)+"="+encodeURIComponent(e[n]);else t="&x_"+encodeURIComponent(e)+"="+encodeURIComponent(n);return t}(e,n);(new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom-no-pv"+t+"&t="+Math.random()}},tracks:{recordEvent:function(e,n){n=n||{},0===e.indexOf("jetpack_")?window._tkq.push(["recordEvent",e,n]):t('- Event name must be prefixed by "jetpack_"')},recordPageView:function(e){o.tracks.recordEvent("jetpack_page_view",{path:e})}},setUser:function(e,t){n={ID:e,username:t}},identifyUser:function(){n&&window._tkq.push(["identifyUser",n.ID,n.username])},clearedIdentity:function(){window._tkq.push(["clearIdentity"])}};e.exports=o}},n={};var t=function t(o){var r=n[o];if(void 0!==r)return r.exports;var i=n[o]={exports:{}};return e[o](i,i.exports,t),i.exports}(7961);window.analytics=t})();

View File

@ -77,7 +77,7 @@ class Jetpack_IXR_Client extends IXR_Client {
/**
* Perform the IXR request.
*
* @param string[] ...$args IXR args.
* @param mixed ...$args IXR method and args.
*
* @return bool True if request succeeded, false otherwise.
*/

View File

@ -40,7 +40,6 @@ class Jetpack_Options {
'allowed_xsite_search_ids', // (array) Array of WP.com blog ids that are allowed to search the content of this site
'available_modules',
'do_activate',
'edit_links_calypso_redirect', // (bool) Whether post/page edit links on front end should point to Calypso.
'log',
'slideshow_background_color',
'widget_twitter',
@ -66,7 +65,6 @@ class Jetpack_Options {
'safe_mode_confirmed', // (bool) True if someone confirms that this site was correctly put into safe mode automatically after an identity crisis is discovered.
'migrate_for_idc', // (bool) True if someone confirms that this site should migrate stats and subscribers from its previous URL
'ab_connect_banner_green_bar', // (int) Version displayed of the A/B test for the green bar at the top of the connect banner.
'onboarding', // (string) Auth token to be used in the onboarding connection flow
'tos_agreed', // (bool) Whether or not the TOS for connection has been agreed upon.
'static_asset_cdn_files', // (array) An nested array of files that we can swap out for cdn versions.
'mapbox_api_key', // (string) Mapbox API Key, for use with Map block.
@ -86,7 +84,6 @@ class Jetpack_Options {
case 'network':
return array(
'onboarding', // (string) Auth token to be used in the onboarding connection flow
'file_data', // (array) List of absolute paths to all Jetpack modules
);
}
@ -128,6 +125,9 @@ class Jetpack_Options {
'identity_crisis_url_secret', // (array) The IDC URL secret and its expiration date.
'identity_crisis_ip_requester', // (array) The IDC IP address and its expiration date.
'dismissed_welcome_banner', // (bool) Determines if the welcome banner has been dismissed or not.
'recommendations_evaluation', // (object) Catalog of recommended modules with corresponding score following successful site evaluation in Welcome Banner.
'dismissed_recommendations', // (bool) Determines if the recommendations have been dismissed or not.
'historically_active_modules', // (array) List of installed plugins/enabled modules that have at one point in time been active and working
);
}
@ -228,8 +228,6 @@ class Jetpack_Options {
}
}
trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't wish to change legacy behavior.
return $default;
}
@ -284,9 +282,9 @@ class Jetpack_Options {
/**
* Updates the single given option. Updates jetpack_options or jetpack_$name as appropriate.
*
* @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
* @param mixed $value Option value.
* @param string $autoload If not compact option, allows specifying whether to autoload or not.
* @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
* @param mixed $value Option value.
* @param bool|null $autoload If not compact option, allows specifying whether to autoload or not.
*
* @return bool Was the option successfully updated?
*/
@ -636,10 +634,6 @@ class Jetpack_Options {
'jetpack_sso_require_two_step',
'jetpack_sso_remove_login_form',
'jetpack_last_connect_url_check',
'jpo_business_address',
'jpo_site_type',
'jpo_homepage_format',
'jpo_contact_page',
'jetpack_excluded_extensions',
);
}

View File

@ -521,6 +521,7 @@ class Jetpack_XMLRPC_Server {
* Getter for the local user to act as.
*
* @param array $request the current request data.
* @return WP_User|IXR_Error|false IXR_Error if the request is missing a local_user field, WP_User object on success, or false on failure to find a user.
*/
private function fetch_and_verify_local_user( $request ) {
if ( empty( $request['local_user'] ) ) {
@ -544,6 +545,7 @@ class Jetpack_XMLRPC_Server {
* Gets the user object by its data.
*
* @param string $user_id can be any identifying user data.
* @return WP_User|false WP_User object on success, false on failure.
*/
private function get_user_by_anything( $user_id ) {
$user = get_user_by( 'login', $user_id );

View File

@ -0,0 +1,41 @@
<?php
/**
* Connection_Assets.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\Assets;
/**
* Connection_Assets class.
*/
class Connection_Assets {
/**
* Initialize the class.
*/
public static function configure() {
add_action( 'wp_loaded', array( __CLASS__, 'register_assets' ) );
add_filter( 'jetpack_admin_js_script_data', array( Initial_State::class, 'set_connection_script_data' ), 10, 1 );
}
/**
* Register assets.
*/
public static function register_assets() {
Assets::register_script(
'jetpack-connection',
'../dist/jetpack-connection.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-connection',
)
);
}
}

View File

@ -136,7 +136,8 @@ class Error_Handler {
add_action( 'rest_api_init', array( $this, 'register_verify_error_endpoint' ) );
$this->handle_verified_errors();
// Handle verified errors on admin pages.
add_action( 'admin_init', array( $this, 'handle_verified_errors' ) );
// If the site gets reconnected, clear errors.
add_action( 'jetpack_site_registered', array( $this, 'delete_all_errors' ) );

View File

@ -39,6 +39,18 @@ class Initial_State {
);
}
/**
* Set the connection script data.
*
* @param array $data The script data.
*/
public static function set_connection_script_data( $data ) {
$data['connection'] = self::get_data();
return $data;
}
/**
* Render the initial state into a JavaScript variable.
*

View File

@ -199,7 +199,10 @@ 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 ) {
@ -1879,10 +1882,10 @@ class Manager {
*
* @since 2.7.6 Added optional $from and $raw parameters.
*
* @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.
* @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
* @param bool $raw If true, URL will not be escaped.
* @param 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.
*/
@ -2123,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
*/

View File

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

View File

@ -7,6 +7,7 @@
namespace Automattic\Jetpack\Connection;
use Jetpack_Options;
use WP_Error;
/**
@ -204,9 +205,12 @@ class Plugin_Storage {
// 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 ) {
if ( $configured_plugin_keys !== $stored_plugin_keys ) {
self::update_active_plugins_option();
}
}
@ -219,9 +223,14 @@ class Plugin_Storage {
public static function update_active_plugins_option() {
// 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() ) {
self::update_active_plugins_wpcom_no_sync_fallback();
// Remove the checksum for active plugins, so it gets recalculated when sync gets activated.
$jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' );
if ( isset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] ) ) {
unset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] );
Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum );
}
}
}

View File

@ -444,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(),

View File

@ -38,7 +38,7 @@ class Tracking {
/**
* Creates the Tracking object.
*
* @param String $product_name the slug of the product that we are tracking.
* @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 ) {
@ -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

@ -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 ( ! Object.hasOwn( window, '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={
Object.hasOwn( consumerData, 'customContent' ) ? consumerData.customContent : {}
}
isAdmin={ isAdmin }
logo={ Object.hasOwn( consumerData, '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

@ -214,12 +214,14 @@ class Notices {
* 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'
@ -234,7 +236,36 @@ class Notices {
*
* @param string $error Error text.
*/
$error = apply_filters( 'jetpack_sso_disallowed_staging_notice', $error );
$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;
}

View File

@ -268,7 +268,7 @@ class SSO {
// Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed.
$classes[] = 'jetpack-sso';
if ( ! ( new Status() )->is_staging_site() ) {
if ( ! ( new Status() )->in_safe_mode() ) {
/**
* Should we show the SSO login form?
*
@ -446,13 +446,6 @@ class SSO {
return $wants_to_login;
}
/**
* Checks to determine if the user has indicated they want to use the wp-admin interface.
*/
private function use_wp_admin_interface() {
return 'wp-admin' === get_option( 'wpcom_admin_interface' );
}
/**
* Initialization for a SSO request.
*/
@ -488,8 +481,8 @@ class SSO {
if ( isset( $_GET['result'] ) && isset( $_GET['user_id'] ) && isset( $_GET['sso_nonce'] ) && 'success' === $_GET['result'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$this->handle_login();
$this->display_sso_login_form();
} elseif ( ( new Status() )->is_staging_site() ) {
add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_staging' ) );
} elseif ( ( new Status() )->in_safe_mode() ) {
add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_safe_mode' ) );
} else {
// Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) );
@ -510,7 +503,7 @@ class SSO {
* to the WordPress.com login page AND that the request to wp-login.php
* is not something other than login (Like logout!)
*/
if ( ! $this->use_wp_admin_interface() && Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) {
if ( Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) {
add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) );
$reauth = ! empty( $_GET['force_reauth'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$sso_url = $this->get_sso_url_or_die( $reauth );
@ -531,8 +524,8 @@ class SSO {
add_filter( 'login_body_class', array( $this, 'login_body_class' ) );
add_action( 'login_head', array( $this, 'print_inline_admin_css' ) );
if ( ( new Status() )->is_staging_site() ) {
add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_staging' ) );
if ( ( new Status() )->in_safe_mode() ) {
add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_safe_mode' ) );
return;
}

View File

@ -57,7 +57,7 @@ class User_Admin {
add_action( 'delete_user_form', array( $this, 'render_invitations_notices_for_deleted_users' ) );
add_action( 'delete_user', array( $this, 'revoke_user_invite' ) );
add_filter( 'manage_users_columns', array( $this, 'jetpack_user_connected_th' ) );
add_action( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 );
add_filter( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 );
add_action( 'user_row_actions', array( $this, 'jetpack_user_table_row_actions' ), 10, 2 );
add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) );
add_action( 'admin_post_jetpack_invite_user_to_wpcom', array( $this, 'invite_user_to_wpcom' ) );
@ -1203,7 +1203,7 @@ class User_Admin {
'<span tabindex="0" role="tooltip" aria-label="%4$s: %3$s" class="jetpack-sso-invitation-tooltip-icon sso-disconnected-user">
<a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a>
<span class="sso-disconnected-user-icon dashicons dashicons-warning">
<span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip" tabindex="0">%3$s</span>
<span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip">%3$s</span>
</span>
</span>',
add_query_arg(
@ -1221,6 +1221,7 @@ class User_Admin {
return $connection_html;
}
}
return $val;
}
/**