'defer', 'in_footer' => true, 'enqueue' => true, ) ); } /** * Intercept the arguments for building the table, and create WP_User_Query instance * * @param array $args The search arguments. * * @return array */ public function set_user_query( $args ) { self::$user_search = new WP_User_Query( $args ); return $args; } /** * Revokes WordPress.com invitation. * * @param int $user_id The user ID. */ public function revoke_user_invite( $user_id ) { try { $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); if ( $has_pending_invite ) { $response = self::send_revoke_wpcom_invite( $has_pending_invite ); $event = 'sso_user_invite_revoked'; if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => 'invalid-revoke-api-error', ) ); return $response; } $body = json_decode( $response['body'] ); if ( ! $body->deleted ) { self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => 'invalid-invite-revoke', ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return $response; } else { // Delete external contributor if it exists. $wpcom_user_data = ( new Manager() )->get_connected_user_data( $user_id ); if ( isset( $wpcom_user_data['ID'] ) ) { return self::delete_external_contributor( $wpcom_user_data['ID'] ); } } } catch ( Exception $e ) { return false; } } /** * Renders invitations errors/success messages in users.php. * * @phan-suppress PhanUndeclaredFunction,UnusedSuppression -- Existence of wp_admin_notice (added in WP 6.4) is checked inline. * @todo Remove suppression and function_exists check when we drop support for WP 6.3. */ public function handle_invitation_results() { $valid_nonce = isset( $_GET['_wpnonce'] ) ? wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'jetpack-sso-invite-user' ) : false; if ( ! $valid_nonce || ! isset( $_GET['jetpack-sso-invite-user'] ) || ! function_exists( 'wp_admin_notice' ) ) { return; } if ( $_GET['jetpack-sso-invite-user'] === 'success' ) { return wp_admin_notice( __( 'User was invited successfully!', 'jetpack' ), array( 'type' => 'success' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'reinvited-success' ) { return wp_admin_notice( __( 'User was re-invited successfully!', 'jetpack' ), array( 'type' => 'success' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) { return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack' ), array( 'type' => 'success' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-invite-error'] ) ) { switch ( $_GET['jetpack-sso-invite-error'] ) { case 'invalid-user': return wp_admin_notice( __( 'Tried to invite a user that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-email': return wp_admin_notice( __( 'Tried to invite a user that doesn’t have an email address.', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-user-permissions': return wp_admin_notice( __( 'You don’t have permission to invite users.', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-user-revoke': return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-invite-revoke': return wp_admin_notice( __( 'Tried to revoke an invite that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-revoke-permissions': return wp_admin_notice( __( 'You don’t have permission to revoke invites.', 'jetpack' ), array( 'type' => 'error' ) ); case 'empty-invite': return wp_admin_notice( __( 'There is no previous invite for this user', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-invite': return wp_admin_notice( __( 'Attempted to send a new invitation to a user using an invite that doesn’t exist.', 'jetpack' ), array( 'type' => 'error' ) ); case 'error-revoke': return wp_admin_notice( __( 'An error has occurred when revoking the invite for the user.', 'jetpack' ), array( 'type' => 'error' ) ); case 'invalid-revoke-api-error': return wp_admin_notice( __( 'An error has occurred when revoking the user invite.', 'jetpack' ), array( 'type' => 'error' ) ); default: return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack' ), array( 'type' => 'error' ) ); } } } /** * Invites a user to connect to WordPress.com to allow them to log in via SSO. */ public function invite_user_to_wpcom() { check_admin_referer( 'jetpack-sso-invite-user', 'invite_nonce' ); $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); $event = 'sso_user_invite_sent'; if ( ! current_user_can( 'create_users' ) ) { $error = 'invalid-user-permissions'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); return self::create_error_notice_and_redirect( $query_params ); } elseif ( isset( $_GET['user_id'] ) ) { $user_id = intval( wp_unslash( $_GET['user_id'] ) ); $user = get_user_by( 'id', $user_id ); $user_email = $user->user_email; if ( ! $user || ! $user_email ) { $reason = ! $user ? 'invalid-user' : 'invalid-email'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $reason, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $reason, ) ); return self::create_error_notice_and_redirect( $query_params ); } $blog_id = Manager::get_site_id( true ); $roles = new Roles(); $user_role = $roles->translate_user_to_role( $user ); $url = '/sites/' . $blog_id . '/invites/new'; $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'invitees' => array( array( 'email_or_username' => $user_email, 'role' => $user_role, ), ), ), 'wpcom' ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $error = 'invalid-invite-api-error'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } // access the first item since we're inviting one user. $body = json_decode( $response['body'] )[0]; $query_params = array( 'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed', '_wpnonce' => $nonce, ); if ( ! $body->success && $body->errors ) { $response_error = array_keys( (array) $body->errors ); $query_params['jetpack-sso-invite-error'] = $response_error[0]; self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $response_error[0], ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return self::create_error_notice_and_redirect( $query_params ); } else { $error = 'invalid-user'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } wp_die(); } /** * Revokes a user's invitation to connect to WordPress.com. * * @param string $invite_id The ID of the invite to revoke. */ public function send_revoke_wpcom_invite( $invite_id ) { $blog_id = Manager::get_site_id( true ); $url = '/sites/' . $blog_id . '/invites/delete'; return Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'invite_ids' => array( $invite_id ), ), 'wpcom' ); } /** * Handles logic to revoke user invite. */ public function handle_request_revoke_invite() { check_admin_referer( 'jetpack-sso-revoke-user-invite', 'revoke_invite_nonce' ); $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); $event = 'sso_user_invite_revoked'; if ( ! current_user_can( 'promote_users' ) ) { $error = 'invalid-revoke-permissions'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); return self::create_error_notice_and_redirect( $query_params ); } elseif ( isset( $_GET['user_id'] ) ) { $user_id = intval( wp_unslash( $_GET['user_id'] ) ); $user = get_user_by( 'id', $user_id ); if ( ! $user ) { $error = 'invalid-user-revoke'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } if ( ! isset( $_GET['invite_id'] ) ) { $error = 'invalid-invite-revoke'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } $invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) ); $response = self::send_revoke_wpcom_invite( $invite_id ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $error = 'invalid-revoke-api-error'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, // general error message '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } $body = json_decode( $response['body'] ); $query_params = array( 'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed', '_wpnonce' => $nonce, ); if ( ! $body->deleted ) { // no invite was deleted, probably it does not exist $error = 'invalid-invite-revoke'; $query_params['jetpack-sso-invite-error'] = $error; self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return self::create_error_notice_and_redirect( $query_params ); } else { $error = 'invalid-user-revoke'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } wp_die(); } /** * Handles resend user invite. */ public function handle_request_resend_invite() { check_admin_referer( 'jetpack-sso-resend-user-invite', 'resend_invite_nonce' ); $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); $event = 'sso_user_invite_resend'; if ( ! current_user_can( 'create_users' ) ) { $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => 'invalid-user-permissions', '_wpnonce' => $nonce, ); return self::create_error_notice_and_redirect( $query_params ); } elseif ( isset( $_GET['invite_id'] ) ) { $invite_slug = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) ); $blog_id = Manager::get_site_id( true ); $url = '/sites/' . $blog_id . '/invites/resend'; $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'invite_slug' => $invite_slug, ), 'wpcom' ); $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { $message_type = $status_code === 404 ? 'invalid-invite' : ''; // empty is the general error message $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $message_type, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $message_type, ) ); return self::create_error_notice_and_redirect( $query_params ); } $body = json_decode( $response['body'] ); $invite_response_message = $body->success ? 'reinvited-success' : 'failed'; $query_params = array( 'jetpack-sso-invite-user' => $invite_response_message, '_wpnonce' => $nonce, ); if ( ! $body->success ) { self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $invite_response_message, ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return self::create_error_notice_and_redirect( $query_params ); } else { $error = 'empty-invite'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => 'empty-invite', '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } } /** * Adds 'Revoke invite' and 'Resend invite' link to user table row actions. * Removes 'Reset password' link. * * @param array $actions - User row actions. * @param WP_User $user_object - User object. */ public function jetpack_user_table_row_actions( $actions, $user_object ) { $user_id = $user_object->ID; $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); if ( current_user_can( 'promote_users' ) && $has_pending_invite ) { $nonce = wp_create_nonce( 'jetpack-sso-revoke-user-invite' ); $actions['sso_revoke_invite'] = sprintf( '%s', add_query_arg( array( 'action' => 'jetpack_revoke_invite_user_to_wpcom', 'user_id' => $user_id, 'revoke_invite_nonce' => $nonce, 'invite_id' => $has_pending_invite, ), admin_url( 'admin-post.php' ) ), esc_html__( 'Revoke invite', 'jetpack' ) ); } if ( current_user_can( 'promote_users' ) && $has_pending_invite ) { $nonce = wp_create_nonce( 'jetpack-sso-resend-user-invite' ); $actions['sso_resend_invite'] = sprintf( '%s', add_query_arg( array( 'action' => 'jetpack_resend_invite_user_to_wpcom', 'user_id' => $user_id, 'resend_invite_nonce' => $nonce, 'invite_id' => $has_pending_invite, ), admin_url( 'admin-post.php' ) ), esc_html__( 'Resend invite', 'jetpack' ) ); } if ( current_user_can( 'promote_users' ) && ( $has_pending_invite || Jetpack_SSO_Helpers::is_user_connected( $user_id ) ) ) { unset( $actions['resetpassword'] ); } return $actions; } /** * Render the invitation email message. */ public function render_invitation_email_message() { // @todo Remove function_exists check (and phan suppression below) when we drop support for WP 6.3. if ( ! function_exists( 'wp_admin_notice' ) ) { return; } $message = wp_kses( __( 'We highly recommend inviting users to join WordPress.com and log in securely using to ensure maximum security and efficiency.', 'jetpack' ), array( 'a' => array( 'class' => array(), 'href' => array(), 'rel' => array(), 'target' => array(), ), ) ); // @phan-suppress-next-line PhanUndeclaredFunction -- Existence of wp_admin_notice (added in WP 6.4) is checked above. @phan-suppress-current-line UnusedPluginSuppression wp_admin_notice( $message, array( 'id' => 'invitation_message', 'type' => 'info', 'dismissible' => false, 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ), ) ); } /** * Render a note that wp.com invites will be automatically revoked. */ public function render_invitations_notices_for_deleted_users() { // @todo Remove function_exists check (and phan suppression below) when we drop support for WP 6.3. if ( ! function_exists( 'wp_admin_notice' ) ) { return; } check_admin_referer( 'bulk-users' ); // When one user is deleted, the param is `user`, when multiple users are deleted, the param is `users`. // We start with `users` and fallback to `user`. $user_id = isset( $_GET['user'] ) ? intval( wp_unslash( $_GET['user'] ) ) : null; $user_ids = isset( $_GET['users'] ) ? array_map( 'intval', wp_unslash( $_GET['users'] ) ) : array( $user_id ); $users_with_invites = array_filter( $user_ids, function ( $user_id ) { return $user_id !== null && self::has_pending_wpcom_invite( $user_id ); } ); $users_with_invites = array_map( function ( $user_id ) { $user = get_user_by( 'id', $user_id ); return $user->user_login; }, $users_with_invites ); $invites_count = count( $users_with_invites ); if ( $invites_count > 0 ) { $users_with_invites = implode( ', ', $users_with_invites ); $message = wp_kses( sprintf( /* translators: %s is a comma-separated list of user logins. */ _n( 'WordPress.com invitation will be automatically revoked for user: %s.', 'WordPress.com invitations will be automatically revoked for users: %s.', $invites_count, 'jetpack' ), $users_with_invites ), array( 'strong' => true ) ); // @phan-suppress-next-line PhanUndeclaredFunction -- Existence of wp_admin_notice (added in WP 6.4) is checked above. @phan-suppress-current-line UnusedPluginSuppression wp_admin_notice( $message, array( 'id' => 'invitation_message', 'type' => 'info', 'dismissible' => false, 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ), ) ); } } /** * Render WordPress.com invite checkbox for new user registration. * * @param string $type The type of new user form the hook follows. */ public function render_wpcom_invite_checkbox( $type ) { /* * Only check this box by default on WordPress.com sites * that do not use the WooCommerce plugin. */ $is_checked = ( new Host() )->is_wpcom_platform() && ! class_exists( 'WooCommerce' ); if ( $type === 'add-new-user' ) { ?>