Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 821
0.00% covered (danger)
0.00%
0 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
User_Admin
0.00% covered (danger)
0.00%
0 / 819
0.00% covered (danger)
0.00%
0 / 25
19182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 jetpack_new_users_styles
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 set_user_query
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 revoke_user_invite
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
72
 handle_invitation_results
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
506
 invite_user_to_wpcom
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 1
210
 send_revoke_wpcom_invite
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 handle_request_revoke_invite
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 1
110
 handle_request_resend_invite
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
56
 jetpack_user_table_row_actions
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
72
 render_invitation_email_message
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 render_invitations_notices_for_deleted_users
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
30
 render_wpcom_invite_checkbox
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 render_wpcom_external_user_checkbox
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 render_custom_email_message_form_field
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 should_send_wp_mail_new_user
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 send_wpcom_mail_user_invite
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 1
240
 rebuild_invite_cache
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
56
 get_pending_cached_wpcom_invite
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 has_pending_wpcom_invite
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 delete_external_contributor
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 jetpack_show_connection_status
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
 create_error_notice_and_redirect
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 jetpack_user_table_styles
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_scripts
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Extra UI elements added to the User Menu for the SSO feature.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection\SSO;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Connection\Client;
12use Automattic\Jetpack\Connection\Manager;
13use Automattic\Jetpack\Connection\Package_Version;
14use Automattic\Jetpack\Connection\Users_Connection_Admin as Base_Admin;
15use Automattic\Jetpack\Roles;
16use Automattic\Jetpack\Status\Host;
17use Automattic\Jetpack\Tracking;
18use WP_Error;
19use WP_User;
20use WP_User_Query;
21
22if ( ! defined( 'ABSPATH' ) ) {
23    exit( 0 );
24}
25
26/**
27 * Jetpack sso user admin class.
28 *
29 * @phan-constructor-used-for-side-effects
30 */
31class User_Admin extends Base_Admin {
32    /**
33     * Instance of WP_User_Query.
34     *
35     * @var $user_search
36     */
37    private static $user_search = null;
38    /**
39     * Array of cached invites.
40     *
41     * @var $cached_invites
42     */
43    private static $cached_invites = null;
44
45    /**
46     * Instance of Jetpack Tracking.
47     *
48     * @var $instance
49     */
50    private static $tracking = null;
51
52    /**
53     * Constructor function.
54     */
55    public function __construct() {
56        add_action( 'delete_user', array( Helpers::class, 'delete_connection_for_user' ) );
57        // If the user has no errors on creation, send an invite to WordPress.com.
58        add_filter( 'user_profile_update_errors', array( $this, 'send_wpcom_mail_user_invite' ), 10, 3 );
59        add_filter( 'wp_send_new_user_notification_to_user', array( $this, 'should_send_wp_mail_new_user' ) );
60        add_action( 'user_new_form', array( $this, 'render_invitation_email_message' ) );
61        add_action( 'user_new_form', array( $this, 'render_wpcom_invite_checkbox' ), 1 );
62        add_action( 'user_new_form', array( $this, 'render_wpcom_external_user_checkbox' ), 1 );
63        add_action( 'user_new_form', array( $this, 'render_custom_email_message_form_field' ), 1 );
64        add_action( 'delete_user_form', array( $this, 'render_invitations_notices_for_deleted_users' ) );
65        add_action( 'delete_user', array( $this, 'revoke_user_invite' ) );
66        add_filter( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 );
67        add_action( 'user_row_actions', array( $this, 'jetpack_user_table_row_actions' ), 10, 2 );
68
69        if ( isset( $_GET['jetpack-sso-invite-user'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
70            add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) );
71        }
72
73        add_action( 'admin_post_jetpack_invite_user_to_wpcom', array( $this, 'invite_user_to_wpcom' ) );
74        add_action( 'admin_post_jetpack_revoke_invite_user_to_wpcom', array( $this, 'handle_request_revoke_invite' ) );
75        add_action( 'admin_post_jetpack_resend_invite_user_to_wpcom', array( $this, 'handle_request_resend_invite' ) );
76        add_action( 'admin_print_styles-users.php', array( $this, 'jetpack_user_table_styles' ) );
77        add_filter( 'users_list_table_query_args', array( $this, 'set_user_query' ), 100, 1 );
78        add_action( 'admin_print_styles-user-new.php', array( $this, 'jetpack_new_users_styles' ) );
79        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
80
81        self::$tracking = new Tracking();
82    }
83
84    /**
85     * Enqueue assets for user-new.php.
86     */
87    public function jetpack_new_users_styles() {
88        Assets::register_script(
89            'jetpack-sso-admin-create-user',
90            '../../dist/jetpack-sso-admin-create-user.js',
91            __FILE__,
92            array(
93                'strategy'  => 'defer',
94                'in_footer' => true,
95                'enqueue'   => true,
96            )
97        );
98    }
99
100    /**
101     * Intercept the arguments for building the table, and create WP_User_Query instance
102     *
103     * @param array $args The search arguments.
104     *
105     * @return array
106     */
107    public function set_user_query( $args ) {
108        self::$user_search = new WP_User_Query( $args );
109        return $args;
110    }
111
112    /**
113     * Revokes WordPress.com invitation.
114     *
115     * @param int $user_id The user ID.
116     * @return mixed Response from the API call or false on failure.
117     */
118    public function revoke_user_invite( $user_id ) {
119        try {
120            $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
121
122            if ( $has_pending_invite ) {
123                $response = self::send_revoke_wpcom_invite( $has_pending_invite );
124                $event    = 'sso_user_invite_revoked';
125
126                if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
127                    $body                = json_decode( wp_remote_retrieve_body( $response ) );
128                    $tracking_event_data = array(
129                        'success'    => 'false',
130                        'error_code' => 'invalid-revoke-api-error',
131                    );
132
133                    if ( ! empty( $body ) && ! empty( $body->message ) ) {
134                        $tracking_event_data['error_message'] = $body->message;
135                    }
136                    self::$tracking->record_user_event(
137                        $event,
138                        $tracking_event_data
139                    );
140                    return $response;
141                }
142
143                $body = json_decode( $response['body'] );
144
145                if ( ! $body->deleted ) {
146                    self::$tracking->record_user_event(
147                        $event,
148                        array(
149                            'success'       => 'false',
150                            'error_message' => 'invalid-invite-revoke',
151                        )
152                    );
153                } else {
154                    self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
155                }
156
157                return $response;
158            } else {
159                // Delete external contributor if it exists.
160                $wpcom_user_data = ( new Manager() )->get_connected_user_data( $user_id );
161                if ( isset( $wpcom_user_data['ID'] ) ) {
162                    return self::delete_external_contributor( $wpcom_user_data['ID'] );
163                }
164            }
165        } catch ( \Exception $e ) {
166            return false;
167        }
168    }
169
170    /**
171     * Renders invitations errors/success messages in users.php.
172     */
173    public function handle_invitation_results() {
174        $valid_nonce = isset( $_GET['_wpnonce'] )
175            ? wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'jetpack-sso-invite-user' )
176            : false;
177
178        if ( ! $valid_nonce || ! isset( $_GET['jetpack-sso-invite-user'] ) ) {
179            return;
180        }
181        if ( $_GET['jetpack-sso-invite-user'] === 'success' ) {
182            return wp_admin_notice( __( 'User was invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) );
183        }
184        if ( $_GET['jetpack-sso-invite-user'] === 'reinvited-success' ) {
185            return wp_admin_notice( __( 'User was re-invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) );
186        }
187
188        if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) {
189            return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack-connection' ), array( 'type' => 'success' ) );
190        }
191
192        if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-api-error-message'] ) ) {
193            return wp_admin_notice( wp_kses( wp_unslash( $_GET['jetpack-sso-api-error-message'] ), array() ), array( 'type' => 'error' ) );
194        }
195
196        if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-invite-error'] ) ) {
197            switch ( $_GET['jetpack-sso-invite-error'] ) {
198                case 'invalid-user':
199                    return wp_admin_notice( __( 'Tried to invite a user that doesn&#8217;t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
200                case 'invalid-email':
201                    return wp_admin_notice( __( 'Tried to invite a user that doesn&#8217;t have an email address.', 'jetpack-connection' ), array( 'type' => 'error' ) );
202                case 'invalid-user-permissions':
203                    return wp_admin_notice( __( 'You don&#8217;t have permission to invite users.', 'jetpack-connection' ), array( 'type' => 'error' ) );
204                case 'invalid-user-revoke':
205                    return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn&#8217;t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
206                case 'invalid-invite-revoke':
207                    return wp_admin_notice( __( 'Tried to revoke an invite that doesn&#8217;t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
208                case 'invalid-revoke-permissions':
209                    return wp_admin_notice( __( 'You don&#8217;t have permission to revoke invites.', 'jetpack-connection' ), array( 'type' => 'error' ) );
210                case 'empty-invite':
211                    return wp_admin_notice( __( 'There is no previous invite for this user', 'jetpack-connection' ), array( 'type' => 'error' ) );
212                case 'invalid-invite':
213                    return wp_admin_notice( __( 'Attempted to send a new invitation to a user using an invite that doesn&#8217;t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) );
214                case 'error-revoke':
215                    return wp_admin_notice( __( 'An error has occurred when revoking the invite for the user.', 'jetpack-connection' ), array( 'type' => 'error' ) );
216                case 'invalid-revoke-api-error':
217                    return wp_admin_notice( __( 'An error has occurred when revoking the user invite.', 'jetpack-connection' ), array( 'type' => 'error' ) );
218                default:
219                    return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack-connection' ), array( 'type' => 'error' ) );
220            }
221        }
222    }
223
224    /**
225     * Invites a user to connect to WordPress.com to allow them to log in via SSO.
226     */
227    public function invite_user_to_wpcom() {
228        check_admin_referer( 'jetpack-sso-invite-user', 'invite_nonce' );
229        $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
230        $event = 'sso_user_invite_sent';
231
232        if ( ! current_user_can( 'create_users' ) ) {
233            $error        = 'invalid-user-permissions';
234            $query_params = array(
235                'jetpack-sso-invite-user'  => 'failed',
236                'jetpack-sso-invite-error' => $error,
237                '_wpnonce'                 => $nonce,
238            );
239            return self::create_error_notice_and_redirect( $query_params );
240        } elseif ( isset( $_GET['user_id'] ) ) {
241            $user_id    = intval( wp_unslash( $_GET['user_id'] ) );
242            $user       = get_user_by( 'id', $user_id );
243            $user_email = $user->user_email;
244
245            if ( ! $user || ! $user_email ) {
246                $reason       = ! $user ? 'invalid-user' : 'invalid-email';
247                $query_params = array(
248                    'jetpack-sso-invite-user'  => 'failed',
249                    'jetpack-sso-invite-error' => $reason,
250                    '_wpnonce'                 => $nonce,
251                );
252
253                self::$tracking->record_user_event(
254                    $event,
255                    array(
256                        'success'       => 'false',
257                        'error_message' => $reason,
258                    )
259                );
260                return self::create_error_notice_and_redirect( $query_params );
261            }
262
263            $blog_id   = Manager::get_site_id( true );
264            $roles     = new Roles();
265            $user_role = $roles->translate_user_to_role( $user );
266
267            $url      = '/sites/' . $blog_id . '/invites/new';
268            $response = Client::wpcom_json_api_request_as_user(
269                $url,
270                'v2',
271                array(
272                    'method' => 'POST',
273                ),
274                array(
275                    'invitees' => array(
276                        array(
277                            'email_or_username' => $user_email,
278                            'role'              => $user_role,
279                        ),
280                    ),
281                ),
282                'wpcom'
283            );
284
285            if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
286                $error_code   = 'invalid-invite-api-error';
287                $query_params = array(
288                    'jetpack-sso-invite-user'  => 'failed',
289                    'jetpack-sso-invite-error' => $error_code,
290                    '_wpnonce'                 => $nonce,
291                );
292
293                $tracking_event_data = array(
294                    'success'    => 'false',
295                    'error_code' => $error_code,
296                );
297
298                $body = json_decode( wp_remote_retrieve_body( $response ) );
299                if ( ! empty( $body ) && ! empty( $body->message ) ) {
300                    $query_params['jetpack-sso-api-error-message'] = $body->message;
301                    $tracking_event_data['error_message']          = $body->message;
302                }
303
304                self::$tracking->record_user_event(
305                    $event,
306                    $tracking_event_data
307                );
308                return self::create_error_notice_and_redirect( $query_params );
309            }
310
311            $body = json_decode( wp_remote_retrieve_body( $response ) );
312
313            // access the first item since we're inviting one user.
314            if ( is_array( $body ) && ! empty( $body ) ) {
315                $body = $body[0];
316            }
317
318            $query_params = array(
319                'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed',
320                '_wpnonce'                => $nonce,
321            );
322
323            if ( ! $body->success && $body->errors ) {
324                $response_error                           = array_keys( (array) $body->errors );
325                $query_params['jetpack-sso-invite-error'] = $response_error[0];
326                self::$tracking->record_user_event(
327                    $event,
328                    array(
329                        'success'       => 'false',
330                        'error_message' => $response_error[0],
331                    )
332                );
333            } else {
334                self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
335            }
336
337            return self::create_error_notice_and_redirect( $query_params );
338        } else {
339            $error        = 'invalid-user';
340            $query_params = array(
341                'jetpack-sso-invite-user'  => 'failed',
342                'jetpack-sso-invite-error' => $error,
343                '_wpnonce'                 => $nonce,
344            );
345            self::$tracking->record_user_event(
346                $event,
347                array(
348                    'success'       => 'false',
349                    'error_message' => $error,
350                )
351            );
352            return self::create_error_notice_and_redirect( $query_params );
353        }
354        wp_die();
355    }
356
357    /**
358     * Revokes a user's invitation to connect to WordPress.com.
359     *
360     * @param string $invite_id The ID of the invite to revoke.
361     */
362    public function send_revoke_wpcom_invite( $invite_id ) {
363        $blog_id = Manager::get_site_id( true );
364
365        $url = '/sites/' . $blog_id . '/invites/delete';
366        return Client::wpcom_json_api_request_as_user(
367            $url,
368            'v2',
369            array(
370                'method' => 'POST',
371            ),
372            array(
373                'invite_ids' => array( $invite_id ),
374            ),
375            'wpcom'
376        );
377    }
378
379    /**
380     * Handles logic to revoke user invite.
381     */
382    public function handle_request_revoke_invite() {
383        check_admin_referer( 'jetpack-sso-revoke-user-invite', 'revoke_invite_nonce' );
384        $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
385        $event = 'sso_user_invite_revoked';
386        if ( ! current_user_can( 'promote_users' ) ) {
387            $error        = 'invalid-revoke-permissions';
388            $query_params = array(
389                'jetpack-sso-invite-user'  => 'failed',
390                'jetpack-sso-invite-error' => $error,
391                '_wpnonce'                 => $nonce,
392            );
393
394            return self::create_error_notice_and_redirect( $query_params );
395        } elseif ( isset( $_GET['user_id'] ) ) {
396            $user_id = intval( wp_unslash( $_GET['user_id'] ) );
397            $user    = get_user_by( 'id', $user_id );
398            if ( ! $user ) {
399                $error        = 'invalid-user-revoke';
400                $query_params = array(
401                    'jetpack-sso-invite-user'  => 'failed',
402                    'jetpack-sso-invite-error' => $error,
403                    '_wpnonce'                 => $nonce,
404                );
405
406                self::$tracking->record_user_event(
407                    $event,
408                    array(
409                        'success'       => 'false',
410                        'error_message' => $error,
411                    )
412                );
413                return self::create_error_notice_and_redirect( $query_params );
414            }
415
416            if ( ! isset( $_GET['invite_id'] ) ) {
417                $error        = 'invalid-invite-revoke';
418                $query_params = array(
419                    'jetpack-sso-invite-user'  => 'failed',
420                    'jetpack-sso-invite-error' => $error,
421                    '_wpnonce'                 => $nonce,
422                );
423                self::$tracking->record_user_event(
424                    $event,
425                    array(
426                        'success'       => 'false',
427                        'error_message' => $error,
428                    )
429                );
430                return self::create_error_notice_and_redirect( $query_params );
431            }
432
433            $invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
434            $response  = self::send_revoke_wpcom_invite( $invite_id );
435
436            if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
437                $error        = 'invalid-revoke-api-error';
438                $query_params = array(
439                    'jetpack-sso-invite-user'  => 'failed',
440                    'jetpack-sso-invite-error' => $error, // general error message
441                    '_wpnonce'                 => $nonce,
442                );
443
444                $tracking_event_data = array(
445                    'success'    => 'false',
446                    'error_code' => $error,
447                );
448
449                $body = json_decode( wp_remote_retrieve_body( $response ) );
450                if ( ! empty( $body ) && ! empty( $body->message ) ) {
451                    $query_params['jetpack-sso-api-error-message'] = $body->message;
452                    $tracking_event_data['error_message']          = $body->message;
453                }
454
455                self::$tracking->record_user_event(
456                    $event,
457                    $tracking_event_data
458                );
459                return self::create_error_notice_and_redirect( $query_params );
460            }
461
462            $body         = json_decode( $response['body'] );
463            $query_params = array(
464                'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed',
465                '_wpnonce'                => $nonce,
466            );
467            if ( ! $body->deleted ) { // no invite was deleted, probably it does not exist
468                $error                                    = 'invalid-invite-revoke';
469                $query_params['jetpack-sso-invite-error'] = $error;
470                self::$tracking->record_user_event(
471                    $event,
472                    array(
473                        'success'       => 'false',
474                        'error_message' => $error,
475                    )
476                );
477            } else {
478                self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
479            }
480            return self::create_error_notice_and_redirect( $query_params );
481        } else {
482            $error        = 'invalid-user-revoke';
483            $query_params = array(
484                'jetpack-sso-invite-user'  => 'failed',
485                'jetpack-sso-invite-error' => $error,
486                '_wpnonce'                 => $nonce,
487            );
488            self::$tracking->record_user_event(
489                $event,
490                array(
491                    'success'       => 'false',
492                    'error_message' => $error,
493                )
494            );
495            return self::create_error_notice_and_redirect( $query_params );
496        }
497
498        wp_die();
499    }
500
501    /**
502     * Handles resend user invite.
503     */
504    public function handle_request_resend_invite() {
505        check_admin_referer( 'jetpack-sso-resend-user-invite', 'resend_invite_nonce' );
506        $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
507        $event = 'sso_user_invite_resend';
508        if ( ! current_user_can( 'create_users' ) ) {
509            $query_params = array(
510                'jetpack-sso-invite-user'  => 'failed',
511                'jetpack-sso-invite-error' => 'invalid-user-permissions',
512                '_wpnonce'                 => $nonce,
513            );
514            return self::create_error_notice_and_redirect( $query_params );
515        } elseif ( isset( $_GET['invite_id'] ) ) {
516            $invite_slug = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
517            $blog_id     = Manager::get_site_id( true );
518            $url         = '/sites/' . $blog_id . '/invites/resend';
519            $response    = Client::wpcom_json_api_request_as_user(
520                $url,
521                'v2',
522                array(
523                    'method' => 'POST',
524                ),
525                array(
526                    'invite_slug' => $invite_slug,
527                ),
528                'wpcom'
529            );
530
531            $status_code = wp_remote_retrieve_response_code( $response );
532
533            if ( 200 !== $status_code ) {
534                $message_type = $status_code === 404 ? 'invalid-invite' : ''; // empty is the general error message
535                $query_params = array(
536                    'jetpack-sso-invite-user'  => 'failed',
537                    'jetpack-sso-invite-error' => $message_type,
538                    '_wpnonce'                 => $nonce,
539                );
540                self::$tracking->record_user_event(
541                    $event,
542                    array(
543                        'success'       => 'false',
544                        'error_message' => $message_type,
545                    )
546                );
547                return self::create_error_notice_and_redirect( $query_params );
548            }
549
550            $body                    = json_decode( $response['body'] );
551            $invite_response_message = $body->success ? 'reinvited-success' : 'failed';
552            $query_params            = array(
553                'jetpack-sso-invite-user' => $invite_response_message,
554                '_wpnonce'                => $nonce,
555            );
556
557            if ( ! $body->success ) {
558                self::$tracking->record_user_event(
559                    $event,
560                    array(
561                        'success'       => 'false',
562                        'error_message' => $invite_response_message,
563                    )
564                );
565            } else {
566                self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
567            }
568
569            return self::create_error_notice_and_redirect( $query_params );
570        } else {
571            $error        = 'empty-invite';
572            $query_params = array(
573                'jetpack-sso-invite-user'  => 'failed',
574                'jetpack-sso-invite-error' => 'empty-invite',
575                '_wpnonce'                 => $nonce,
576            );
577            self::$tracking->record_user_event(
578                $event,
579                array(
580                    'success'       => 'false',
581                    'error_message' => $error,
582                )
583            );
584            return self::create_error_notice_and_redirect( $query_params );
585        }
586    }
587
588    /**
589     * Adds 'Revoke invite' and 'Resend invite' link to user table row actions.
590     * Removes 'Reset password' link.
591     *
592     * @param array   $actions - User row actions.
593     * @param WP_User $user_object - User object.
594     */
595    public function jetpack_user_table_row_actions( $actions, $user_object ) {
596        $user_id            = $user_object->ID;
597        $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
598
599        if ( current_user_can( 'promote_users' ) && $has_pending_invite ) {
600            $nonce                        = wp_create_nonce( 'jetpack-sso-revoke-user-invite' );
601            $actions['sso_revoke_invite'] = sprintf(
602                '<a class="jetpack-sso-revoke-invite-action" href="%s">%s</a>',
603                add_query_arg(
604                    array(
605                        'action'              => 'jetpack_revoke_invite_user_to_wpcom',
606                        'user_id'             => $user_id,
607                        'revoke_invite_nonce' => $nonce,
608                        'invite_id'           => $has_pending_invite,
609                    ),
610                    admin_url( 'admin-post.php' )
611                ),
612                esc_html__( 'Revoke invite', 'jetpack-connection' )
613            );
614        }
615        if ( current_user_can( 'promote_users' ) && $has_pending_invite ) {
616            $nonce                        = wp_create_nonce( 'jetpack-sso-resend-user-invite' );
617            $actions['sso_resend_invite'] = sprintf(
618                '<a class="jetpack-sso-resend-invite-action" href="%s">%s</a>',
619                add_query_arg(
620                    array(
621                        'action'              => 'jetpack_resend_invite_user_to_wpcom',
622                        'user_id'             => $user_id,
623                        'resend_invite_nonce' => $nonce,
624                        'invite_id'           => $has_pending_invite,
625                    ),
626                    admin_url( 'admin-post.php' )
627                ),
628                esc_html__( 'Resend invite', 'jetpack-connection' )
629            );
630        }
631
632        if (
633            current_user_can( 'promote_users' )
634            && (
635                $has_pending_invite
636                || ( new Manager() )->is_user_connected( $user_id )
637            )
638        ) {
639            unset( $actions['resetpassword'] );
640        }
641
642        return $actions;
643    }
644
645    /**
646     * Render the invitation email message.
647     */
648    public function render_invitation_email_message() {
649        $message = wp_kses(
650            __(
651                '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.',
652                'jetpack-connection'
653            ),
654            array(
655                'a' => array(
656                    'class'  => array(),
657                    'href'   => array(),
658                    'rel'    => array(),
659                    'target' => array(),
660                ),
661            )
662        );
663        wp_admin_notice(
664            $message,
665            array(
666                'id'                 => 'invitation_message',
667                'type'               => 'info',
668                'dismissible'        => false,
669                'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ),
670            )
671        );
672    }
673
674    /**
675     * Render a note that wp.com invites will be automatically revoked.
676     */
677    public function render_invitations_notices_for_deleted_users() {
678        check_admin_referer( 'bulk-users' );
679
680        // When one user is deleted, the param is `user`, when multiple users are deleted, the param is `users`.
681        // We start with `users` and fallback to `user`.
682        $user_id  = isset( $_GET['user'] ) ? intval( wp_unslash( $_GET['user'] ) ) : null;
683        $user_ids = isset( $_GET['users'] ) ? array_map( 'intval', wp_unslash( $_GET['users'] ) ) : array( $user_id );
684
685        $users_with_invites = array_filter(
686            $user_ids,
687            function ( $user_id ) {
688                return $user_id !== null && self::has_pending_wpcom_invite( $user_id );
689            }
690        );
691
692        $users_with_invites = array_map(
693            function ( $user_id ) {
694                $user = get_user_by( 'id', $user_id );
695                return $user->user_login;
696            },
697            $users_with_invites
698        );
699
700        $invites_count = count( $users_with_invites );
701        if ( $invites_count > 0 ) {
702            $users_with_invites = implode( ', ', $users_with_invites );
703            $message            = wp_kses(
704                sprintf(
705                /* translators: %s is a comma-separated list of user logins. */
706                    _n(
707                        'WordPress.com invitation will be automatically revoked for user: <strong>%s</strong>.',
708                        'WordPress.com invitations will be automatically revoked for users: <strong>%s</strong>.',
709                        $invites_count,
710                        'jetpack-connection'
711                    ),
712                    $users_with_invites
713                ),
714                array( 'strong' => true )
715            );
716            wp_admin_notice(
717                $message,
718                array(
719                    'id'                 => 'invitation_message',
720                    'type'               => 'info',
721                    'dismissible'        => false,
722                    'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ),
723                )
724            );
725        }
726    }
727
728    /**
729     * Render WordPress.com invite checkbox for new user registration.
730     *
731     * @param string $type The type of new user form the hook follows.
732     */
733    public function render_wpcom_invite_checkbox( $type ) {
734        /*
735         * Only check this box by default on WordPress.com sites
736         * that do not use the WooCommerce plugin.
737         */
738        $is_checked = ( new Host() )->is_wpcom_platform() && ! class_exists( 'WooCommerce' );
739
740        if ( $type === 'add-new-user' ) {
741            ?>
742            <table class="form-table">
743                <tr class="form-field">
744                    <th scope="row">
745                        <label for="invite_user_wpcom"><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></label>
746                    </th>
747                    <td>
748                        <fieldset>
749                            <legend class="screen-reader-text">
750                                <span><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></span>
751                            </legend>
752                            <label for="invite_user_wpcom">
753                                <input
754                                    name="invite_user_wpcom"
755                                    type="checkbox"
756                                    id="invite_user_wpcom"
757                                    <?php checked( $is_checked ); ?>
758                                    >
759                                <?php esc_html_e( 'Invite user to WordPress.com', 'jetpack-connection' ); ?>
760                            </label>
761                        </fieldset>
762                    </td>
763                </tr>
764            </table>
765            <?php
766        }
767    }
768
769    /**
770     * Render a checkbox to differentiate if a user is external.
771     *
772     * @param string $type The type of new user form the hook follows.
773     */
774    public function render_wpcom_external_user_checkbox( $type ) {
775        // Only enable this feature on WordPress.com sites.
776        if ( ! ( new Host() )->is_wpcom_platform() ) {
777            return;
778        }
779
780        if ( $type === 'add-new-user' ) {
781            ?>
782            <table class="form-table">
783                <tr class="form-field">
784                    <th scope="row">
785                        <label for="user_external_contractor"><?php esc_html_e( 'External User', 'jetpack-connection' ); ?></label>
786                    </th>
787                    <td>
788                        <fieldset>
789                            <legend class="screen-reader-text">
790                                <span><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></span>
791                            </legend>
792                            <label for="user_external_contractor">
793                                <input
794                                    name="user_external_contractor"
795                                    type="checkbox"
796                                    id="user_external_contractor"
797                                    >
798                                <?php esc_html_e( 'Mark as external collaborator', 'jetpack-connection' ); ?>
799                            </label>
800                        </fieldset>
801                    </td>
802                </tr>
803            </table>
804            <?php
805        }
806    }
807
808    /**
809     * Render the custom email message form field for new user registration.
810     *
811     * @param string $type The type of new user form the hook follows.
812     */
813    public function render_custom_email_message_form_field( $type ) {
814        if ( $type === 'add-new-user' ) {
815            $valid_nonce          = isset( $_POST['_wpnonce_create-user'] )
816                    ? wp_verify_nonce( sanitize_key( $_POST['_wpnonce_create-user'] ), 'create-user' )
817                    : false;
818            $custom_email_message = ( $valid_nonce && isset( $_POST['custom_email_message'] ) ) ? sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) : '';
819            ?>
820            <table class="form-table" id="custom_email_message_block">
821                <tr class="form-field">
822                    <th scope="row">
823                        <label for="custom_email_message"><?php esc_html_e( 'Custom Message', 'jetpack-connection' ); ?></label>
824                    </th>
825                    <td>
826                        <label for="custom_email_message">
827                            <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>
828                            <p id="custom_email_message_description">
829                                <?php
830                                esc_html_e( 'This user will be invited to WordPress.com. You can include a personalized welcome message with the invitation.', 'jetpack-connection' );
831                                ?>
832                        </label>
833                    </td>
834                </tr>
835            </table>
836            <?php
837        }
838    }
839
840    /**
841     * Conditionally disable the core invitation email.
842     * It should be sent when SSO is disabled or when admins opt-out of WordPress.com invites intentionally.
843     * If the "Send User Notification" checkbox is checked, the core invitation email should be sent.
844     *
845     * @param boolean $send_wp_email Whether the core invitation email should be sent.
846     *
847     * @return boolean Indicating if the core invitation main should be sent.
848     */
849    public function should_send_wp_mail_new_user( $send_wp_email ) {
850        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.
851            return $send_wp_email;
852        }
853        return false;
854    }
855
856    /**
857     * Send user invitation to WordPress.com if user has no errors.
858     *
859     * @param WP_Error  $errors The WP_Error object.
860     * @param bool      $update Whether the user is being updated or not.
861     * @param \stdClass $user   The User object about to be created.
862     * @return WP_Error The modified or not WP_Error object.
863     */
864    public function send_wpcom_mail_user_invite( $errors, $update, $user ) {
865        // Only admins should be able to invite new users.
866        if ( ! current_user_can( 'create_users' ) ) {
867            return $errors;
868        }
869
870        if ( $update ) {
871            return $errors;
872        }
873
874        // check for a valid nonce.
875        if (
876            ! isset( $_POST['_wpnonce_create-user'] )
877            || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce_create-user'] ), 'create-user' )
878        ) {
879            return $errors;
880        }
881
882        // Check if the user is being invited to WordPress.com.
883        if ( ! isset( $_POST['invite_user_wpcom'] ) ) {
884            return $errors;
885        }
886
887        // check if the custom email message is too long.
888        if (
889            ! empty( $_POST['custom_email_message'] )
890            && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 500
891        ) {
892            $errors->add(
893                'custom_email_message',
894                wp_kses(
895                    __( '<strong>Error</strong>: The custom message is too long. Please keep it under 500 characters.', 'jetpack-connection' ),
896                    array(
897                        'strong' => array(),
898                    )
899                )
900            );
901        }
902
903        $site_id = Manager::get_site_id( true );
904        if ( ! $site_id ) {
905            $errors->add(
906                'invalid_site_id',
907                wp_kses(
908                    __( '<strong>Error</strong>: Invalid site ID.', 'jetpack-connection' ),
909                    array(
910                        'strong' => array(),
911                    )
912                )
913            );
914        }
915
916        // Bail if there are any errors.
917        if ( $errors->has_errors() ) {
918            return $errors;
919        }
920
921        $new_user_request = array(
922            'email_or_username' => sanitize_email( $user->user_email ),
923            'role'              => sanitize_key( $user->role ),
924        );
925
926        if (
927            isset( $_POST['custom_email_message'] )
928            && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 0
929        ) {
930            $new_user_request['message'] = sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) );
931        }
932
933        if ( isset( $_POST['user_external_contractor'] ) ) {
934                $new_user_request['is_external'] = true;
935        }
936
937        $response = Client::wpcom_json_api_request_as_user(
938            sprintf(
939                '/sites/%d/invites/new',
940                (int) $site_id
941            ),
942            '2', // Api version
943            array(
944                'method' => 'POST',
945            ),
946            array(
947                'invitees' => array( $new_user_request ),
948            )
949        );
950
951        $event_name          = 'sso_new_user_invite_sent';
952        $custom_message_sent = isset( $new_user_request['message'] ) ? 'true' : 'false';
953
954        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
955            $errors->add(
956                'invitation_not_sent',
957                wp_kses(
958                    __( '<strong>Error</strong>: The user invitation email could not be sent, the user account was not created.', 'jetpack-connection' ),
959                    array(
960                        'strong' => array(),
961                    )
962                )
963            );
964            self::$tracking->record_user_event(
965                $event_name,
966                array(
967                    'success' => 'false',
968                    'error'   => wp_remote_retrieve_body( $response ), // Get as much information as possible.
969                )
970            );
971        } else {
972            self::$tracking->record_user_event(
973                $event_name,
974                array(
975                    'success'             => 'true',
976                    'custom_message_sent' => $custom_message_sent,
977                )
978            );
979        }
980
981        return $errors;
982    }
983
984    /**
985     * Executed when our WP_User_Query instance is set, and we don't have cached invites.
986     * This function uses the user emails and the 'are-users-invited' endpoint to build the cache.
987     *
988     * @return void
989     */
990    private static function rebuild_invite_cache() {
991        $blog_id = Manager::get_site_id( true );
992
993        if ( self::$cached_invites === null && self::$user_search !== null ) {
994
995            self::$cached_invites = array();
996
997            $results = self::$user_search->get_results();
998
999            $user_emails = array_reduce(
1000                $results,
1001                function ( $current, $item ) {
1002                    if ( ! ( new Manager() )->is_user_connected( $item->ID ) ) {
1003                        $current[] = rawurlencode( $item->user_email );
1004                    } else {
1005                        self::$cached_invites[] = array(
1006                            'email_or_username' => $item->user_email,
1007                            'invited'           => false,
1008                            'invite_code'       => '',
1009                        );
1010                    }
1011                    return $current;
1012                },
1013                array()
1014            );
1015
1016            if ( ! empty( $user_emails ) ) {
1017                $url = '/sites/' . $blog_id . '/invites/are-users-invited';
1018
1019                $response = Client::wpcom_json_api_request_as_user(
1020                    $url,
1021                    'v2',
1022                    array(
1023                        'method' => 'POST',
1024                    ),
1025                    array( 'users' => $user_emails ),
1026                    'wpcom'
1027                );
1028
1029                if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
1030                    $body = json_decode( $response['body'], true );
1031
1032                    // ensure array_merge happens with the right parameters
1033                    if ( empty( $body ) ) {
1034                        $body = array();
1035                    }
1036
1037                    self::$cached_invites = array_merge( self::$cached_invites, $body );
1038                }
1039            }
1040        }
1041    }
1042
1043    /**
1044     * Check if there is cached invite for a user email.
1045     *
1046     * @access private
1047     * @static
1048     *
1049     * @param string $email The user email.
1050     *
1051     * @return array|void Returns the cached invite if found.
1052     */
1053    public static function get_pending_cached_wpcom_invite( $email ) {
1054        if ( self::$cached_invites === null ) {
1055            self::rebuild_invite_cache();
1056        }
1057
1058        if ( ! empty( self::$cached_invites ) && is_array( self::$cached_invites ) ) {
1059            $index = array_search( $email, array_column( self::$cached_invites, 'email_or_username' ), true );
1060            if ( $index !== false ) {
1061                return self::$cached_invites[ $index ];
1062            }
1063        }
1064    }
1065
1066    /**
1067     * Check if a given user is invited to the site.
1068     *
1069     * @access private
1070     * @static
1071     * @param int $user_id The user ID.
1072     *
1073     * @return false|string returns the user invite code if the user is invited, false otherwise.
1074     */
1075    private static function has_pending_wpcom_invite( $user_id ) {
1076        $user = get_user_by( 'id', $user_id );
1077        if ( ! $user instanceof \WP_User ) {
1078            return false;
1079        }
1080
1081        $blog_id       = Manager::get_site_id( true );
1082        $cached_invite = self::get_pending_cached_wpcom_invite( $user->user_email );
1083
1084        if ( $cached_invite ) {
1085            return $cached_invite['invite_code'];
1086        }
1087
1088        $url      = '/sites/' . $blog_id . '/invites/is-invited';
1089        $url      = add_query_arg(
1090            array(
1091                'email_or_username' => rawurlencode( $user->user_email ),
1092            ),
1093            $url
1094        );
1095        $response = Client::wpcom_json_api_request_as_user(
1096            $url,
1097            'v2',
1098            array(),
1099            null,
1100            'wpcom'
1101        );
1102
1103        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1104            return false;
1105        }
1106
1107        $body_response = wp_remote_retrieve_body( $response );
1108        if ( empty( $body_response ) ) {
1109            return false;
1110        }
1111
1112        $body = json_decode( $body_response, true );
1113        if ( ! empty( $body['invite_code'] ) ) {
1114            return $body['invite_code'];
1115        }
1116
1117        return false;
1118    }
1119
1120    /**
1121     * Delete an external contributor from the site.
1122     *
1123     * @access private
1124     * @static
1125     * @param int $user_id The user ID.
1126     *
1127     * @return bool Returns true if the user was successfully deleted, false otherwise.
1128     */
1129    private static function delete_external_contributor( $user_id ) {
1130        $blog_id  = Manager::get_site_id( true );
1131        $url      = '/sites/' . $blog_id . '/external-contributors/remove';
1132        $response = Client::wpcom_json_api_request_as_user(
1133            $url,
1134            'v2',
1135            array(
1136                'method' => 'POST',
1137            ),
1138            array(
1139                'user_id' => $user_id,
1140            ),
1141            'wpcom'
1142        );
1143
1144        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1145            return false;
1146        }
1147
1148        return true;
1149    }
1150
1151    /**
1152     * Show Jetpack SSO user connection status.
1153     *
1154     * @param string $val HTML for the column.
1155     * @param string $col User list table column.
1156     * @param int    $user_id User ID.
1157     * @return string Modified column content.
1158     */
1159    public function jetpack_show_connection_status( $val, $col, $user_id ) {
1160        if ( 'user_jetpack' !== $col ) {
1161            return $val;
1162        }
1163
1164        // Get base connection status from parent
1165        $connection_status = parent::render_connection_column( '', $col, $user_id );
1166
1167        // If user is not connected, check for pending invite
1168        if ( ! $connection_status ) {
1169            $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
1170            if ( $has_pending_invite ) {
1171                return sprintf(
1172                    '<span title="%1$s" class="jetpack-sso-invitation sso-pending-invite">%2$s</span>',
1173                    esc_attr__( 'This user didn&#8217;t accept the invitation to join this site yet.', 'jetpack-connection' ),
1174                    esc_html__( 'Pending invite', 'jetpack-connection' )
1175                );
1176            }
1177
1178            // Show invite button for non-connected users
1179            $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
1180            return sprintf(
1181                '<span tabindex="0" role="tooltip" aria-label="%4$s: %3$s" class="jetpack-sso-invitation-tooltip-icon sso-disconnected-user">
1182                    <a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a>
1183                    <span class="sso-disconnected-user-icon dashicons dashicons-warning">
1184                        <span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip">%3$s</span>
1185                    </span>
1186                </span>',
1187                add_query_arg(
1188                    array(
1189                        'user_id'      => $user_id,
1190                        'invite_nonce' => $nonce,
1191                        'action'       => 'jetpack_invite_user_to_wpcom',
1192                    ),
1193                    admin_url( 'admin-post.php' )
1194                ),
1195                esc_html__( 'Send invite', 'jetpack-connection' ),
1196                esc_attr__( 'This user doesn&#8217;t have a Jetpack SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack-connection' ),
1197                esc_attr__( 'Tooltip', 'jetpack-connection' )
1198            );
1199        }
1200
1201        return $connection_status;
1202    }
1203
1204    /**
1205     * Creates error notices and redirects the user to the previous page.
1206     *
1207     * @param array $query_params - query parameters added to redirection URL.
1208     * @phan-suppress PhanPluginNeverReturnMethod
1209     */
1210    public function create_error_notice_and_redirect( $query_params ) {
1211        $ref = wp_get_referer();
1212        if ( empty( $ref ) ) {
1213            $ref = network_admin_url( 'users.php' );
1214        }
1215
1216        $url = add_query_arg(
1217            $query_params,
1218            $ref
1219        );
1220        wp_safe_redirect( $url );
1221        exit;
1222    }
1223
1224    /**
1225     * Style the Jetpack user rows and columns.
1226     */
1227    public function jetpack_user_table_styles() {
1228        ?>
1229    <style>
1230        #the-list tr:has(.sso-disconnected-user) {
1231            background: #F5F1E1;
1232        }
1233        #the-list tr:has(.sso-pending-invite) {
1234            background: #E9F0F5;
1235        }
1236        .jetpack-sso-invitation {
1237            background: none;
1238            border: none;
1239            color: #50575e;
1240            padding: 0;
1241            text-align: unset;
1242        }
1243        .jetpack-sso-invitation.sso-disconnected-user {
1244            color: #0073aa;
1245            cursor: pointer;
1246            text-decoration: underline;
1247        }
1248        .jetpack-sso-invitation.sso-disconnected-user:hover,
1249        .jetpack-sso-invitation.sso-disconnected-user:focus,
1250        .jetpack-sso-invitation.sso-disconnected-user:active {
1251            color: #0096dd;
1252        }
1253
1254        .sso-disconnected-user-icon {
1255            margin-left: 4px;
1256            cursor: pointer;
1257            background: gray;
1258            border-radius: 10px;
1259        }
1260
1261        .sso-disconnected-user-icon.dashicons {
1262            font-size: 1rem;
1263            height: 1rem;
1264            width: 1rem;
1265            background-color: #9D6E00;
1266            color: #F5F1E1;
1267        }
1268        .jetpack-sso-invitation-tooltip-icon{
1269            position: relative;
1270            cursor: pointer;
1271        }
1272        .jetpack-sso-td-tooltip {
1273            left: -256px;
1274        }
1275        .jetpack-sso-invitation-tooltip {
1276            position: absolute;
1277            background: #f6f7f7;
1278            top: -85px;
1279            width: 250px;
1280            padding: 7px;
1281            color: #3c434a;
1282            font-size: .75rem;
1283            line-height: 17px;
1284            text-align: left;
1285            margin: 0;
1286            display: none;
1287            border-radius: 4px;
1288            font-family: sans-serif;
1289            box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.1);
1290        }
1291
1292    </style>
1293        <?php
1294    }
1295
1296    /**
1297     * Enqueue SSO-specific scripts.
1298     *
1299     * @param string $hook The current admin page.
1300     */
1301    public function enqueue_scripts( $hook ) {
1302        if ( 'users.php' !== $hook ) {
1303            return;
1304        }
1305
1306        parent::enqueue_scripts( $hook );
1307        // Enqueue the SSO users script.
1308        Assets::register_script(
1309            'jetpack-sso-users',
1310            '../../dist/jetpack-sso-users.js',
1311            __FILE__,
1312            array(
1313                'strategy'  => 'defer',
1314                'in_footer' => true,
1315                'enqueue'   => true,
1316                'version'   => Package_Version::PACKAGE_VERSION,
1317            )
1318        );
1319    }
1320}