File "class-user-admin.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/woocommerce/vendor/automattic/jetpack-connection/src/sso/class-user-admin.php
File size: 40.12 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Extra UI elements added to the User Menu for the SSO feature.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack\Connection\SSO;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Connection\Package_Version;
use Automattic\Jetpack\Roles;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Tracking;
use WP_Error;
use WP_User;
use WP_User_Query;
/**
* Jetpack sso user admin class.
*/
class User_Admin {
/**
* Instance of WP_User_Query.
*
* @var $user_search
*/
private static $user_search = null;
/**
* Array of cached invites.
*
* @var $cached_invites
*/
private static $cached_invites = null;
/**
* Instance of Jetpack Tracking.
*
* @var $instance
*/
private static $tracking = null;
/**
* Constructor function.
*/
public function __construct() {
add_action( 'delete_user', array( Helpers::class, 'delete_connection_for_user' ) );
// If the user has no errors on creation, send an invite to WordPress.com.
add_filter( 'user_profile_update_errors', array( $this, 'send_wpcom_mail_user_invite' ), 10, 3 );
add_filter( 'wp_send_new_user_notification_to_user', array( $this, 'should_send_wp_mail_new_user' ) );
add_action( 'user_new_form', array( $this, 'render_invitation_email_message' ) );
add_action( 'user_new_form', array( $this, 'render_wpcom_invite_checkbox' ), 1 );
add_action( 'user_new_form', array( $this, 'render_wpcom_external_user_checkbox' ), 1 );
add_action( 'user_new_form', array( $this, 'render_custom_email_message_form_field' ), 1 );
add_action( 'delete_user_form', array( $this, 'render_invitations_notices_for_deleted_users' ) );
add_action( 'delete_user', array( $this, 'revoke_user_invite' ) );
add_filter( 'manage_users_columns', array( $this, 'jetpack_user_connected_th' ) );
add_filter( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 );
add_action( 'user_row_actions', array( $this, 'jetpack_user_table_row_actions' ), 10, 2 );
if ( isset( $_GET['jetpack-sso-invite-user'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) );
}
add_action( 'admin_post_jetpack_invite_user_to_wpcom', array( $this, 'invite_user_to_wpcom' ) );
add_action( 'admin_post_jetpack_revoke_invite_user_to_wpcom', array( $this, 'handle_request_revoke_invite' ) );
add_action( 'admin_post_jetpack_resend_invite_user_to_wpcom', array( $this, 'handle_request_resend_invite' ) );
add_action( 'admin_print_styles-users.php', array( $this, 'jetpack_user_table_styles' ) );
add_filter( 'users_list_table_query_args', array( $this, 'set_user_query' ), 100, 1 );
add_action( 'admin_print_styles-user-new.php', array( $this, 'jetpack_new_users_styles' ) );
self::$tracking = new Tracking();
}
/**
* Enqueue assets for user-new.php.
*/
public function jetpack_new_users_styles() {
Assets::register_script(
'jetpack-sso-admin-create-user',
'../../dist/jetpack-sso-admin-create-user.js',
__FILE__,
array(
'strategy' => 'defer',
'in_footer' => true,
'enqueue' => true,
)
);
}
/**
* Intercept the arguments for building the table, and create WP_User_Query instance
*
* @param array $args The search arguments.
*
* @return array
*/
public function set_user_query( $args ) {
self::$user_search = new WP_User_Query( $args );
return $args;
}
/**
* Revokes WordPress.com invitation.
*
* @param int $user_id The user ID.
*/
public function revoke_user_invite( $user_id ) {
try {
$has_pending_invite = self::has_pending_wpcom_invite( $user_id );
if ( $has_pending_invite ) {
$response = self::send_revoke_wpcom_invite( $has_pending_invite );
$event = 'sso_user_invite_revoked';
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$body = json_decode( wp_remote_retrieve_body( $response ) );
$tracking_event_data = array(
'success' => 'false',
'error_code' => 'invalid-revoke-api-error',
);
if ( ! empty( $body ) && ! empty( $body->message ) ) {
$tracking_event_data['error_message'] = $body->message;
}
self::$tracking->record_user_event(
$event,
$tracking_event_data
);
return $response;
}
$body = json_decode( $response['body'] );
if ( ! $body->deleted ) {
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => 'invalid-invite-revoke',
)
);
} else {
self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
}
return $response;
} else {
// Delete external contributor if it exists.
$wpcom_user_data = ( new Manager() )->get_connected_user_data( $user_id );
if ( isset( $wpcom_user_data['ID'] ) ) {
return self::delete_external_contributor( $wpcom_user_data['ID'] );
}
}
} catch ( \Exception $e ) {
return false;
}
}
/**
* Renders invitations errors/success messages in users.php.
*/
public function handle_invitation_results() {
$valid_nonce = isset( $_GET['_wpnonce'] )
? wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'jetpack-sso-invite-user' )
: false;
if ( ! $valid_nonce || ! isset( $_GET['jetpack-sso-invite-user'] ) ) {
return;
}
if ( $_GET['jetpack-sso-invite-user'] === 'success' ) {
return wp_admin_notice( __( 'User was invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) );
}
if ( $_GET['jetpack-sso-invite-user'] === 'reinvited-success' ) {
return wp_admin_notice( __( 'User was re-invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) );
}
if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) {
return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack-connection' ), array( 'type' => 'success' ) );
}
if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-api-error-message'] ) ) {
return wp_admin_notice( wp_kses( wp_unslash( $_GET['jetpack-sso-api-error-message'] ), array() ), array( 'type' => 'error' ) );
}
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-connection' ), array( 'type' => 'error' ) );
case 'invalid-email':
return wp_admin_notice( __( 'Tried to invite a user that doesn’t have an email address.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'invalid-user-permissions':
return wp_admin_notice( __( 'You don’t have permission to invite users.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'invalid-user-revoke':
return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'invalid-invite-revoke':
return wp_admin_notice( __( 'Tried to revoke an invite that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'invalid-revoke-permissions':
return wp_admin_notice( __( 'You don’t have permission to revoke invites.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'empty-invite':
return wp_admin_notice( __( 'There is no previous invite for this user', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'invalid-invite':
return wp_admin_notice( __( 'Attempted to send a new invitation to a user using an invite that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'error-revoke':
return wp_admin_notice( __( 'An error has occurred when revoking the invite for the user.', 'jetpack-connection' ), array( 'type' => 'error' ) );
case 'invalid-revoke-api-error':
return wp_admin_notice( __( 'An error has occurred when revoking the user invite.', 'jetpack-connection' ), array( 'type' => 'error' ) );
default:
return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack-connection' ), 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' );
$event = 'sso_user_invite_sent';
if ( ! current_user_can( 'create_users' ) ) {
$error = 'invalid-user-permissions';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error,
'_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,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $reason,
)
);
return self::create_error_notice_and_redirect( $query_params );
}
$blog_id = Manager::get_site_id( true );
$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'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$error_code = 'invalid-invite-api-error';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error_code,
'_wpnonce' => $nonce,
);
$tracking_event_data = array(
'success' => 'false',
'error_code' => $error_code,
);
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( ! empty( $body ) && ! empty( $body->message ) ) {
$query_params['jetpack-sso-api-error-message'] = $body->message;
$tracking_event_data['error_message'] = $body->message;
}
self::$tracking->record_user_event(
$event,
$tracking_event_data
);
return self::create_error_notice_and_redirect( $query_params );
}
$body = json_decode( wp_remote_retrieve_body( $response ) );
// access the first item since we're inviting one user.
if ( is_array( $body ) && ! empty( $body ) ) {
$body = $body[0];
}
$query_params = array(
'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed',
'_wpnonce' => $nonce,
);
if ( ! $body->success && $body->errors ) {
$response_error = array_keys( (array) $body->errors );
$query_params['jetpack-sso-invite-error'] = $response_error[0];
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $response_error[0],
)
);
} else {
self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
}
return self::create_error_notice_and_redirect( $query_params );
} else {
$error = 'invalid-user';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error,
'_wpnonce' => $nonce,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $error,
)
);
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 send_revoke_wpcom_invite( $invite_id ) {
$blog_id = Manager::get_site_id( true );
$url = '/sites/' . $blog_id . '/invites/delete';
return Client::wpcom_json_api_request_as_user(
$url,
'v2',
array(
'method' => 'POST',
),
array(
'invite_ids' => array( $invite_id ),
),
'wpcom'
);
}
/**
* 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' );
$event = 'sso_user_invite_revoked';
if ( ! current_user_can( 'promote_users' ) ) {
$error = 'invalid-revoke-permissions';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error,
'_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 ) {
$error = 'invalid-user-revoke';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error,
'_wpnonce' => $nonce,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $error,
)
);
return self::create_error_notice_and_redirect( $query_params );
}
if ( ! isset( $_GET['invite_id'] ) ) {
$error = 'invalid-invite-revoke';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error,
'_wpnonce' => $nonce,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $error,
)
);
return self::create_error_notice_and_redirect( $query_params );
}
$invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
$response = self::send_revoke_wpcom_invite( $invite_id );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$error = 'invalid-revoke-api-error';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error, // general error message
'_wpnonce' => $nonce,
);
$tracking_event_data = array(
'success' => 'false',
'error_code' => $error,
);
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( ! empty( $body ) && ! empty( $body->message ) ) {
$query_params['jetpack-sso-api-error-message'] = $body->message;
$tracking_event_data['error_message'] = $body->message;
}
self::$tracking->record_user_event(
$event,
$tracking_event_data
);
return self::create_error_notice_and_redirect( $query_params );
}
$body = json_decode( $response['body'] );
$query_params = array(
'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed',
'_wpnonce' => $nonce,
);
if ( ! $body->deleted ) { // no invite was deleted, probably it does not exist
$error = 'invalid-invite-revoke';
$query_params['jetpack-sso-invite-error'] = $error;
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $error,
)
);
} else {
self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
}
return self::create_error_notice_and_redirect( $query_params );
} else {
$error = 'invalid-user-revoke';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $error,
'_wpnonce' => $nonce,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $error,
)
);
return self::create_error_notice_and_redirect( $query_params );
}
wp_die();
}
/**
* Handles resend user invite.
*/
public function handle_request_resend_invite() {
check_admin_referer( 'jetpack-sso-resend-user-invite', 'resend_invite_nonce' );
$nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
$event = 'sso_user_invite_resend';
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['invite_id'] ) ) {
$invite_slug = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
$blog_id = Manager::get_site_id( true );
$url = '/sites/' . $blog_id . '/invites/resend';
$response = Client::wpcom_json_api_request_as_user(
$url,
'v2',
array(
'method' => 'POST',
),
array(
'invite_slug' => $invite_slug,
),
'wpcom'
);
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
$message_type = $status_code === 404 ? 'invalid-invite' : ''; // empty is the general error message
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => $message_type,
'_wpnonce' => $nonce,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $message_type,
)
);
return self::create_error_notice_and_redirect( $query_params );
}
$body = json_decode( $response['body'] );
$invite_response_message = $body->success ? 'reinvited-success' : 'failed';
$query_params = array(
'jetpack-sso-invite-user' => $invite_response_message,
'_wpnonce' => $nonce,
);
if ( ! $body->success ) {
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $invite_response_message,
)
);
} else {
self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
}
return self::create_error_notice_and_redirect( $query_params );
} else {
$error = 'empty-invite';
$query_params = array(
'jetpack-sso-invite-user' => 'failed',
'jetpack-sso-invite-error' => 'empty-invite',
'_wpnonce' => $nonce,
);
self::$tracking->record_user_event(
$event,
array(
'success' => 'false',
'error_message' => $error,
)
);
return self::create_error_notice_and_redirect( $query_params );
}
}
/**
* Adds 'Revoke invite' and 'Resend 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(
'<a class="jetpack-sso-revoke-invite-action" href="%s">%s</a>',
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-connection' )
);
}
if ( current_user_can( 'promote_users' ) && $has_pending_invite ) {
$nonce = wp_create_nonce( 'jetpack-sso-resend-user-invite' );
$actions['sso_resend_invite'] = sprintf(
'<a class="jetpack-sso-resend-invite-action" href="%s">%s</a>',
add_query_arg(
array(
'action' => 'jetpack_resend_invite_user_to_wpcom',
'user_id' => $user_id,
'resend_invite_nonce' => $nonce,
'invite_id' => $has_pending_invite,
),
admin_url( 'admin-post.php' )
),
esc_html__( 'Resend invite', 'jetpack-connection' )
);
}
if (
current_user_can( 'promote_users' )
&& (
$has_pending_invite
|| ( new Manager() )->is_user_connected( $user_id )
)
) {
unset( $actions['resetpassword'] );
}
return $actions;
}
/**
* Render the invitation email message.
*/
public function render_invitation_email_message() {
$message = wp_kses(
__(
'We highly recommend inviting users to join WordPress.com and log in securely using <a class="jetpack-sso-admin-create-user-invite-message-link-sso" rel="noopener noreferrer" target="_blank" href="https://jetpack.com/support/sso/">Secure Sign On</a> to ensure maximum security and efficiency.',
'jetpack-connection'
),
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: <strong>%s</strong>.',
'WordPress.com invitations will be automatically revoked for users: <strong>%s</strong>.',
$invites_count,
'jetpack-connection'
),
$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 WordPress.com invite checkbox for new user registration.
*
* @param string $type The type of new user form the hook follows.
*/
public function render_wpcom_invite_checkbox( $type ) {
/*
* Only check this box by default on WordPress.com sites
* that do not use the WooCommerce plugin.
*/
$is_checked = ( new Host() )->is_wpcom_platform() && ! class_exists( 'WooCommerce' );
if ( $type === 'add-new-user' ) {
?>
<table class="form-table">
<tr class="form-field">
<th scope="row">
<label for="invite_user_wpcom"><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></label>
</th>
<td>
<fieldset>
<legend class="screen-reader-text">
<span><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></span>
</legend>
<label for="invite_user_wpcom">
<input
name="invite_user_wpcom"
type="checkbox"
id="invite_user_wpcom"
<?php checked( $is_checked ); ?>
>
<?php esc_html_e( 'Invite user to WordPress.com', 'jetpack-connection' ); ?>
</label>
</fieldset>
</td>
</tr>
</table>
<?php
}
}
/**
* Render a checkbox to differentiate if a user is external.
*
* @param string $type The type of new user form the hook follows.
*/
public function render_wpcom_external_user_checkbox( $type ) {
// Only enable this feature on WordPress.com sites.
if ( ! ( new Host() )->is_wpcom_platform() ) {
return;
}
if ( $type === 'add-new-user' ) {
?>
<table class="form-table">
<tr class="form-field">
<th scope="row">
<label for="user_external_contractor"><?php esc_html_e( 'External User', 'jetpack-connection' ); ?></label>
</th>
<td>
<fieldset>
<legend class="screen-reader-text">
<span><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></span>
</legend>
<label for="user_external_contractor">
<input
name="user_external_contractor"
type="checkbox"
id="user_external_contractor"
>
<?php esc_html_e( 'This user is a contractor, freelancer, consultant, or agency.', 'jetpack-connection' ); ?>
</label>
</fieldset>
</td>
</tr>
</table>
<?php
}
}
/**
* 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( sanitize_key( $_POST['_wpnonce_create-user'] ), 'create-user' )
: false;
$custom_email_message = ( $valid_nonce && isset( $_POST['custom_email_message'] ) ) ? sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) : '';
?>
<table class="form-table" id="custom_email_message_block">
<tr class="form-field">
<th scope="row">
<label for="custom_email_message"><?php esc_html_e( 'Custom Message', 'jetpack-connection' ); ?></label>
</th>
<td>
<label for="custom_email_message">
<textarea aria-describedby="custom_email_message_description" rows="3" maxlength="500" id="custom_email_message" name="custom_email_message"><?php echo esc_html( $custom_email_message ); ?></textarea>
<p id="custom_email_message_description">
<?php
esc_html_e( 'This user will be invited to WordPress.com. You can include a personalized welcome message with the invitation.', 'jetpack-connection' );
?>
</label>
</td>
</tr>
</table>
<?php
}
}
/**
* Conditionally disable the core invitation email.
* It should be sent when SSO is disabled or when admins opt-out of WordPress.com invites intentionally.
* If the "Send User Notification" checkbox is checked, the core invitation email should be sent.
*
* @param boolean $send_wp_email Whether the core invitation email should be sent.
*
* @return boolean Indicating if the core invitation main should be sent.
*/
public function should_send_wp_mail_new_user( $send_wp_email ) {
if ( ! isset( $_POST['invite_user_wpcom'] ) && isset( $_POST['send_user_notification'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Hooked to 'wp_send_new_user_notification_to_user' to conditionally disable the core invitation email. At this point nonces should be checked already.
return $send_wp_email;
}
return false;
}
/**
* Send user invitation to WordPress.com if user has no errors.
*
* @param WP_Error $errors The WP_Error object.
* @param bool $update Whether the user is being updated or not.
* @param \stdClass $user The User object about to be created.
* @return WP_Error The modified or not WP_Error object.
*/
public function send_wpcom_mail_user_invite( $errors, $update, $user ) {
// Only admins should be able to invite new users.
if ( ! current_user_can( 'create_users' ) ) {
return $errors;
}
if ( $update ) {
return $errors;
}
// check for a valid nonce.
if (
! isset( $_POST['_wpnonce_create-user'] )
|| ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce_create-user'] ), 'create-user' )
) {
return $errors;
}
// Check if the user is being invited to WordPress.com.
if ( ! isset( $_POST['invite_user_wpcom'] ) ) {
return $errors;
}
// check if the custom email message is too long.
if (
! empty( $_POST['custom_email_message'] )
&& strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 500
) {
$errors->add(
'custom_email_message',
wp_kses(
__( '<strong>Error</strong>: The custom message is too long. Please keep it under 500 characters.', 'jetpack-connection' ),
array(
'strong' => array(),
)
)
);
}
$site_id = Manager::get_site_id( true );
if ( ! $site_id ) {
$errors->add(
'invalid_site_id',
wp_kses(
__( '<strong>Error</strong>: Invalid site ID.', 'jetpack-connection' ),
array(
'strong' => array(),
)
)
);
}
// Bail if there are any errors.
if ( $errors->has_errors() ) {
return $errors;
}
$new_user_request = array(
'email_or_username' => sanitize_email( $user->user_email ),
'role' => sanitize_key( $user->role ),
);
if (
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'] ) );
}
if ( isset( $_POST['user_external_contractor'] ) ) {
$new_user_request['is_external'] = true;
}
$response = Client::wpcom_json_api_request_as_user(
sprintf(
'/sites/%d/invites/new',
(int) $site_id
),
'2', // Api version
array(
'method' => 'POST',
),
array(
'invitees' => array( $new_user_request ),
)
);
$event_name = 'sso_new_user_invite_sent';
$custom_message_sent = isset( $new_user_request['message'] ) ? 'true' : 'false';
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$errors->add(
'invitation_not_sent',
wp_kses(
__( '<strong>Error</strong>: The user invitation email could not be sent, the user account was not created.', 'jetpack-connection' ),
array(
'strong' => array(),
)
)
);
self::$tracking->record_user_event(
$event_name,
array(
'success' => 'false',
'error' => wp_remote_retrieve_body( $response ), // Get as much information as possible.
)
);
} else {
self::$tracking->record_user_event(
$event_name,
array(
'success' => 'true',
'custom_message_sent' => $custom_message_sent,
)
);
}
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 ) {
Assets::register_script(
'jetpack-sso-users',
'../../dist/jetpack-sso-users.js',
__FILE__,
array(
'strategy' => 'defer',
'in_footer' => true,
'enqueue' => true,
'version' => Package_Version::PACKAGE_VERSION,
)
);
$tooltip_string = esc_attr__( 'Jetpack SSO allows a seamless and secure experience on WordPress.com. Join millions of WordPress users who trust us to keep their accounts safe.', 'jetpack-connection' );
wp_add_inline_script(
'jetpack-sso-users',
"var Jetpack_SSOTooltip = { 'tooltipString': '{$tooltip_string}' }",
'before'
);
$columns['user_jetpack'] = sprintf(
'<span class="jetpack-sso-invitation-tooltip-icon jetpack-sso-status-column" role="tooltip" aria-label="%3$s: %1$s" tabindex="0">%2$s</span>',
$tooltip_string,
esc_html__( 'SSO Status', 'jetpack-connection' ),
esc_attr__( 'Tooltip', 'jetpack-connection' )
);
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 = Manager::get_site_id( true );
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 ( ! ( new Manager() )->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 ( 200 === wp_remote_retrieve_response_code( $response ) ) {
$body = json_decode( $response['body'], true );
// ensure array_merge happens with the right parameters
if ( empty( $body ) ) {
$body = array();
}
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 ) && is_array( 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 = Manager::get_site_id( true );
$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 ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return false;
}
$body_response = wp_remote_retrieve_body( $response );
if ( empty( $body_response ) ) {
return false;
}
$body = json_decode( $body_response, true );
if ( ! empty( $body['invite_code'] ) ) {
return $body['invite_code'];
}
return false;
}
/**
* Delete an external contributor from the site.
*
* @access private
* @static
* @param int $user_id The user ID.
*
* @return bool Returns true if the user was successfully deleted, false otherwise.
*/
private static function delete_external_contributor( $user_id ) {
$blog_id = Manager::get_site_id( true );
$url = '/sites/' . $blog_id . '/external-contributors/remove';
$response = Client::wpcom_json_api_request_as_user(
$url,
'v2',
array(
'method' => 'POST',
),
array(
'user_id' => $user_id,
),
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return false;
}
return true;
}
/**
* 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 ) {
if ( ( new Manager() )->is_user_connected( $user_id ) ) {
$connection_html = sprintf(
'<span title="%1$s" class="jetpack-sso-invitation">%2$s</span>',
esc_attr__( 'This user is connected and can log-in to this site.', 'jetpack-connection' ),
esc_html__( 'Connected', 'jetpack-connection' )
);
return $connection_html;
} else {
$has_pending_invite = self::has_pending_wpcom_invite( $user_id );
if ( $has_pending_invite ) {
$connection_html = sprintf(
'<span title="%1$s" class="jetpack-sso-invitation sso-pending-invite">%2$s</span>',
esc_attr__( 'This user didn’t accept the invitation to join this site yet.', 'jetpack-connection' ),
esc_html__( 'Pending invite', 'jetpack-connection' )
);
return $connection_html;
}
$nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
$connection_html = sprintf(
// Using formmethod and formaction because we can't nest forms and have to submit using the main form.
'<span tabindex="0" role="tooltip" aria-label="%4$s: %3$s" class="jetpack-sso-invitation-tooltip-icon sso-disconnected-user">
<a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a>
<span class="sso-disconnected-user-icon dashicons dashicons-warning">
<span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip">%3$s</span>
</span>
</span>',
add_query_arg(
array(
'user_id' => $user_id,
'invite_nonce' => $nonce,
'action' => 'jetpack_invite_user_to_wpcom',
),
admin_url( 'admin-post.php' )
),
esc_html__( 'Send invite', 'jetpack-connection' ),
esc_attr__( 'This user doesn’t have an SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack-connection' ),
esc_attr__( 'Tooltip', 'jetpack-connection' )
);
return $connection_html;
}
}
return $val;
}
/**
* 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();
if ( empty( $ref ) ) {
$ref = network_admin_url( 'users.php' );
}
$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() {
?>
<style>
#the-list tr:has(.sso-disconnected-user) {
background: #F5F1E1;
}
#the-list tr:has(.sso-pending-invite) {
background: #E9F0F5;
}
.fixed .column-user_jetpack {
width: 100px;
}
.jetpack-sso-invitation {
background: none;
border: none;
color: #50575e;
padding: 0;
text-align: unset;
}
.jetpack-sso-invitation.sso-disconnected-user {
color: #0073aa;
cursor: pointer;
text-decoration: underline;
}
.jetpack-sso-invitation.sso-disconnected-user:hover,
.jetpack-sso-invitation.sso-disconnected-user:focus,
.jetpack-sso-invitation.sso-disconnected-user:active {
color: #0096dd;
}
.sso-disconnected-user-icon {
margin-left: 4px;
cursor: pointer;
background: gray;
border-radius: 10px;
}
.sso-disconnected-user-icon.dashicons {
font-size: 1rem;
height: 1rem;
width: 1rem;
background-color: #9D6E00;
color: #F5F1E1;
}
.jetpack-sso-invitation-tooltip-icon{
position: relative;
cursor: pointer;
}
.jetpack-sso-th-tooltip {
left: -170px;
}
.jetpack-sso-td-tooltip {
left: -256px;
}
.jetpack-sso-invitation-tooltip {
position: absolute;
background: #f6f7f7;
top: -85px;
width: 250px;
padding: 7px;
color: #3c434a;
font-size: .75rem;
line-height: 17px;
text-align: left;
margin: 0;
display: none;
border-radius: 4px;
font-family: sans-serif;
box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.1);
}
</style>
<?php
}
}