Plugin Directory

source: jetpack/trunk/modules/sso/class.jetpack-sso-user-admin.php @ 3056649

Last change on this file since 3056649 was 3056649, checked in by zinigor, 4 months ago

Updating trunk to version 13.2.2

File size: 34.2 KB
Line 
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2use Automattic\Jetpack\Connection\Client;
3use Automattic\Jetpack\Roles;
4use Automattic\Jetpack\Tracking;
5
6if ( ! class_exists( 'Jetpack_SSO_User_Admin' ) ) :
7        require_once JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php';
8        /**
9         * Jetpack sso user admin class.
10         */
11        class Jetpack_SSO_User_Admin {
12
13                /**
14                 * Instance of WP_User_Query.
15                 *
16                 * @var $user_search
17                 */
18                private static $user_search = null;
19                /**
20                 * Array of cached invites.
21                 *
22                 * @var $cached_invites
23                 */
24                private static $cached_invites = null;
25
26                /**
27                 * Instance of JetPack Tracking.
28                 *
29                 * @var $instance
30                 */
31                private static $tracking = null;
32
33                /**
34                 * Constructor function.
35                 */
36                public function __construct() {
37                        add_action( 'delete_user', array( 'Jetpack_SSO_Helpers', 'delete_connection_for_user' ) );
38                        // If the user has no errors on creation, send an invite to WordPress.com.
39                        add_filter( 'user_profile_update_errors', array( $this, 'send_wpcom_mail_user_invite' ), 10, 3 );
40                        add_filter( 'wp_send_new_user_notification_to_user', array( $this, 'should_send_wp_mail_new_user' ) );
41                        add_action( 'user_new_form', array( $this, 'render_invitation_email_message' ) );
42                        add_action( 'user_new_form', array( $this, 'render_wpcom_invite_checkbox' ), 1 );
43                        add_action( 'user_new_form', array( $this, 'render_custom_email_message_form_field' ), 1 );
44                        add_action( 'delete_user_form', array( $this, 'render_invitations_notices_for_deleted_users' ) );
45                        add_action( 'delete_user', array( $this, 'revoke_user_invite' ) );
46                        add_filter( 'manage_users_columns', array( $this, 'jetpack_user_connected_th' ) );
47                        add_action( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 );
48                        add_action( 'user_row_actions', array( $this, 'jetpack_user_table_row_actions' ), 10, 2 );
49                        add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) );
50                        add_action( 'admin_post_jetpack_invite_user_to_wpcom', array( $this, 'invite_user_to_wpcom' ) );
51                        add_action( 'admin_post_jetpack_revoke_invite_user_to_wpcom', array( $this, 'handle_request_revoke_invite' ) );
52                        add_action( 'admin_post_jetpack_resend_invite_user_to_wpcom', array( $this, 'handle_request_resend_invite' ) );
53                        add_action( 'admin_print_styles-users.php', array( $this, 'jetpack_user_table_styles' ) );
54                        add_action( 'admin_print_styles-user-new.php', array( $this, 'jetpack_user_new_form_styles' ) );
55                        add_filter( 'users_list_table_query_args', array( $this, 'set_user_query' ), 100, 1 );
56
57                        self::$tracking = new Tracking();
58                }
59
60                /**
61                 * Intercept the arguments for building the table, and create WP_User_Query instance
62                 *
63                 * @param array $args The search arguments.
64                 *
65                 * @return array
66                 */
67                public function set_user_query( $args ) {
68                        self::$user_search = new WP_User_Query( $args );
69                        return $args;
70                }
71
72                /**
73                 * Revokes WordPress.com invitation.
74                 *
75                 * @param int $user_id The user ID.
76                 */
77                public function revoke_user_invite( $user_id ) {
78                        try {
79                                $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
80
81                                if ( $has_pending_invite ) {
82                                        $response = self::send_revoke_wpcom_invite( $has_pending_invite );
83                                        $event    = 'sso_user_invite_revoked';
84
85                                        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
86                                                self::$tracking->record_user_event(
87                                                        $event,
88                                                        array(
89                                                                'success'       => 'false',
90                                                                'error_message' => 'invalid-revoke-api-error',
91                                                        )
92                                                );
93                                                return $response;
94                                        }
95
96                                        $body = json_decode( $response['body'] );
97
98                                        if ( ! $body->deleted ) {
99                                                self::$tracking->record_user_event(
100                                                        $event,
101                                                        array(
102                                                                'success'       => 'false',
103                                                                'error_message' => 'invalid-invite-revoke',
104                                                        )
105                                                );
106                                        } else {
107                                                self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
108                                        }
109
110                                        return $response;
111                                }
112                        } catch ( Exception $e ) {
113                                return false;
114                        }
115                }
116
117                /**
118                 * Renders invitations errors/success messages in users.php.
119                 */
120                public function handle_invitation_results() {
121                        $valid_nonce = isset( $_GET['_wpnonce'] ) ? wp_verify_nonce( $_GET['_wpnonce'], 'jetpack-sso-invite-user' ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
122
123                        if ( ! $valid_nonce || ! isset( $_GET['jetpack-sso-invite-user'] ) || ! function_exists( 'wp_admin_notice' ) ) {
124                                return;
125                        }
126                        if ( $_GET['jetpack-sso-invite-user'] === 'success' ) {
127                                return wp_admin_notice( __( 'User was invited successfully!', 'jetpack' ), array( 'type' => 'success' ) );
128                        }
129                        if ( $_GET['jetpack-sso-invite-user'] === 'reinvited-success' ) {
130                                return wp_admin_notice( __( 'User was re-invited successfully!', 'jetpack' ), array( 'type' => 'success' ) );
131                        }
132
133                        if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) {
134                                return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack' ), array( 'type' => 'success' ) );
135                        }
136
137                        if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-invite-error'] ) ) {
138                                switch ( $_GET['jetpack-sso-invite-error'] ) {
139                                        case 'invalid-user':
140                                                return wp_admin_notice( __( 'Tried to invite a user that doesn&#8217;t exist.', 'jetpack' ), array( 'type' => 'error' ) );
141                                        case 'invalid-email':
142                                                return wp_admin_notice( __( 'Tried to invite a user that doesn&#8217;t have an email address.', 'jetpack' ), array( 'type' => 'error' ) );
143                                        case 'invalid-user-permissions':
144                                                return wp_admin_notice( __( 'You don&#8217;t have permission to invite users.', 'jetpack' ), array( 'type' => 'error' ) );
145                                        case 'invalid-user-revoke':
146                                                return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn&#8217;t exist.', 'jetpack' ), array( 'type' => 'error' ) );
147                                        case 'invalid-invite-revoke':
148                                                return wp_admin_notice( __( 'Tried to revoke an invite that doesn&#8217;t exist.', 'jetpack' ), array( 'type' => 'error' ) );
149                                        case 'invalid-revoke-permissions':
150                                                return wp_admin_notice( __( 'You don&#8217;t have permission to revoke invites.', 'jetpack' ), array( 'type' => 'error' ) );
151                                        case 'empty-invite':
152                                                return wp_admin_notice( __( 'There is no previous invite for this user', 'jetpack' ), array( 'type' => 'error' ) );
153                                        case 'invalid-invite':
154                                                return wp_admin_notice( __( 'Attempted to send a new invitation to a user using an invite that doesn&#8217;t exist.', 'jetpack' ), array( 'type' => 'error' ) );
155                                        case 'error-revoke':
156                                                return wp_admin_notice( __( 'An error has occurred when revoking the invite for the user.', 'jetpack' ), array( 'type' => 'error' ) );
157                                        case 'invalid-revoke-api-error':
158                                                return wp_admin_notice( __( 'An error has occurred when revoking the user invite.', 'jetpack' ), array( 'type' => 'error' ) );
159                                        default:
160                                                return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack' ), array( 'type' => 'error' ) );
161                                }
162                        }
163                }
164
165                /**
166                 * Invites a user to connect to WordPress.com to allow them to log in via SSO.
167                 */
168                public function invite_user_to_wpcom() {
169                        check_admin_referer( 'jetpack-sso-invite-user', 'invite_nonce' );
170                        $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
171                        $event = 'sso_user_invite_sent';
172
173                        if ( ! current_user_can( 'create_users' ) ) {
174                                $error        = 'invalid-user-permissions';
175                                $query_params = array(
176                                        'jetpack-sso-invite-user'  => 'failed',
177                                        'jetpack-sso-invite-error' => $error,
178                                        '_wpnonce'                 => $nonce,
179                                );
180                                return self::create_error_notice_and_redirect( $query_params );
181                        } elseif ( isset( $_GET['user_id'] ) ) {
182                                $user_id    = intval( wp_unslash( $_GET['user_id'] ) );
183                                $user       = get_user_by( 'id', $user_id );
184                                $user_email = $user->user_email;
185
186                                if ( ! $user || ! $user_email ) {
187                                        $reason       = ! $user ? 'invalid-user' : 'invalid-email';
188                                        $query_params = array(
189                                                'jetpack-sso-invite-user'  => 'failed',
190                                                'jetpack-sso-invite-error' => $reason,
191                                                '_wpnonce'                 => $nonce,
192                                        );
193
194                                        self::$tracking->record_user_event(
195                                                $event,
196                                                array(
197                                                        'success'       => 'false',
198                                                        'error_message' => $reason,
199                                                )
200                                        );
201                                        return self::create_error_notice_and_redirect( $query_params );
202                                }
203
204                                $blog_id   = Jetpack_Options::get_option( 'id' );
205                                $roles     = new Roles();
206                                $user_role = $roles->translate_user_to_role( $user );
207
208                                $url      = '/sites/' . $blog_id . '/invites/new';
209                                $response = Client::wpcom_json_api_request_as_user(
210                                        $url,
211                                        'v2',
212                                        array(
213                                                'method' => 'POST',
214                                        ),
215                                        array(
216                                                'invitees' => array(
217                                                        array(
218                                                                'email_or_username' => $user_email,
219                                                                'role'              => $user_role,
220                                                        ),
221                                                ),
222                                        ),
223                                        'wpcom'
224                                );
225
226                                if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
227                                        $error        = 'invalid-invite-api-error';
228                                        $query_params = array(
229                                                'jetpack-sso-invite-user'  => 'failed',
230                                                'jetpack-sso-invite-error' => $error,
231                                                '_wpnonce'                 => $nonce,
232                                        );
233
234                                        self::$tracking->record_user_event(
235                                                $event,
236                                                array(
237                                                        'success'       => 'false',
238                                                        'error_message' => $error,
239                                                )
240                                        );
241                                        return self::create_error_notice_and_redirect( $query_params );
242                                }
243
244                                // access the first item since we're inviting one user.
245                                $body = json_decode( $response['body'] )[0];
246
247                                $query_params = array(
248                                        'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed',
249                                        '_wpnonce'                => $nonce,
250                                );
251
252                                if ( ! $body->success && $body->errors ) {
253                                        $response_error                           = array_keys( (array) $body->errors );
254                                        $query_params['jetpack-sso-invite-error'] = $response_error[0];
255                                        self::$tracking->record_user_event(
256                                                $event,
257                                                array(
258                                                        'success'       => 'false',
259                                                        'error_message' => $response_error[0],
260                                                )
261                                        );
262                                } else {
263                                        self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
264                                }
265
266                                return self::create_error_notice_and_redirect( $query_params );
267                        } else {
268                                $error        = 'invalid-user';
269                                $query_params = array(
270                                        'jetpack-sso-invite-user'  => 'failed',
271                                        'jetpack-sso-invite-error' => $error,
272                                        '_wpnonce'                 => $nonce,
273                                );
274                                self::$tracking->record_user_event(
275                                        $event,
276                                        array(
277                                                'success'       => 'false',
278                                                'error_message' => $error,
279                                        )
280                                );
281                                return self::create_error_notice_and_redirect( $query_params );
282                        }
283                        wp_die();
284                }
285
286                /**
287                 * Revokes a user's invitation to connect to WordPress.com.
288                 *
289                 * @param string $invite_id The ID of the invite to revoke.
290                 */
291                public function send_revoke_wpcom_invite( $invite_id ) {
292                        $blog_id = Jetpack_Options::get_option( 'id' );
293
294                        $url = '/sites/' . $blog_id . '/invites/delete';
295                        return Client::wpcom_json_api_request_as_user(
296                                $url,
297                                'v2',
298                                array(
299                                        'method' => 'POST',
300                                ),
301                                array(
302                                        'invite_ids' => array( $invite_id ),
303                                ),
304                                'wpcom'
305                        );
306                }
307
308                /**
309                 * Handles logic to revoke user invite.
310                 */
311                public function handle_request_revoke_invite() {
312                        check_admin_referer( 'jetpack-sso-revoke-user-invite', 'revoke_invite_nonce' );
313                        $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
314                        $event = 'sso_user_invite_revoked';
315                        if ( ! current_user_can( 'promote_users' ) ) {
316                                $error        = 'invalid-revoke-permissions';
317                                $query_params = array(
318                                        'jetpack-sso-invite-user'  => 'failed',
319                                        'jetpack-sso-invite-error' => $error,
320                                        '_wpnonce'                 => $nonce,
321                                );
322
323                                return self::create_error_notice_and_redirect( $query_params );
324                        } elseif ( isset( $_GET['user_id'] ) ) {
325                                $user_id = intval( wp_unslash( $_GET['user_id'] ) );
326                                $user    = get_user_by( 'id', $user_id );
327                                if ( ! $user ) {
328                                        $error        = 'invalid-user-revoke';
329                                        $query_params = array(
330                                                'jetpack-sso-invite-user'  => 'failed',
331                                                'jetpack-sso-invite-error' => $error,
332                                                '_wpnonce'                 => $nonce,
333                                        );
334
335                                        self::$tracking->record_user_event(
336                                                $event,
337                                                array(
338                                                        'success'       => 'false',
339                                                        'error_message' => $error,
340                                                )
341                                        );
342                                        return self::create_error_notice_and_redirect( $query_params );
343                                }
344
345                                if ( ! isset( $_GET['invite_id'] ) ) {
346                                        $error        = 'invalid-invite-revoke';
347                                        $query_params = array(
348                                                'jetpack-sso-invite-user'  => 'failed',
349                                                'jetpack-sso-invite-error' => $error,
350                                                '_wpnonce'                 => $nonce,
351                                        );
352                                        self::$tracking->record_user_event(
353                                                $event,
354                                                array(
355                                                        'success'       => 'false',
356                                                        'error_message' => $error,
357                                                )
358                                        );
359                                        return self::create_error_notice_and_redirect( $query_params );
360                                }
361
362                                $invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
363                                $response  = self::send_revoke_wpcom_invite( $invite_id );
364
365                                if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
366                                        $error        = 'invalid-revoke-api-error';
367                                        $query_params = array(
368                                                'jetpack-sso-invite-user'  => 'failed',
369                                                'jetpack-sso-invite-error' => $error, // general error message
370                                                '_wpnonce'                 => $nonce,
371                                        );
372                                        self::$tracking->record_user_event(
373                                                $event,
374                                                array(
375                                                        'success'       => 'false',
376                                                        'error_message' => $error,
377                                                )
378                                        );
379                                        return self::create_error_notice_and_redirect( $query_params );
380                                }
381
382                                $body         = json_decode( $response['body'] );
383                                $query_params = array(
384                                        'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed',
385                                        '_wpnonce'                => $nonce,
386                                );
387                                if ( ! $body->deleted ) { // no invite was deleted, probably it does not exist
388                                        $error                                    = 'invalid-invite-revoke';
389                                        $query_params['jetpack-sso-invite-error'] = $error;
390                                        self::$tracking->record_user_event(
391                                                $event,
392                                                array(
393                                                        'success'       => 'false',
394                                                        'error_message' => $error,
395                                                )
396                                        );
397                                } else {
398                                        self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
399                                }
400                                return self::create_error_notice_and_redirect( $query_params );
401                        } else {
402                                $error        = 'invalid-user-revoke';
403                                $query_params = array(
404                                        'jetpack-sso-invite-user'  => 'failed',
405                                        'jetpack-sso-invite-error' => $error,
406                                        '_wpnonce'                 => $nonce,
407                                );
408                                self::$tracking->record_user_event(
409                                        $event,
410                                        array(
411                                                'success'       => 'false',
412                                                'error_message' => $error,
413                                        )
414                                );
415                                return self::create_error_notice_and_redirect( $query_params );
416                        }
417
418                        wp_die();
419                }
420
421                /**
422                 * Handles resend user invite.
423                 */
424                public function handle_request_resend_invite() {
425                        check_admin_referer( 'jetpack-sso-resend-user-invite', 'resend_invite_nonce' );
426                        $nonce = wp_create_nonce( 'jetpack-sso-invite-user' );
427                        $event = 'sso_user_invite_resend';
428                        if ( ! current_user_can( 'create_users' ) ) {
429                                $query_params = array(
430                                        'jetpack-sso-invite-user'  => 'failed',
431                                        'jetpack-sso-invite-error' => 'invalid-user-permissions',
432                                        '_wpnonce'                 => $nonce,
433                                );
434                                return self::create_error_notice_and_redirect( $query_params );
435                        } elseif ( isset( $_GET['invite_id'] ) ) {
436                                $invite_slug = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) );
437                                $blog_id     = Jetpack_Options::get_option( 'id' );
438                                $url         = '/sites/' . $blog_id . '/invites/resend';
439                                $response    = Client::wpcom_json_api_request_as_user(
440                                        $url,
441                                        'v2',
442                                        array(
443                                                'method' => 'POST',
444                                        ),
445                                        array(
446                                                'invite_slug' => $invite_slug,
447                                        ),
448                                        'wpcom'
449                                );
450
451                                $status_code = wp_remote_retrieve_response_code( $response );
452
453                                if ( 200 !== $status_code ) {
454                                        $message_type = $status_code === 404 ? 'invalid-invite' : ''; // empty is the general error message
455                                        $query_params = array(
456                                                'jetpack-sso-invite-user'  => 'failed',
457                                                'jetpack-sso-invite-error' => $message_type,
458                                                '_wpnonce'                 => $nonce,
459                                        );
460                                        self::$tracking->record_user_event(
461                                                $event,
462                                                array(
463                                                        'success'       => 'false',
464                                                        'error_message' => $message_type,
465                                                )
466                                        );
467                                        return self::create_error_notice_and_redirect( $query_params );
468                                }
469
470                                $body                    = json_decode( $response['body'] );
471                                $invite_response_message = $body->success ? 'reinvited-success' : 'failed';
472                                $query_params            = array(
473                                        'jetpack-sso-invite-user' => $invite_response_message,
474                                        '_wpnonce'                => $nonce,
475                                );
476
477                                if ( ! $body->success ) {
478                                        self::$tracking->record_user_event(
479                                                $event,
480                                                array(
481                                                        'success'       => 'false',
482                                                        'error_message' => $invite_response_message,
483                                                )
484                                        );
485                                } else {
486                                        self::$tracking->record_user_event( $event, array( 'success' => 'true' ) );
487                                }
488
489                                return self::create_error_notice_and_redirect( $query_params );
490                        } else {
491                                $error        = 'empty-invite';
492                                $query_params = array(
493                                        'jetpack-sso-invite-user'  => 'failed',
494                                        'jetpack-sso-invite-error' => 'empty-invite',
495                                        '_wpnonce'                 => $nonce,
496                                );
497                                self::$tracking->record_user_event(
498                                        $event,
499                                        array(
500                                                'success'       => 'false',
501                                                'error_message' => $error,
502                                        )
503                                );
504                                return self::create_error_notice_and_redirect( $query_params );
505                        }
506                }
507
508                /**
509                 * Adds 'Revoke invite' and 'Resend invite' link to user table row actions.
510                 * Removes 'Reset password' link.
511                 *
512                 * @param array   $actions - User row actions.
513                 * @param WP_User $user_object - User object.
514                 */
515                public function jetpack_user_table_row_actions( $actions, $user_object ) {
516                        $user_id            = $user_object->ID;
517                        $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
518
519                        if ( current_user_can( 'promote_users' ) && $has_pending_invite ) {
520                                $nonce                        = wp_create_nonce( 'jetpack-sso-revoke-user-invite' );
521                                $actions['sso_revoke_invite'] = sprintf(
522                                        '<a class="jetpack-sso-revoke-invite-action" href="%s">%s</a>',
523                                        add_query_arg(
524                                                array(
525                                                        'action'              => 'jetpack_revoke_invite_user_to_wpcom',
526                                                        'user_id'             => $user_id,
527                                                        'revoke_invite_nonce' => $nonce,
528                                                        'invite_id'           => $has_pending_invite,
529                                                ),
530                                                admin_url( 'admin-post.php' )
531                                        ),
532                                        esc_html__( 'Revoke invite', 'jetpack' )
533                                );
534                        }
535                        if ( current_user_can( 'promote_users' ) && $has_pending_invite ) {
536                                $nonce                        = wp_create_nonce( 'jetpack-sso-resend-user-invite' );
537                                $actions['sso_resend_invite'] = sprintf(
538                                        '<a class="jetpack-sso-resend-invite-action" href="%s">%s</a>',
539                                        add_query_arg(
540                                                array(
541                                                        'action'              => 'jetpack_resend_invite_user_to_wpcom',
542                                                        'user_id'             => $user_id,
543                                                        'resend_invite_nonce' => $nonce,
544                                                        'invite_id'           => $has_pending_invite,
545                                                ),
546                                                admin_url( 'admin-post.php' )
547                                        ),
548                                        esc_html__( 'Resend invite', 'jetpack' )
549                                );
550                        }
551
552                        unset( $actions['resetpassword'] );
553
554                        return $actions;
555                }
556
557                /**
558                 * Render the invitation email message.
559                 */
560                public function render_invitation_email_message() {
561                        if ( ! function_exists( 'wp_admin_notice' ) ) {
562                                return;
563                        }
564                        $message = wp_kses(
565                                __(
566                                        'New users will receive an invite to join WordPress.com, so they can 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>.',
567                                        'jetpack'
568                                ),
569                                array(
570                                        'a' => array(
571                                                'class'  => array(),
572                                                'href'   => array(),
573                                                'rel'    => array(),
574                                                'target' => array(),
575                                        ),
576                                )
577                        );
578                        wp_admin_notice(
579                                $message,
580                                array(
581                                        'id'                 => 'invitation_message',
582                                        'type'               => 'info',
583                                        'dismissible'        => false,
584                                        'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ),
585                                )
586                        );
587                }
588
589                /**
590                 * Render a note that wp.com invites will be automatically revoked.
591                 */
592                public function render_invitations_notices_for_deleted_users() {
593                        if ( ! function_exists( 'wp_admin_notice' ) ) {
594                                return;
595                        }
596                        check_admin_referer( 'bulk-users' );
597
598                        // When one user is deleted, the param is `user`, when multiple users are deleted, the param is `users`.
599                        // We start with `users` and fallback to `user`.
600                        $user_id  = isset( $_GET['user'] ) ? intval( wp_unslash( $_GET['user'] ) ) : null;
601                        $user_ids = isset( $_GET['users'] ) ? array_map( 'intval', wp_unslash( $_GET['users'] ) ) : array( $user_id );
602
603                        $users_with_invites = array_filter(
604                                $user_ids,
605                                function ( $user_id ) {
606                                        return $user_id !== null && self::has_pending_wpcom_invite( $user_id );
607                                }
608                        );
609
610                        $users_with_invites = array_map(
611                                function ( $user_id ) {
612                                        $user = get_user_by( 'id', $user_id );
613                                        return $user->user_login;
614                                },
615                                $users_with_invites
616                        );
617
618                        $invites_count = count( $users_with_invites );
619                        if ( $invites_count > 0 ) {
620                                $users_with_invites = implode( ', ', $users_with_invites );
621                                $message            = wp_kses(
622                                        sprintf(
623                                        /* translators: %s is a comma-separated list of user logins. */
624                                                _n(
625                                                        'WordPress.com invitation will be automatically revoked for user: <strong>%s</strong>.',
626                                                        'WordPress.com invitations will be automatically revoked for users: <strong>%s</strong>.',
627                                                        $invites_count,
628                                                        'jetpack'
629                                                ),
630                                                $users_with_invites
631                                        ),
632                                        array( 'strong' => true )
633                                );
634                                wp_admin_notice(
635                                        $message,
636                                        array(
637                                                'id'                 => 'invitation_message',
638                                                'type'               => 'info',
639                                                'dismissible'        => false,
640                                                'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ),
641                                        )
642                                );
643                        }
644                }
645
646                /**
647                 * Render WordPress.com invite checkbox for new user registration.
648                 *
649                 * @param string $type The type of new user form the hook follows.
650                 */
651                public function render_wpcom_invite_checkbox( $type ) {
652                        if ( $type === 'add-new-user' ) {
653                                ?>
654                                <table class="form-table">
655                                        <tr class="form-field">
656                                                <th scope="row">
657                                                        <label for="invite_user_wpcom"><?php esc_html_e( 'Invite user:', 'jetpack' ); ?></label>
658                                                </th>
659                                                <td>
660                                                        <fieldset>
661                                                                <legend class="screen-reader-text">
662                                                                        <span><?php esc_html_e( 'Invite user', 'jetpack' ); ?></span>
663                                                                </legend>
664                                                                <label for="invite_user_wpcom">
665                                                                        <input name="invite_user_wpcom" type="checkbox" id="invite_user_wpcom" checked>
666                                                                        <?php esc_html_e( 'Invite user to WordPress.com', 'jetpack' ); ?>
667                                                                </label>
668                                                        </fieldset>
669                                                </td>
670                                        </tr>
671                                </table>
672                                <?php
673                        }
674                }
675
676                /**
677                 * Render the custom email message form field for new user registration.
678                 *
679                 * @param string $type The type of new user form the hook follows.
680                 */
681                public function render_custom_email_message_form_field( $type ) {
682                        if ( $type === 'add-new-user' ) {
683                                $valid_nonce          = isset( $_POST['_wpnonce_create-user'] ) ? wp_verify_nonce( $_POST['_wpnonce_create-user'], 'create-user' ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
684                                $custom_email_message = ( $valid_nonce && isset( $_POST['custom_email_message'] ) ) ? sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) : '';
685                                ?>
686                        <table class="form-table">
687                                <tr class="form-field">
688                                        <th scope="row">
689                                                <label for="custom_email_message"><?php esc_html_e( 'Custom Message', 'jetpack' ); ?></label>
690                                        </th>
691                                        <td>
692                                                <label for="custom_email_message">
693                                                        <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>
694                                                        <p id="custom_email_message_description">
695                                                                <?php
696                                                                esc_html_e( 'This user will be invited to WordPress.com. You can include a personalized welcome message with the invitation.', 'jetpack' );
697                                                                ?>
698                                                </label>
699                                        </td>
700                                </tr>
701                        </table>
702                                <?php
703                        }
704                }
705
706                /**
707                 * Conditionally disable the core invitation email.
708                 * It should be sent when SSO is disabled or when admins opt-out of WordPress.com invites intentionally.
709                 *
710                 * @return boolean Indicating if the core invitation main should be sent.
711                 */
712                public function should_send_wp_mail_new_user() {
713                        return empty( $_POST['invite_user_wpcom'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
714                }
715
716                /**
717                 * Send user invitation to WordPress.com if user has no errors.
718                 *
719                 * @param WP_Error $errors The WP_Error object.
720                 * @param bool     $update Whether the user is being updated or not.
721                 * @param stdClass $user   The User object about to be created.
722                 * @return WP_Error The modified or not WP_Error object.
723                 */
724                public function send_wpcom_mail_user_invite( $errors, $update, $user ) {
725                        if ( ! $update ) {
726                                $valid_nonce = isset( $_POST['_wpnonce_create-user'] ) ? wp_verify_nonce( $_POST['_wpnonce_create-user'], 'create-user' ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
727
728                                if ( $this->should_send_wp_mail_new_user() ) {
729                                        return $errors;
730                                }
731
732                                if ( $valid_nonce && ! empty( $_POST['custom_email_message'] ) && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 500 ) {
733                                        $errors->add( 'custom_email_message', __( '<strong>Error</strong>: The custom message is too long. Please keep it under 500 characters.', 'jetpack' ) );
734                                }
735
736                                if ( $errors->has_errors() ) {
737                                        return $errors;
738                                }
739
740                                $email   = $user->user_email;
741                                $role    = $user->role;
742                                $blog_id = Jetpack_Options::get_option( 'id' );
743                                $url     = '/sites/' . $blog_id . '/invites/new';
744
745                                $new_user_request = array(
746                                        'email_or_username' => $email,
747                                        'role'              => $role,
748                                );
749
750                                if ( $valid_nonce && isset( $_POST['custom_email_message'] ) && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) > 0 ) ) {
751                                        $new_user_request['message'] = sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) );
752                                }
753
754                                $response = Client::wpcom_json_api_request_as_user(
755                                        $url,
756                                        '2', // Api version
757                                        array(
758                                                'method' => 'POST',
759                                        ),
760                                        array(
761                                                'invitees' => array( $new_user_request ),
762                                        )
763                                );
764
765                                $event               = 'sso_new_user_invite_sent';
766                                $custom_message_sent = isset( $new_user_request['message'] ) ? 'true' : 'false';
767
768                                if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
769                                        $errors->add( 'invitation_not_sent', __( '<strong>Error</strong>: The user invitation email could not be sent, the user account was not created.', 'jetpack' ) );
770                                        self::$tracking->record_user_event(
771                                                $event,
772                                                array(
773                                                        'success' => 'false',
774                                                )
775                                        );
776                                } else {
777                                        self::$tracking->record_user_event(
778                                                $event,
779                                                array(
780                                                        'success'             => 'true',
781                                                        'custom_message_sent' => $custom_message_sent,
782                                                )
783                                        );
784                                }
785                        }
786
787                        return $errors;
788                }
789
790                /**
791                 * Adds a column in the user admin table to display user connection status and actions.
792                 *
793                 * @param array $columns User list table columns.
794                 *
795                 * @return array
796                 */
797                public function jetpack_user_connected_th( $columns ) {
798                        $columns['user_jetpack'] = sprintf(
799                                '<span title="%1$s">%2$s [?]</span>',
800                                esc_attr__( 'Jetpack SSO allows a seamless and secure experience on WordPress.com. Join millions of WordPress users who trust us to keep their accounts safe.', 'jetpack' ),
801                                esc_html__( 'SSO Status', 'jetpack' )
802                        );
803                        return $columns;
804                }
805
806                /**
807                 * Executed when our WP_User_Query instance is set, and we don't have cached invites.
808                 * This function uses the user emails and the 'are-users-invited' endpoint to build the cache.
809                 *
810                 * @return void
811                 */
812                private static function rebuild_invite_cache() {
813                        $blog_id = Jetpack_Options::get_option( 'id' );
814
815                        if ( self::$cached_invites === null && self::$user_search !== null ) {
816
817                                self::$cached_invites = array();
818
819                                $results = self::$user_search->get_results();
820
821                                $user_emails = array_reduce(
822                                        $results,
823                                        function ( $current, $item ) {
824                                                if ( ! Jetpack::connection()->is_user_connected( $item->ID ) ) {
825                                                        $current[] = rawurlencode( $item->user_email );
826                                                } else {
827                                                        self::$cached_invites[] = array(
828                                                                'email_or_username' => $item->user_email,
829                                                                'invited'           => false,
830                                                                'invite_code'       => '',
831                                                        );
832                                                }
833                                                return $current;
834                                        },
835                                        array()
836                                );
837
838                                if ( ! empty( $user_emails ) ) {
839                                        $url = '/sites/' . $blog_id . '/invites/are-users-invited';
840
841                                        $response = Client::wpcom_json_api_request_as_user(
842                                                $url,
843                                                'v2',
844                                                array(
845                                                        'method' => 'POST',
846                                                ),
847                                                array( 'users' => $user_emails ),
848                                                'wpcom'
849                                        );
850
851                                        if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
852                                                $body = json_decode( $response['body'], true );
853
854                                                // ensure array_merge happens with the right parameters
855                                                if ( empty( $body ) ) {
856                                                        $body = array();
857                                                }
858
859                                                self::$cached_invites = array_merge( self::$cached_invites, $body );
860                                        }
861                                }
862                        }
863                }
864
865                /**
866                 * Check if there is cached invite for a user email.
867                 *
868                 * @access private
869                 * @static
870                 *
871                 * @param string $email The user email.
872                 *
873                 * @return array|void Returns the cached invite if found.
874                 */
875                public static function get_pending_cached_wpcom_invite( $email ) {
876                        if ( self::$cached_invites === null ) {
877                                self::rebuild_invite_cache();
878                        }
879
880                        if ( ! empty( self::$cached_invites ) ) {
881                                $index = array_search( $email, array_column( self::$cached_invites, 'email_or_username' ), true );
882                                if ( $index !== false ) {
883                                        return self::$cached_invites[ $index ];
884                                }
885                        }
886                }
887
888                /**
889                 * Check if a given user is invited to the site.
890                 *
891                 * @access private
892                 * @static
893                 * @param int $user_id The user ID.
894                 *
895                 * @return {false|string} returns the user invite code if the user is invited, false otherwise.
896                 */
897                private static function has_pending_wpcom_invite( $user_id ) {
898                        $blog_id       = Jetpack_Options::get_option( 'id' );
899                        $user          = get_user_by( 'id', $user_id );
900                        $cached_invite = self::get_pending_cached_wpcom_invite( $user->user_email );
901
902                        if ( $cached_invite ) {
903                                return $cached_invite['invite_code'];
904                        }
905
906                        $url      = '/sites/' . $blog_id . '/invites/is-invited';
907                        $url      = add_query_arg(
908                                array(
909                                        'email_or_username' => rawurlencode( $user->user_email ),
910                                ),
911                                $url
912                        );
913                        $response = Client::wpcom_json_api_request_as_user(
914                                $url,
915                                'v2',
916                                array(),
917                                null,
918                                'wpcom'
919                        );
920
921                        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
922                                return false;
923                        }
924
925                        return json_decode( $response['body'], true )['invite_code'];
926                }
927
928                /**
929                 * Show Jetpack SSO user connection status.
930                 *
931                 * @param string $val HTML for the column.
932                 * @param string $col User list table column.
933                 * @param int    $user_id User ID.
934                 *
935                 * @return string
936                 */
937                public function jetpack_show_connection_status( $val, $col, $user_id ) {
938                        if ( 'user_jetpack' === $col ) {
939                                if ( Jetpack::connection()->is_user_connected( $user_id ) ) {
940                                        $connection_html = sprintf(
941                                                '<span title="%1$s" class="jetpack-sso-invitation">%2$s</span>',
942                                                esc_attr__( 'This user is connected and can log-in to this site.', 'jetpack' ),
943                                                esc_html__( 'Connected', 'jetpack' )
944                                        );
945                                        return $connection_html;
946                                } else {
947                                        $has_pending_invite = self::has_pending_wpcom_invite( $user_id );
948                                        if ( $has_pending_invite ) {
949                                                $connection_html = sprintf(
950                                                        '<span title="%1$s" class="jetpack-sso-invitation sso-pending-invite">%2$s</span>',
951                                                        esc_attr__( 'This user didn&#8217;t accept the invitation to join this site yet.', 'jetpack' ),
952                                                        esc_html__( 'Pending invite', 'jetpack' )
953                                                );
954                                                return $connection_html;
955                                        }
956                                        $nonce           = wp_create_nonce( 'jetpack-sso-invite-user' );
957                                        $connection_html = sprintf(
958                                        // Using formmethod and formaction because we can't nest forms and have to submit using the main form.
959                                                '<a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a><span title="%3$s" class="sso-disconnected-user-icon dashicons dashicons-warning"></span>',
960                                                add_query_arg(
961                                                        array(
962                                                                'user_id'      => $user_id,
963                                                                'invite_nonce' => $nonce,
964                                                                'action'       => 'jetpack_invite_user_to_wpcom',
965                                                        ),
966                                                        admin_url( 'admin-post.php' )
967                                                ),
968                                                esc_html__( 'Send invite', 'jetpack' ),
969                                                esc_attr__( 'This user doesn&#8217;t have an SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack' )
970                                        );
971                                        return $connection_html;
972                                }
973                        }
974                }
975
976                /**
977                 * Creates error notices and redirects the user to the previous page.
978                 *
979                 * @param array $query_params - query parameters added to redirection URL.
980                 */
981                public function create_error_notice_and_redirect( $query_params ) {
982                        $ref = wp_get_referer();
983                        if ( empty( $ref ) ) {
984                                $ref = network_admin_url( 'users.php' );
985                        }
986
987                        $url = add_query_arg(
988                                $query_params,
989                                $ref
990                        );
991                        return wp_safe_redirect( $url );
992                }
993
994                /**
995                 * Style the Jetpack user rows and columns.
996                 */
997                public function jetpack_user_table_styles() {
998                        ?>
999                <style>
1000                        #the-list tr:has(.sso-disconnected-user) {
1001                                background: #F5F1E1;
1002                        }
1003                        #the-list tr:has(.sso-pending-invite) {
1004                                background: #E9F0F5;
1005                        }
1006                        .fixed .column-user_jetpack {
1007                                width: 100px;
1008                        }
1009                        .jetpack-sso-invitation {
1010                                background: none;
1011                                border: none;
1012                                color: #50575e;
1013                                padding: 0;
1014                                text-align: unset;
1015                        }
1016                        .jetpack-sso-invitation.sso-disconnected-user {
1017                                color: #0073aa;
1018                                cursor: pointer;
1019                                text-decoration: underline;
1020                        }
1021                        .jetpack-sso-invitation.sso-disconnected-user:hover,
1022                        .jetpack-sso-invitation.sso-disconnected-user:focus,
1023                        .jetpack-sso-invitation.sso-disconnected-user:active {
1024                                color: #0096dd;
1025                        }
1026
1027                        .sso-disconnected-user-icon {
1028                                margin-left: 4px;
1029                                cursor: pointer;
1030                                background: gray;
1031                                border-radius: 10px;
1032                        }
1033
1034                        .sso-disconnected-user-icon.dashicons {
1035                                font-size: 1rem;
1036                                height: 1rem;
1037                                width: 1rem;
1038                                background-color: #9D6E00;
1039                                color: #F5F1E1;
1040                        }
1041
1042                </style>
1043                        <?php
1044                }
1045
1046                /**
1047                 * Enqueue style for the Jetpack user new form.
1048                 */
1049                public function jetpack_user_new_form_styles() {
1050                        // Enqueue the CSS for the admin create user page.
1051                        wp_enqueue_style( 'jetpack-sso-admin-create-user', plugins_url( 'modules/sso/jetpack-sso-admin-create-user.css', JETPACK__PLUGIN_FILE ), array(), time() );
1052                }
1053        }
1054endif;
Note: See TracBrowser for help on using the repository browser.