PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-connection/src/sso/class-user-admin.php +1336 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,1336 @@
1 +<?php
2 +/**
3 + * Extra UI elements added to the User Menu for the SSO feature.
4 + *
5 + * @package automattic/jetpack-connection
6 + */
7 +
8 +namespace Automattic\Jetpack\Connection\SSO;
9 +
10 +use Automattic\Jetpack\Assets;
11 +use Automattic\Jetpack\Connection\Client;
12 +use Automattic\Jetpack\Connection\Manager;
13 +use Automattic\Jetpack\Connection\Package_Version;
14 +use Automattic\Jetpack\Connection\Users_Connection_Admin as Base_Admin;
15 +use Automattic\Jetpack\Roles;
16 +use Automattic\Jetpack\Status\Host;
17 +use Automattic\Jetpack\Tracking;
18 +use WP_Error;
19 +use WP_User;
20 +use WP_User_Query;
21 +
22 +if ( ! defined( 'ABSPATH' ) ) {
23 + exit( 0 );
24 +}
25 +
26 +/**
27 + * Jetpack sso user admin class.
28 + *
29 + * @phan-constructor-used-for-side-effects
30 + */
31 +class 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 +}
1304 +CSS;
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 +}