diff --git a/projects/plugins/jetpack/changelog/Add column tooltip b/projects/plugins/jetpack/changelog/Add column tooltip
new file mode 100644
index 0000000000000..eaf8f0319d40b
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/Add column tooltip
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Rename status column to sso status and add tooltip
diff --git a/projects/plugins/jetpack/changelog/Improve column b/projects/plugins/jetpack/changelog/Improve column
new file mode 100644
index 0000000000000..a31f898fa711a
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/Improve column
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Improve column by renaming and adding icon
diff --git a/projects/plugins/jetpack/changelog/Improve question mark icon b/projects/plugins/jetpack/changelog/Improve question mark icon
new file mode 100644
index 0000000000000..48020c9692aab
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/Improve question mark icon
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Replace question mark icon
diff --git a/projects/plugins/jetpack/changelog/add-new-user-additional-email-message b/projects/plugins/jetpack/changelog/add-new-user-additional-email-message
new file mode 100644
index 0000000000000..88b9200284912
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-new-user-additional-email-message
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Added custom message textarea to send a message via email when adding new users
diff --git a/projects/plugins/jetpack/changelog/add-sso-cached-invites b/projects/plugins/jetpack/changelog/add-sso-cached-invites
new file mode 100644
index 0000000000000..bafd1b5be8879
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-sso-cached-invites
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Add caching for user invites
diff --git a/projects/plugins/jetpack/changelog/add-sso-move-to-admin-class b/projects/plugins/jetpack/changelog/add-sso-move-to-admin-class
new file mode 100644
index 0000000000000..db0210bf486df
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-sso-move-to-admin-class
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Move user customization to seperate file
diff --git a/projects/plugins/jetpack/changelog/add-sso-users-table-revoke-invite b/projects/plugins/jetpack/changelog/add-sso-users-table-revoke-invite
new file mode 100644
index 0000000000000..c27ee599f5211
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-sso-users-table-revoke-invite
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+SSO: Add user invite revoke row action in users table
diff --git a/projects/plugins/jetpack/changelog/fix-registration-user-invite b/projects/plugins/jetpack/changelog/fix-registration-user-invite
new file mode 100644
index 0000000000000..fd0944f49ab9c
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/fix-registration-user-invite
@@ -0,0 +1,4 @@
+Significance: patch
+Type: other
+
+Trigger user invitation for new users
diff --git a/projects/plugins/jetpack/changelog/fix-translations b/projects/plugins/jetpack/changelog/fix-translations
new file mode 100644
index 0000000000000..da549f7ebc073
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/fix-translations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: other
+
+Fix message translation content
diff --git a/projects/plugins/jetpack/changelog/improve-binding-local-remote-users b/projects/plugins/jetpack/changelog/improve-binding-local-remote-users
new file mode 100644
index 0000000000000..0c66cb9580a7b
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/improve-binding-local-remote-users
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+SSO: improve messaging and account binding between local and wp.com users
diff --git a/projects/plugins/jetpack/changelog/improve_invite_form b/projects/plugins/jetpack/changelog/improve_invite_form
new file mode 100644
index 0000000000000..070491429e8f6
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/improve_invite_form
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+SSO: When creating a new users, mail the users with an invitation to WPCom.
diff --git a/projects/plugins/jetpack/changelog/revoke-deleted-users b/projects/plugins/jetpack/changelog/revoke-deleted-users
new file mode 100644
index 0000000000000..39682160b4ddd
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/revoke-deleted-users
@@ -0,0 +1,4 @@
+Significance: patch
+Type: other
+
+SSO: revoke invites sent to users upon users deletion
diff --git a/projects/plugins/jetpack/changelog/update-improve-users-table b/projects/plugins/jetpack/changelog/update-improve-users-table
new file mode 100644
index 0000000000000..df3e90478e918
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/update-improve-users-table
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+SSO: Updated column heading and row background color when invitation is pending.
diff --git a/projects/plugins/jetpack/changelog/update-user-new-save-form-data b/projects/plugins/jetpack/changelog/update-user-new-save-form-data
new file mode 100644
index 0000000000000..41ad15adbcb42
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/update-user-new-save-form-data
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Persist user-new.php custom message form field after submission with errors
diff --git a/projects/plugins/jetpack/changelog/update-users-table-sso-background-colors b/projects/plugins/jetpack/changelog/update-users-table-sso-background-colors
new file mode 100644
index 0000000000000..336db0aa5a0e9
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/update-users-table-sso-background-colors
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Update users table ssorow background colors
diff --git a/projects/plugins/jetpack/class.jetpack.php b/projects/plugins/jetpack/class.jetpack.php
index ed3f0cd155c05..7750ec98e9c36 100644
--- a/projects/plugins/jetpack/class.jetpack.php
+++ b/projects/plugins/jetpack/class.jetpack.php
@@ -8,7 +8,6 @@
*/
use Automattic\Jetpack\Assets;
-use Automattic\Jetpack\Assets\Logo as Jetpack_Logo;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
use Automattic\Jetpack\Config;
use Automattic\Jetpack\Connection\Client;
@@ -3363,11 +3362,6 @@ public function admin_init() {
// Artificially throw errors in certain specific cases during plugin activation.
add_action( 'activate_plugin', array( $this, 'throw_error_on_activate_plugin' ) );
}
-
- // Add custom column in wp-admin/users.php to show whether user is linked.
- add_filter( 'manage_users_columns', array( $this, 'jetpack_icon_user_connected' ) );
- add_action( 'manage_users_custom_column', array( $this, 'jetpack_show_user_connected_icon' ), 10, 3 );
- add_action( 'admin_print_styles', array( $this, 'jetpack_user_col_style' ) );
}
/**
@@ -6410,65 +6404,6 @@ public function get_user_option_meta_box_order_dashboard( $sorted ) {
return $sorted;
}
-
- /**
- * Adds a "blank" column in the user admin table to display indication of user connection.
- *
- * @param array $columns User list table columns.
- *
- * @return array
- */
- public function jetpack_icon_user_connected( $columns ) {
- $columns['user_jetpack'] = '';
- return $columns;
- }
-
- /**
- * Show Jetpack icon if the user is linked.
- *
- * @param string $val HTML for the icon.
- * @param string $col User list table column.
- * @param int $user_id User ID.
- *
- * @return string
- */
- public function jetpack_show_user_connected_icon( $val, $col, $user_id ) {
- if ( 'user_jetpack' === $col && self::connection()->is_user_connected( $user_id ) ) {
- $jetpack_logo = new Jetpack_Logo();
- $emblem_html = sprintf(
- '%2$s',
- esc_attr__( 'This user is linked and ready to fly with Jetpack.', 'jetpack' ),
- $jetpack_logo->get_jp_emblem()
- );
- return $emblem_html;
- }
-
- return $val;
- }
-
- /**
- * Style the Jetpack user column
- */
- public function jetpack_user_col_style() {
- global $current_screen;
- if ( ! empty( $current_screen->base ) && 'users' === $current_screen->base ) {
- ?>
-
- errors['loggedout'] ) ) {
$logout_message = wp_kses(
sprintf(
- /* translators: %1$s is a link to the WordPress.com account settings page. */
+ /* translators: %1$s is a link to the WordPress.com account settings page. */
__( 'If you are on a shared computer, remember to also log out of WordPress.com.', 'jetpack' ),
'https://wordpress.com/me'
),
@@ -143,7 +145,7 @@ public function maybe_logout_user() {
if ( 1 === (int) $current_user->jetpack_force_logout ) {
delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
- self::delete_connection_for_user( $current_user->ID );
+ Jetpack_SSO_Helpers::delete_connection_for_user( $current_user->ID );
wp_logout();
wp_safe_redirect( wp_login_url() );
exit;
@@ -180,7 +182,7 @@ public function xmlrpc_user_disconnect( $user_id ) {
if ( $user instanceof WP_User ) {
$user = wp_set_current_user( $user->ID );
update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
- self::delete_connection_for_user( $user->ID );
+ Jetpack_SSO_Helpers::delete_connection_for_user( $user->ID );
return true;
}
return false;
@@ -320,10 +322,10 @@ public function render_require_two_step() {
-
+
+
>
-
+
-
+
+
>
-
+
-
+
- %s', 'jetpack' ), esc_html( $display_name ) ),
- array( 'span' => true )
- );
- ?>
+ %s', 'jetpack' ), esc_html( $display_name ) ),
+ array( 'span' => true )
+ );
+ ?>
-
+
- build_sso_button( array(), 'is_primary' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping done in build_sso_button() ?>
+ build_sso_button( array(), 'is_primary' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping done in build_sso_button() ?>
-
+
@@ -610,18 +612,18 @@ public function login_form() {
-
+
@@ -637,39 +639,9 @@ public function login_form() {
esc_html_e( 'Log in with WordPress.com', 'jetpack' )
?>
-
+
- is_user_connected() ) {
- static::delete_connection_for_user( get_current_user_id() );
+ Jetpack_SSO_Helpers::delete_connection_for_user( get_current_user_id() );
}
}
- /**
- * 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();
- }
-
/**
* Retrieves nonce used for SSO form.
*/
public static function request_initial_nonce() {
$nonce = ! empty( $_COOKIE['jetpack_sso_nonce'] )
- ? sanitize_key( wp_unslash( $_COOKIE['jetpack_sso_nonce'] ) )
- : false;
+ ? sanitize_key( wp_unslash( $_COOKIE['jetpack_sso_nonce'] ) )
+ : false;
if ( ! $nonce ) {
$xml = new Jetpack_IXR_Client();
@@ -904,8 +846,8 @@ public function handle_login() {
}
$user_found_with = $new_user_override_role
- ? 'user_created_new_user_override'
- : 'user_created_users_can_register';
+ ? 'user_created_new_user_override'
+ : 'user_created_users_can_register';
} else {
$tracking->record_user_event(
'sso_login_failed',
@@ -991,7 +933,7 @@ public function handle_login() {
add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
wp_safe_redirect(
- /** This filter is documented in core/src/wp-login.php */
+ /** This filter is documented in core/src/wp-login.php */
apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
);
exit;
@@ -1016,7 +958,7 @@ public function handle_login() {
}
/**
- * Retreive the admin profile page URL.
+ * Retrieve the admin profile page URL.
*/
public static function profile_page_url() {
return admin_url( 'profile.php' );
@@ -1032,8 +974,8 @@ public static function profile_page_url() {
public function build_sso_button( $args = array(), $is_primary = false ) {
$url = $this->build_sso_button_url( $args );
$classes = $is_primary
- ? 'jetpack-sso button button-primary'
- : 'jetpack-sso button';
+ ? 'jetpack-sso button button-primary'
+ : 'jetpack-sso button';
return sprintf(
'%3$s %4$s',
@@ -1080,7 +1022,7 @@ public function get_sso_url_or_die( $reauth = false, $args = array() ) {
if ( empty( $reauth ) ) {
$sso_redirect = $this->build_sso_url( $args );
} else {
- self::clear_wpcom_profile_cookies();
+ Jetpack_SSO_Helpers::clear_wpcom_profile_cookies();
$sso_redirect = $this->build_reauth_and_sso_url( $args );
}
diff --git a/projects/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php b/projects/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php
index d2e3053754122..24ffd1a3b9f71 100644
--- a/projects/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php
+++ b/projects/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php
@@ -366,6 +366,66 @@ public static function get_custom_login_url() {
// 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();
+ }
}
endif;
diff --git a/projects/plugins/jetpack/modules/sso/class.jetpack-sso-user-admin.php b/projects/plugins/jetpack/modules/sso/class.jetpack-sso-user-admin.php
new file mode 100644
index 0000000000000..63fae88071697
--- /dev/null
+++ b/projects/plugins/jetpack/modules/sso/class.jetpack-sso-user-admin.php
@@ -0,0 +1,722 @@
+ 'success' ) );
+ }
+
+ if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) {
+ return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack' ), array( 'type' => 'success' ) );
+ }
+
+ if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-invite-error'] ) ) {
+ switch ( $_GET['jetpack-sso-invite-error'] ) {
+ case 'invalid-user':
+ return wp_admin_notice( __( 'Tried to invite a user that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) );
+ case 'invalid-email':
+ return wp_admin_notice( __( 'Tried to invite a user that doesn’t have an email address.', 'jetpack' ), array( 'type' => 'error' ) );
+ case 'invalid-user-permissions':
+ return wp_admin_notice( __( 'You don’t have permission to invite users.', 'jetpack' ), array( 'type' => 'error' ) );
+ case 'invalid-user-revoke':
+ return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) );
+ case 'invalid-invite-revoke':
+ return wp_admin_notice( __( 'Tried to revoke an invite that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) );
+ case 'invalid-revoke-permissions':
+ return wp_admin_notice( __( 'You don’t have permission to revoke invites.', 'jetpack' ), array( 'type' => 'error' ) );
+ default:
+ return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack' ), array( 'type' => 'error' ) );
+ }
+ }
+ }
+
+ /**
+ * Invites a user to connect to WordPress.com to allow them to log in via SSO.
+ */
+ public function invite_user_to_wpcom() {
+ check_admin_referer( 'jetpack-sso-invite-user', 'invite_nonce' );
+ $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
+
+ if ( ! current_user_can( 'create_users' ) ) {
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => 'invalid-user-permissions',
+ '_wpnonce' => $nonce,
+ );
+ return self::create_error_notice_and_redirect( $query_params );
+ } elseif ( isset( $_GET['user_id'] ) ) {
+ $user_id = intval( wp_unslash( $_GET['user_id'] ) );
+ $user = get_user_by( 'id', $user_id );
+ $user_email = $user->user_email;
+
+ if ( ! $user || ! $user_email ) {
+ $reason = ! $user ? 'invalid-user' : 'invalid-email';
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => $reason,
+ '_wpnonce' => $nonce,
+ );
+
+ return self::create_error_notice_and_redirect( $query_params );
+ }
+
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $roles = new Roles();
+ $user_role = $roles->translate_user_to_role( $user );
+
+ $url = '/sites/' . $blog_id . '/invites/new';
+ $response = Client::wpcom_json_api_request_as_user(
+ $url,
+ 'v2',
+ array(
+ 'method' => 'POST',
+ ),
+ array(
+ 'invitees' => array(
+ array(
+ 'email_or_username' => $user_email,
+ 'role' => $user_role,
+ ),
+ ),
+ ),
+ 'wpcom'
+ );
+
+ // access the first item since we're inviting one user.
+ $body = json_decode( $response['body'] )[0];
+
+ $query_params = array(
+ 'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed',
+ '_wpnonce' => $nonce,
+ );
+
+ if ( ! $body->success ) {
+ $query_params = array(
+ 'jetpack-sso-invite-error' => $body->errors[0],
+ );
+ }
+ return self::create_error_notice_and_redirect( $query_params );
+ } else {
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => 'invalid-user',
+ '_wpnonce' => $nonce,
+ );
+ return self::create_error_notice_and_redirect( $query_params );
+ }
+ wp_die();
+ }
+
+ /**
+ * Revokes a user's invitation to connect to WordPress.com.
+ *
+ * @param string $invite_id The ID of the invite to revoke.
+ */
+ public function revoke_wpcom_invite( $invite_id ) {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+
+ $url = '/sites/' . $blog_id . '/invites/delete';
+ $response = Client::wpcom_json_api_request_as_user(
+ $url,
+ 'v2',
+ array(
+ 'method' => 'POST',
+ ),
+ array(
+ 'invite_ids' => array( $invite_id ),
+ ),
+ 'wpcom'
+ );
+
+ return json_decode( $response['body'] );
+ }
+
+ /**
+ * Handles logic to revoke user invite.
+ */
+ public function handle_request_revoke_invite() {
+ check_admin_referer( 'jetpack-sso-revoke-user-invite', 'revoke_invite_nonce' );
+ $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
+
+ if ( ! current_user_can( 'promote_users' ) ) {
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => 'invalid-revoke-permissions',
+ '_wpnonce' => $nonce,
+ );
+
+ return self::create_error_notice_and_redirect( $query_params );
+ } elseif ( isset( $_GET['user_id'] ) ) {
+ $user_id = intval( wp_unslash( $_GET['user_id'] ) );
+ $user = get_user_by( 'id', $user_id );
+
+ if ( ! $user ) {
+
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => 'invalid-user-revoke',
+ '_wpnonce' => $nonce,
+ );
+
+ return self::create_error_notice_and_redirect( $query_params );
+ }
+
+ if ( ! isset( $_GET['invite_id'] ) ) {
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => 'invalid-invite-revoke',
+ '_wpnonce' => $nonce,
+ );
+ return self::create_error_notice_and_redirect( $query_params );
+ }
+
+ $invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
+ $body = self::revoke_wpcom_invite( $invite_id );
+ $query_params = array(
+ 'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed',
+ '_wpnonce' => $nonce,
+ );
+
+ if ( ! $body->deleted ) {
+ $query_params['jetpack-sso-invite-error'] = 'invalid-invite-revoke';
+ }
+ return self::create_error_notice_and_redirect( $query_params );
+ } else {
+ $query_params = array(
+ 'jetpack-sso-invite-user' => 'failed',
+ 'jetpack-sso-invite-error' => 'invalid-user-revoke',
+ '_wpnonce' => $nonce,
+ );
+ return self::create_error_notice_and_redirect( $query_params );
+ }
+ wp_die();
+ }
+
+ /**
+ * Adds 'Revoke invite' link to user table row actions.
+ * Removes 'Reset password' link.
+ *
+ * @param array $actions - User row actions.
+ * @param WP_User $user_object - User object.
+ */
+ public function jetpack_user_table_row_actions( $actions, $user_object ) {
+ $user_id = $user_object->ID;
+ $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
+
+ if ( current_user_can( 'promote_users' ) && $has_pending_invite ) {
+ $nonce = wp_create_nonce( 'jetpack-sso-revoke-user-invite' );
+ $actions['sso_revoke_invite'] = sprintf(
+ '%s',
+ add_query_arg(
+ array(
+ 'action' => 'jetpack_revoke_invite_user_to_wpcom',
+ 'user_id' => $user_id,
+ 'revoke_invite_nonce' => $nonce,
+ 'invite_id' => $has_pending_invite,
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ esc_html__( 'Revoke invite', 'jetpack' )
+ );
+ }
+
+ unset( $actions['resetpassword'] );
+
+ return $actions;
+ }
+
+ /**
+ * Render the invitation email message.
+ */
+ public function render_invitation_email_message() {
+ $message = wp_kses(
+ __(
+ 'New users will receive an invite to join WordPress.com, so they can log in securely using Secure Sign On.',
+ 'jetpack'
+ ),
+ array(
+ 'a' => array(
+ 'class' => array(),
+ 'href' => array(),
+ 'rel' => array(),
+ 'target' => array(),
+ ),
+ )
+ );
+ wp_admin_notice(
+ $message,
+ array(
+ 'id' => 'invitation_message',
+ 'type' => 'info',
+ 'dismissible' => false,
+ 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ),
+ )
+ );
+ }
+
+ /**
+ * Render a note that wp.com invites will be automatically revoked.
+ */
+ public function render_invitations_notices_for_deleted_users() {
+ check_admin_referer( 'bulk-users' );
+
+ // When one user is deleted, the param is `user`, when multiple users are deleted, the param is `users`.
+ // We start with `users` and fallback to `user`.
+ $user_id = isset( $_GET['user'] ) ? intval( wp_unslash( $_GET['user'] ) ) : null;
+ $user_ids = isset( $_GET['users'] ) ? array_map( 'intval', wp_unslash( $_GET['users'] ) ) : array( $user_id );
+
+ $users_with_invites = array_filter(
+ $user_ids,
+ function ( $user_id ) {
+ return $user_id !== null && self::has_pending_wpcom_invite( $user_id );
+ }
+ );
+
+ $users_with_invites = array_map(
+ function ( $user_id ) {
+ $user = get_user_by( 'id', $user_id );
+ return $user->user_login;
+ },
+ $users_with_invites
+ );
+
+ $invites_count = count( $users_with_invites );
+ if ( $invites_count > 0 ) {
+ $users_with_invites = implode( ', ', $users_with_invites );
+ $message = wp_kses(
+ sprintf(
+ /* translators: %s is a comma-separated list of user logins. */
+ _n(
+ 'WordPress.com invitation will be automatically revoked for user: %s.',
+ 'WordPress.com invitations will be automatically revoked for users: %s.',
+ $invites_count,
+ 'jetpack'
+ ),
+ $users_with_invites
+ ),
+ array( 'strong' => true )
+ );
+ wp_admin_notice(
+ $message,
+ array(
+ 'id' => 'invitation_message',
+ 'type' => 'info',
+ 'dismissible' => false,
+ 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ),
+ )
+ );
+ }
+ }
+
+ /**
+ * Render the custom email message form field for new user registration.
+ *
+ * @param string $type The type of new user form the hook follows.
+ */
+ public function render_custom_email_message_form_field( $type ) {
+ if ( $type === 'add-new-user' ) {
+ $valid_nonce = isset( $_POST['_wpnonce_create-user'] ) ? wp_verify_nonce( $_POST['_wpnonce_create-user'], 'create-user' ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
+ $custom_email_message = ( $valid_nonce && isset( $_POST['custom_email_message'] ) ) ? sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) : '';
+ ?>
+
+ 500 ) {
+ $errors->add( 'custom_email_message', __( 'Error: The custom message is too long. Please keep it under 500 characters.', 'jetpack' ) );
+ }
+
+ if ( $errors->has_errors() ) {
+ return $errors;
+ }
+
+ $email = $user->user_email;
+ $role = $user->role;
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $url = '/sites/' . $blog_id . '/invites/new';
+
+ $new_user_request = array(
+ 'email_or_username' => $email,
+ 'role' => $role,
+ );
+
+ if ( $valid_nonce && isset( $_POST['custom_email_message'] ) && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) > 0 ) ) {
+ $new_user_request['message'] = sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) );
+ }
+
+ $response = Client::wpcom_json_api_request_as_user(
+ $url,
+ '2', // Api version
+ array(
+ 'method' => 'POST',
+ ),
+ array(
+ 'invitees' => array( $new_user_request ),
+ )
+ );
+
+ if ( 200 !== $response['response']['code'] ) {
+ $errors->add( 'invitation_not_sent', __( 'Error: The user invitation email could not be sent, the user account was not created.', 'jetpack' ) );
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Adds a column in the user admin table to display user connection status and actions.
+ *
+ * @param array $columns User list table columns.
+ *
+ * @return array
+ */
+ public function jetpack_user_connected_th( $columns ) {
+ $columns['user_jetpack'] = sprintf(
+ '%2$s [?]',
+ esc_attr__( 'Jetpack SSO is required for a seamless and secure experience on WordPress.com. Join millions of WordPress users who trust us to keep their accounts safe.', 'jetpack' ),
+ esc_html__( 'SSO Status', 'jetpack' )
+ );
+ return $columns;
+ }
+
+ /**
+ * Executed when our WP_User_Query instance is set, and we don't have cached invites.
+ * This function uses the user emails and the 'are-users-invited' endpoint to build the cache.
+ *
+ * @return void
+ */
+ private static function rebuild_invite_cache() {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+
+ if ( self::$cached_invites === null && self::$user_search !== null ) {
+
+ self::$cached_invites = array();
+
+ $results = self::$user_search->get_results();
+
+ $user_emails = array_reduce(
+ $results,
+ function ( $current, $item ) {
+ if ( ! Jetpack::connection()->is_user_connected( $item->ID ) ) {
+ $current[] = rawurlencode( $item->user_email );
+ } else {
+ self::$cached_invites[] = array(
+ 'email_or_username' => $item->user_email,
+ 'invited' => false,
+ 'invite_code' => '',
+ );
+ }
+ return $current;
+ },
+ array()
+ );
+
+ if ( ! empty( $user_emails ) ) {
+ $url = '/sites/' . $blog_id . '/invites/are-users-invited';
+
+ $response = Client::wpcom_json_api_request_as_user(
+ $url,
+ 'v2',
+ array(
+ 'method' => 'POST',
+ ),
+ array( 'users' => $user_emails ),
+ 'wpcom'
+ );
+
+ if ( ! is_wp_error( $response ) && 200 === $response['response']['code'] ) {
+ $body = json_decode( $response['body'], true );
+ self::$cached_invites = array_merge( self::$cached_invites, $body );
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if there is cached invite for a user email.
+ *
+ * @access private
+ * @static
+ *
+ * @param string $email The user email.
+ *
+ * @return array|void Returns the cached invite if found.
+ */
+ public static function get_pending_cached_wpcom_invite( $email ) {
+ if ( self::$cached_invites === null ) {
+ self::rebuild_invite_cache();
+ }
+
+ if ( ! empty( self::$cached_invites ) ) {
+ $index = array_search( $email, array_column( self::$cached_invites, 'email_or_username' ), true );
+ if ( $index !== false ) {
+ return self::$cached_invites[ $index ];
+ }
+ }
+ }
+
+ /**
+ * Check if a given user is invited to the site.
+ *
+ * @access private
+ * @static
+ * @param int $user_id The user ID.
+ *
+ * @return {false|string} returns the user invite code if the user is invited, false otherwise.
+ */
+ private static function has_pending_wpcom_invite( $user_id ) {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $user = get_user_by( 'id', $user_id );
+ $cached_invite = self::get_pending_cached_wpcom_invite( $user->user_email );
+
+ if ( $cached_invite ) {
+ return $cached_invite['invite_code'];
+ }
+
+ $url = '/sites/' . $blog_id . '/invites/is-invited';
+ $url = add_query_arg(
+ array(
+ 'email_or_username' => rawurlencode( $user->user_email ),
+ ),
+ $url
+ );
+ $response = Client::wpcom_json_api_request_as_user(
+ $url,
+ 'v2',
+ array(),
+ null,
+ 'wpcom'
+ );
+ if ( is_wp_error( $response ) ) {
+ return false;
+ }
+
+ if ( 200 !== $response['response']['code'] ) {
+ return false;
+ }
+
+ return json_decode( $response['body'], true )['invite_code'];
+ }
+
+ /**
+ * Show Jetpack SSO user connection status.
+ *
+ * @param string $val HTML for the column.
+ * @param string $col User list table column.
+ * @param int $user_id User ID.
+ *
+ * @return string
+ */
+ public function jetpack_show_connection_status( $val, $col, $user_id ) {
+ if ( 'user_jetpack' === $col && Jetpack::connection()->is_user_connected( $user_id ) ) {
+ $connection_html = sprintf(
+ '%2$s',
+ esc_attr__( 'This user is connected and can log-in to this site.', 'jetpack' ),
+ esc_html__( 'Connected', 'jetpack' )
+ );
+ return $connection_html;
+ } else {
+ $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
+ if ( $has_pending_invite ) {
+ $connection_html = sprintf(
+ '%2$s',
+ esc_attr__( 'This user didn’t accept the invitation to join this site yet.', 'jetpack' ),
+ esc_html__( 'Pending invite', 'jetpack' )
+ );
+ return $connection_html;
+ }
+ $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
+ $connection_html = sprintf(
+ // Using formmethod and formaction because we can't nest forms and have to submit using the main form.
+ '%2$s',
+ add_query_arg(
+ array(
+ 'user_id' => $user_id,
+ 'invite_nonce' => $nonce,
+ 'action' => 'jetpack_invite_user_to_wpcom',
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ esc_html__( 'Send invite', 'jetpack' ),
+ esc_attr__( 'This user doesn’t have a WP.com account and, with your current site settings, won’t be able to log in. Request them to create a WP.com account to be able to function normally.', 'jetpack' )
+ );
+ return $connection_html;
+ }
+ }
+
+ /**
+ * Creates error notices and redirects the user to the previous page.
+ *
+ * @param array $query_params - query parameters added to redirection URL.
+ */
+ public function create_error_notice_and_redirect( $query_params ) {
+ $ref = wp_get_referer();
+ $url = add_query_arg(
+ $query_params,
+ $ref
+ );
+ return wp_safe_redirect( $url );
+ }
+
+ /**
+ * Style the Jetpack user rows and columns.
+ */
+ public function jetpack_user_table_styles() {
+ ?>
+
+