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