Current Path : /home/theafprt/conviviality360.com/wp-content/plugins/user-switching/ |
Current File : /home/theafprt/conviviality360.com/wp-content/plugins/user-switching/user-switching.php |
<?php /** * User Switching plugin for WordPress * * @package user-switching * @link https://github.com/johnbillion/user-switching * @author John Blackbourn * @copyright 2009-2025 John Blackbourn * @license GPL v2 or later * * Plugin Name: User Switching * Description: Instant switching between user accounts in WordPress * Version: 1.9.2 * Plugin URI: https://wordpress.org/plugins/user-switching/ * Author: John Blackbourn * Author URI: https://johnblackbourn.com * Text Domain: user-switching * Domain Path: /languages/ * Network: true * Requires at least: 6.0 * Requires PHP: 7.4 * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Main singleton class for the User Switching plugin. */ final class user_switching { /** * The name used to identify the application during a WordPress redirect. * * @var string */ public static string $application = 'WordPress/User Switching'; const REDIRECT_TYPE_NONE = null; const REDIRECT_TYPE_URL = 'url'; const REDIRECT_TYPE_POST = 'post'; const REDIRECT_TYPE_TERM = 'term'; const REDIRECT_TYPE_USER = 'user'; const REDIRECT_TYPE_COMMENT = 'comment'; /** * Sets up all the filters and actions. */ public function init_hooks(): void { // Required functionality: add_filter( 'user_has_cap', [ $this, 'filter_user_has_cap' ], 10, 4 ); add_filter( 'map_meta_cap', [ $this, 'filter_map_meta_cap' ], 10, 4 ); add_filter( 'user_row_actions', [ $this, 'filter_user_row_actions' ], 10, 2 ); add_action( 'plugins_loaded', [ $this, 'action_plugins_loaded' ], 1 ); add_action( 'init', [ $this, 'action_init' ] ); add_action( 'all_admin_notices', [ $this, 'action_admin_notices' ], 1 ); add_action( 'wp_logout', 'user_switching_clear_olduser_cookie', 10, 0 ); add_action( 'wp_login', 'user_switching_clear_olduser_cookie', 10, 0 ); // Nice-to-haves: add_filter( 'ms_user_row_actions', [ $this, 'filter_user_row_actions' ], 10, 2 ); add_filter( 'login_message', [ $this, 'filter_login_message' ], 1 ); add_filter( 'removable_query_args', [ $this, 'filter_removable_query_args' ] ); add_action( 'wp_meta', [ $this, 'action_wp_meta' ] ); add_filter( 'plugin_row_meta', [ $this, 'filter_plugin_row_meta' ], 10, 2 ); add_action( 'wp_footer', [ $this, 'action_wp_footer' ] ); add_action( 'personal_options', [ $this, 'action_personal_options' ] ); add_action( 'admin_bar_menu', [ $this, 'action_admin_bar_menu' ], 11 ); add_action( 'shutdown', [ $this, 'action_shutdown_for_wp_die' ], 1, 0 ); // BuddyPress integration: add_action( 'bp_member_header_actions', [ $this, 'action_bp_button' ], 11 ); add_action( 'bp_directory_members_actions', [ $this, 'action_bp_button' ], 11 ); // bbPress integration: add_action( 'bbp_template_after_user_details_menu_items', [ $this, 'action_bbpress_button' ] ); // WooCommerce integration: add_action( 'woocommerce_login_form_start', [ $this, 'action_woocommerce_login_form_start' ], 10, 0 ); add_action( 'woocommerce_admin_order_data_after_order_details', [ $this, 'action_woocommerce_order_details' ], 1 ); add_filter( 'woocommerce_account_menu_items', [ $this, 'filter_woocommerce_account_menu_items' ], 999 ); add_filter( 'woocommerce_get_endpoint_url', [ $this, 'filter_woocommerce_get_endpoint_url' ], 10, 2 ); add_action( 'switch_to_user', [ $this, 'forget_woocommerce_session' ] ); add_action( 'switch_back_user', [ $this, 'forget_woocommerce_session' ] ); } /** * Defines the names of the cookies used by User Switching. */ public function action_plugins_loaded(): void { // User Switching's auth_cookie if ( ! defined( 'USER_SWITCHING_COOKIE' ) ) { define( 'USER_SWITCHING_COOKIE', 'wordpress_user_sw_' . COOKIEHASH ); } // User Switching's secure_auth_cookie if ( ! defined( 'USER_SWITCHING_SECURE_COOKIE' ) ) { define( 'USER_SWITCHING_SECURE_COOKIE', 'wordpress_user_sw_secure_' . COOKIEHASH ); } // User Switching's logged_in_cookie if ( ! defined( 'USER_SWITCHING_OLDUSER_COOKIE' ) ) { define( 'USER_SWITCHING_OLDUSER_COOKIE', 'wordpress_user_sw_olduser_' . COOKIEHASH ); } } /** * Outputs the 'Switch To' link on the user editing screen if the current user has permission to switch to them. * * @param WP_User $user User object for this screen. */ public function action_personal_options( WP_User $user ): void { $link = self::maybe_switch_url( $user ); if ( ! $link ) { return; } ?> <tr class="user-switching-wrap"> <th scope="row"> <?php echo esc_html_x( 'User Switching', 'User Switching title on user profile screen', 'user-switching' ); ?> </th> <td> <a id="user_switching_switcher" class="button" href="<?php echo esc_url( $link ); ?>"> <?php esc_html_e( 'Switch To', 'user-switching' ); ?> </a> </td> </tr> <?php } /** * Returns whether the current logged in user is being remembered in the form of a persistent browser cookie * (ie. they checked the 'Remember Me' check box when they logged in). This is used to persist the 'remember me' * value when the user switches to another user. * * @return bool Whether the current user is being 'remembered'. */ public static function remember(): bool { /** This filter is documented in wp-includes/pluggable.php */ $cookie_life = apply_filters( 'auth_cookie_expiration', 172800, get_current_user_id(), false ); $current = wp_parse_auth_cookie( '', 'logged_in' ); if ( ! $current ) { return false; } // Here we calculate the expiration length of the current auth cookie and compare it to the default expiration. // If it's greater than this, then we know the user checked 'Remember Me' when they logged in. return ( intval( $current['expiration'] ) - time() > $cookie_life ); } /** * Loads localisation files and routes actions depending on the 'action' query var. */ public function action_init(): void { load_plugin_textdomain( 'user-switching', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); if ( ! isset( $_REQUEST['action'] ) ) { return; } switch ( $_REQUEST['action'] ) { // We're attempting to switch to another user: case 'switch_to_user': $user_id = absint( $_REQUEST['user_id'] ?? 0 ); // Check authentication: if ( ! current_user_can( 'switch_to_user', $user_id ) ) { wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 403 ); } // Check intent: check_admin_referer( "switch_to_user_{$user_id}" ); $current_user = wp_get_current_user(); $target = get_userdata( $user_id ); if ( ! $target ) { wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 404 ); } $duplicate = self::get_duplicated_switch( $target, $current_user ); if ( $duplicate && ! isset( $_GET['force_switch_user'] ) ) { // Prevent Query Monitor from showing a stack trace for the wp_die() call: // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores do_action( 'qm/cease' ); $message = sprintf( /* Translators: 1: The name of the user who is currently switched to the target user, 2: The name of the target user, 3: Period of time (for example "5 minutes") */ __( '%1$s is currently switched to %2$s. They switched %3$s ago. Do you want to continue switching?', 'user-switching' ), $duplicate['user']->display_name, $target->display_name, human_time_diff( $duplicate['login'] ), ); $yes = sprintf( /* Translators: %s is the name of the target user */ __( 'Yes, switch to %s', 'user-switching' ), $target->display_name, ); $no = __( 'No, go back', 'user-switching' ); wp_die( sprintf( '%1$s<br><br><a class="button" href="%2$s">%3$s</a> <a class="button" href="%4$s">%5$s</a>', esc_html( $message ), esc_url( add_query_arg( 'force_switch_user', '1' ) ), esc_html( $yes ), 'javascript:history.back()', esc_html( $no ), ), '', [ 'response' => 409, 'back_link' => false, ], ); } // Switch user: $user = switch_to_user( $target->ID, self::remember() ); if ( $user ) { $redirect_to = self::get_redirect( $user, $current_user ); // Redirect to the dashboard or the home URL depending on capabilities: $args = [ 'user_switched' => 'true', ]; if ( ! $redirect_to ) { $redirect_to = current_user_can( 'read' ) ? admin_url() : home_url(); } wp_safe_redirect( add_query_arg( $args, $redirect_to ), 302, self::$application ); exit; } else { wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 404 ); } break; // We're attempting to switch back to the originating user: case 'switch_to_olduser': // Fetch the originating user data: $old_user = self::get_old_user(); if ( ! $old_user ) { wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 400 ); } // Check authentication: if ( ! self::authenticate_old_user( $old_user ) ) { wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 403 ); } // Check intent: check_admin_referer( "switch_to_olduser_{$old_user->ID}" ); $current_user = wp_get_current_user(); // Switch user: if ( switch_to_user( $old_user->ID, self::remember(), false ) ) { if ( ! empty( $_REQUEST['interim-login'] ) && function_exists( 'login_header' ) ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $GLOBALS['interim_login'] = 'success'; login_header( '', '' ); exit; } $redirect_to = self::get_redirect( $old_user, $current_user ); $args = [ 'user_switched' => 'true', 'switched_back' => 'true', ]; if ( ! $redirect_to ) { $redirect_to = admin_url( 'users.php' ); } wp_safe_redirect( add_query_arg( $args, $redirect_to ), 302, self::$application ); exit; } else { wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 404 ); } break; // We're attempting to switch off the current user: case 'switch_off': // Check authentication: if ( ! current_user_can( 'switch_off' ) ) { /* Translators: "switch off" means to temporarily log out */ wp_die( esc_html__( 'Could not switch off.', 'user-switching' ), 403 ); } $current_user = wp_get_current_user(); // Check intent: check_admin_referer( 'switch_off' ); // Switch off: if ( switch_off_user() ) { $redirect_to = self::get_redirect( null, $current_user ); $args = [ 'switched_off' => 'true', ]; if ( ! $redirect_to ) { $redirect_to = home_url(); } wp_safe_redirect( add_query_arg( $args, $redirect_to ), 302, self::$application ); exit; } else { /* Translators: "switch off" means to temporarily log out */ wp_die( esc_html__( 'Could not switch off.', 'user-switching' ), 403 ); } break; } } /** * Detects if the target user has any sessions that originated from another user switching into their account. * * Returns information about the first such session, if any. * * @param WP_User $target Target user. * @param WP_User $ignore User to ignore when checking for duplicate switches. * @return array|null { * Information about the duplicate, or null if there is none. * * @type int $login The login time of the session that originated from another user. * @type WP_User $user The user who switched into the target user's account. * } * @phpstan-return array{ * login: int, * user: WP_User, * }|null */ public static function get_duplicated_switch( WP_User $target, WP_User $ignore ): ?array { // Fetch the user sessions for the target user: $sessions = WP_Session_Tokens::get_instance( $target->ID ); // Determine if any of the target user's sessions originated from another user: $other_user_sessions = array_filter( $sessions->get_all(), static fn ( array $session ): bool => ( isset( $session['switched_from_id'] ) && $session['switched_from_id'] !== $ignore->ID ) ); if ( empty( $other_user_sessions ) ) { return null; } $session = reset( $other_user_sessions ); $switched_from_user = get_userdata( $session['switched_from_id'] ); if ( ! $switched_from_user ) { return null; } return [ 'login' => $session['login'], 'user' => $switched_from_user, ]; } /** * Fetches the URL to redirect to for a given user (used after switching). * * @param WP_User $new_user Optional. The new user's WP_User object. * @param WP_User $old_user Optional. The old user's WP_User object. * @return string The URL to redirect to. */ protected static function get_redirect( ?WP_User $new_user = null, ?WP_User $old_user = null ): string { $redirect_to = ''; $requested_redirect_to = ''; $redirect_type = self::REDIRECT_TYPE_NONE; if ( ! empty( $_REQUEST['redirect_to'] ) ) { // URL $redirect_to = self::remove_query_args( wp_unslash( $_REQUEST['redirect_to'] ) ); $requested_redirect_to = wp_unslash( $_REQUEST['redirect_to'] ); $redirect_type = self::REDIRECT_TYPE_URL; } elseif ( ! empty( $_GET['redirect_to_post'] ) ) { // Post $post_id = absint( $_GET['redirect_to_post'] ); $redirect_type = self::REDIRECT_TYPE_POST; if ( is_post_publicly_viewable( $post_id ) ) { $link = get_permalink( $post_id ); if ( is_string( $link ) ) { $redirect_to = $link; $requested_redirect_to = $link; } } } elseif ( ! empty( $_GET['redirect_to_term'] ) ) { // Term $term = get_term( absint( $_GET['redirect_to_term'] ) ); $redirect_type = self::REDIRECT_TYPE_TERM; if ( ( $term instanceof WP_Term ) && is_taxonomy_viewable( $term->taxonomy ) ) { $link = get_term_link( $term ); if ( is_string( $link ) ) { $redirect_to = $link; $requested_redirect_to = $link; } } } elseif ( ! empty( $_GET['redirect_to_user'] ) ) { // User $user = get_userdata( absint( $_GET['redirect_to_user'] ) ); $redirect_type = self::REDIRECT_TYPE_USER; if ( $user instanceof WP_User ) { $link = get_author_posts_url( $user->ID ); if ( is_string( $link ) ) { $redirect_to = $link; $requested_redirect_to = $link; } } } elseif ( ! empty( $_GET['redirect_to_comment'] ) ) { // Comment $comment = get_comment( absint( $_GET['redirect_to_comment'] ) ); $redirect_type = self::REDIRECT_TYPE_COMMENT; if ( $comment instanceof WP_Comment ) { if ( 'approved' === wp_get_comment_status( $comment ) ) { $link = get_comment_link( $comment ); if ( is_string( $link ) ) { $redirect_to = $link; $requested_redirect_to = $link; } } elseif ( is_post_publicly_viewable( (int) $comment->comment_post_ID ) ) { $link = get_permalink( (int) $comment->comment_post_ID ); if ( is_string( $link ) ) { $redirect_to = $link; $requested_redirect_to = $link; } } } } if ( ! $new_user ) { /** This filter is documented in wp-login.php */ $redirect_to = apply_filters( 'logout_redirect', $redirect_to, $requested_redirect_to, $old_user ); } else { /** This filter is documented in wp-login.php */ $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $new_user ); } /** * Filters the redirect location after a user switches to another account or switches off. * * @since 1.7.0 * * @param string $redirect_to The target redirect location, or an empty string if none is specified. * @param string|null $redirect_type The redirect type, see the `user_switching::REDIRECT_*` constants. * @param WP_User|null $new_user The user being switched to, or null if there is none. * @param WP_User|null $old_user The user being switched from, or null if there is none. */ return apply_filters( 'user_switching_redirect_to', $redirect_to, $redirect_type, $new_user, $old_user ); } /** * Displays the 'Switched to {user}' and 'Switch back to {user}' messages in the admin area. */ public function action_admin_notices(): void { $user = wp_get_current_user(); $old_user = self::get_old_user(); if ( $old_user ) { $switched_locale = false; $lang_attr = ''; $locale = get_user_locale( $old_user ); $switched_locale = switch_to_locale( $locale ); $lang_attr = str_replace( '_', '-', $locale ); ?> <div id="user_switching" class="updated notice notice-success is-dismissible"> <?php if ( $lang_attr ) { printf( '<p lang="%s">', esc_attr( $lang_attr ) ); } else { echo '<p>'; } ?> <span class="dashicons dashicons-admin-users" style="color:#56c234" aria-hidden="true"></span> <?php $message = ''; $just_switched = isset( $_GET['user_switched'] ); if ( $just_switched ) { $message = esc_html( self::switched_to_message( $user ) ); } $switch_back_url = add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_back_url( $old_user ) ); $message .= sprintf( ' <a href="%s">%s</a>.', esc_url( $switch_back_url ), esc_html( self::switch_back_message( $old_user ) ) ); /** * Filters the contents of the message that's displayed to switched users in the admin area. * * @since 1.1.0 * * @param string $message The message displayed to the switched user. * @param WP_User $user The current user object. * @param WP_User $old_user The old user object. * @param string $switch_back_url The switch back URL. * @param bool $just_switched Whether the user made the switch on this page request. */ $message = apply_filters( 'user_switching_switched_message', $message, $user, $old_user, $switch_back_url, $just_switched ); echo wp_kses( $message, [ 'a' => [ 'href' => [], ], ] ); ?> </p> </div> <?php if ( $switched_locale ) { restore_previous_locale(); } } elseif ( isset( $_GET['user_switched'] ) ) { ?> <div id="user_switching" class="updated notice notice-success is-dismissible"> <p> <?php if ( isset( $_GET['switched_back'] ) ) { echo esc_html( self::switched_back_message( $user ) ); } else { echo esc_html( self::switched_to_message( $user ) ); } ?> </p> </div> <?php } } /** * Validates the old user cookie and returns its user data. * * @return false|WP_User False if there's no old user cookie or it's invalid, WP_User object if it's present and valid. */ public static function get_old_user() { $cookie = user_switching_get_olduser_cookie(); if ( ! empty( $cookie ) ) { $old_user_id = wp_validate_auth_cookie( $cookie, 'logged_in' ); if ( $old_user_id ) { return get_userdata( $old_user_id ); } } return false; } /** * Authenticates an old user by verifying the latest entry in the auth cookie. * * @param WP_User $user A WP_User object (usually from the logged_in cookie). * @return bool Whether verification with the auth cookie passed. */ public static function authenticate_old_user( WP_User $user ): bool { $cookie = user_switching_get_auth_cookie(); if ( ! empty( $cookie ) ) { if ( self::secure_auth_cookie() ) { $scheme = 'secure_auth'; } else { $scheme = 'auth'; } $old_user_id = wp_validate_auth_cookie( end( $cookie ), $scheme ); if ( $old_user_id ) { return ( $user->ID === $old_user_id ); } } return false; } /** * Adds a 'Switch back to {user}' link to the account menu, and a `Switch To` link to the user edit menu. * * @param WP_Admin_Bar $wp_admin_bar The admin bar object. */ public function action_admin_bar_menu( WP_Admin_Bar $wp_admin_bar ): void { if ( ! is_admin_bar_showing() ) { return; } if ( $wp_admin_bar->get_node( 'user-actions' ) ) { $parent = 'user-actions'; } else { return; } $old_user = self::get_old_user(); if ( $old_user ) { $wp_admin_bar->add_node( [ 'parent' => $parent, 'id' => 'switch-back', 'title' => esc_html( self::switch_back_message( $old_user ) ), 'href' => add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_back_url( $old_user ) ), ] ); } if ( current_user_can( 'switch_off' ) ) { $url = self::switch_off_url(); $redirect_to = is_admin() ? self::get_admin_redirect_to() : [ 'redirect_to' => rawurlencode( self::current_url() ), ]; if ( is_array( $redirect_to ) ) { $url = add_query_arg( $redirect_to, $url ); } $wp_admin_bar->add_node( [ 'parent' => $parent, 'id' => 'switch-off', /* Translators: "switch off" means to temporarily log out */ 'title' => esc_html__( 'Switch Off', 'user-switching' ), 'href' => $url, ] ); } if ( ! is_admin() && is_author() && ( get_queried_object() instanceof WP_User ) ) { if ( $old_user ) { $wp_admin_bar->add_node( [ 'parent' => 'edit', 'id' => 'author-switch-back', 'title' => esc_html( self::switch_back_message( $old_user ) ), 'href' => add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_back_url( $old_user ) ), ] ); } elseif ( current_user_can( 'switch_to_user', get_queried_object_id() ) ) { $wp_admin_bar->add_node( [ 'parent' => 'edit', 'id' => 'author-switch-to', 'title' => esc_html__( 'Switch To', 'user-switching' ), 'href' => add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_to_url( get_queried_object() ) ), ] ); } } } /** * Adds a 'Switch back to {user}' link to access denied messages within the admin area. * * Note that this doesn't appear for the "You need a higher level of permission" errors * because they use a standard `wp_die()` call rather than `admin_page_access_denied`. */ public function action_shutdown_for_wp_die(): void { if ( ! did_action( 'admin_page_access_denied' ) ) { return; } $old_user = self::get_old_user(); if ( ! ( $old_user instanceof WP_User ) ) { return; } $url = add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_back_url( $old_user ) ); printf( '<p style="%s" id="user_switching_wp_die"><a href="%s">%s</a></p>', 'border-top: 1px solid #dadada; margin-top: 3em; padding-top: 2em', esc_url( $url ), esc_html( self::switch_back_message( $old_user ) ) ); ?> <script> // Move the switch back link so it's within the wp_die message container. document.addEventListener( 'DOMContentLoaded', function() { document.querySelector( '.wp-die-message' ).appendChild( document.getElementById( 'user_switching_wp_die' ) ); } ); </script> <?php } /** * Returns a context-aware redirect parameter for use when switching off in the admin area. * * This is used to redirect the user to the URL of the item they're editing at the time. * * @return ?array<string, int> */ public static function get_admin_redirect_to(): ?array { if ( ! empty( $_GET['post'] ) ) { // Post return [ 'redirect_to_post' => intval( $_GET['post'] ), ]; } elseif ( ! empty( $_GET['tag_ID'] ) ) { // Term return [ 'redirect_to_term' => intval( $_GET['tag_ID'] ), ]; } elseif ( ! empty( $_GET['user_id'] ) ) { // User return [ 'redirect_to_user' => intval( $_GET['user_id'] ), ]; } elseif ( ! empty( $_GET['c'] ) ) { // Comment return [ 'redirect_to_comment' => intval( $_GET['c'] ), ]; } return null; } /** * Adds a 'Switch back to {user}' link to the Meta sidebar widget. */ public function action_wp_meta(): void { $old_user = self::get_old_user(); if ( $old_user instanceof WP_User ) { $url = add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_back_url( $old_user ) ); printf( '<li id="user_switching_switch_on"><a href="%s">%s</a></li>', esc_url( $url ), esc_html( self::switch_back_message( $old_user ) ) ); } } /** * Adds a 'Switch back to {user}' link to the WordPress footer if the admin toolbar isn't showing. */ public function action_wp_footer(): void { if ( is_admin_bar_showing() || did_action( 'wp_meta' ) ) { return; } /** * Allows the 'Switch back to {user}' link in the WordPress footer to be disabled. * * @since 1.5.5 * * @param bool $show_in_footer Whether to show the 'Switch back to {user}' link in footer. */ if ( ! apply_filters( 'user_switching_in_footer', true ) ) { return; } $old_user = self::get_old_user(); if ( $old_user instanceof WP_User ) { $url = add_query_arg( [ 'redirect_to' => rawurlencode( self::current_url() ), ], self::switch_back_url( $old_user ) ); printf( '<p id="user_switching_switch_on" style="%s"><a href="%s" style="%s">%s</a></p>', 'position: fixed; bottom: 40px; padding: 0; margin: 0; left: 10px; font-size: 13px; z-index:99999;', esc_url( $url ), 'padding: 8px 10px; background: #fff; color: #3858e9;', esc_html( self::switch_back_message( $old_user ) ) ); } } /** * Adds a 'Switch back to {user}' link to the WordPress login screen. * * @param string $message The login screen message. * @return string The login screen message. */ public function filter_login_message( string $message ): string { $old_user = self::get_old_user(); if ( $old_user instanceof WP_User ) { $url = self::switch_back_url( $old_user ); if ( ! empty( $_REQUEST['interim-login'] ) ) { $url = add_query_arg( [ 'interim-login' => '1', ], $url ); } elseif ( ! empty( $_REQUEST['redirect_to'] ) ) { $url = add_query_arg( [ 'redirect_to' => rawurlencode( wp_unslash( $_REQUEST['redirect_to'] ) ), ], $url ); } $message .= '<p class="message" id="user_switching_switch_on">'; $message .= '<span class="dashicons dashicons-admin-users" style="color:#56c234" aria-hidden="true"></span> '; $message .= sprintf( '<a href="%1$s" onclick="window.location.href=\'%1$s\';return false;">%2$s</a>', esc_url( $url ), esc_html( self::switch_back_message( $old_user ) ) ); $message .= '</p>'; } return $message; } /** * Adds a 'Switch To' link to each list of user actions on the Users screen. * * @param array<string,string> $actions Array of actions to display for this user row. * @param WP_User $user The user object displayed in this row. * @return array<string,string> Array of actions to display for this user row. */ public function filter_user_row_actions( array $actions, WP_User $user ): array { $link = self::maybe_switch_url( $user ); if ( ! $link ) { return $actions; } $actions['switch_to_user'] = sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html__( 'Switch To', 'user-switching' ) ); return $actions; } /** * Adds a 'Switch To' link to each member's profile page and profile listings in BuddyPress. */ public function action_bp_button(): void { $user = null; if ( bp_is_user() ) { $user = get_userdata( bp_displayed_user_id() ); } elseif ( bp_is_members_directory() ) { $user = get_userdata( bp_get_member_user_id() ); } if ( ! $user ) { return; } $link = self::maybe_switch_url( $user ); if ( ! $link ) { return; } if ( function_exists( 'bp_members_get_user_url' ) ) { $redirect_to = bp_members_get_user_url( $user->ID ); } elseif ( function_exists( 'bp_core_get_user_domain' ) ) { $redirect_to = bp_core_get_user_domain( $user->ID ); } else { $redirect_to = home_url(); } $link = add_query_arg( [ 'redirect_to' => rawurlencode( $redirect_to ), ], $link ); $components = array_keys( buddypress()->active_components ); echo bp_get_button( [ 'id' => 'user_switching', 'component' => reset( $components ), 'link_href' => esc_url( $link ), 'link_text' => esc_html__( 'Switch To', 'user-switching' ), 'wrapper_id' => 'user_switching_switch_to', ] ); } /** * Adds a 'Switch To' link to each member's profile page in bbPress. */ public function action_bbpress_button(): void { $user = get_userdata( bbp_get_user_id() ); if ( ! $user ) { return; } $link = self::maybe_switch_url( $user ); if ( ! $link ) { return; } $link = add_query_arg( [ 'redirect_to' => rawurlencode( bbp_get_user_profile_url( $user->ID ) ), ], $link ); echo '<ul id="user_switching_switch_to">'; printf( '<li><a href="%s">%s</a></li>', esc_url( $link ), esc_html__( 'Switch To', 'user-switching' ) ); echo '</ul>'; } /** * Filters the array of row meta for each plugin in the Plugins list table. * * @param array<int,string> $plugin_meta An array of the plugin row's meta data. * @param string $plugin_file Path to the plugin file relative to the plugins directory. * @return array<int,string> An array of the plugin row's meta data. */ public function filter_plugin_row_meta( array $plugin_meta, $plugin_file ): array { if ( 'user-switching/user-switching.php' !== $plugin_file ) { return $plugin_meta; } $plugin_meta[] = sprintf( '<a href="%1$s"><span class="dashicons dashicons-star-filled" aria-hidden="true" style="font-size:14px;line-height:1.3"></span>%2$s</a>', 'https://github.com/sponsors/johnbillion', esc_html_x( 'Sponsor', 'verb', 'user-switching' ) ); return $plugin_meta; } /** * Filters the list of query arguments which get removed from admin area URLs in WordPress. * * @link https://core.trac.wordpress.org/ticket/23367 * * @param array<int,string> $args Array of removable query arguments. * @return array<int,string> Updated array of removable query arguments. */ public function filter_removable_query_args( array $args ): array { return array_merge( $args, [ 'user_switched', 'switched_off', 'switched_back', ] ); } /** * Returns the switch to or switch back URL for a given user. * * @param WP_User $user The user to be switched to. * @return string|false The required URL, or false if there's no old user or the user doesn't have the required capability. */ public static function maybe_switch_url( WP_User $user ) { $old_user = self::get_old_user(); if ( $old_user && ( $old_user->ID === $user->ID ) ) { return self::switch_back_url( $old_user ); } elseif ( current_user_can( 'switch_to_user', $user->ID ) ) { return self::switch_to_url( $user ); } else { return false; } } /** * Returns the nonce-secured URL needed to switch to a given user ID. * * @param WP_User $user The user to be switched to. * @return string The required URL. */ public static function switch_to_url( WP_User $user ): string { return wp_nonce_url( add_query_arg( [ 'action' => 'switch_to_user', 'user_id' => $user->ID, 'nr' => 1, ], wp_login_url() ), "switch_to_user_{$user->ID}" ); } /** * Returns the nonce-secured URL needed to switch back to the originating user. * * @param WP_User $user The old user. * @return string The required URL. */ public static function switch_back_url( WP_User $user ): string { return wp_nonce_url( add_query_arg( [ 'action' => 'switch_to_olduser', 'nr' => 1, ], wp_login_url() ), "switch_to_olduser_{$user->ID}" ); } /** * Returns the nonce-secured URL needed to switch off the current user. * * @since 1.9.0 The `$user` parameter has been removed as it's no longer needed. * * @return string The required URL. */ public static function switch_off_url(): string { return wp_nonce_url( add_query_arg( [ 'action' => 'switch_off', 'nr' => 1, ], wp_login_url() ), 'switch_off' ); } /** * Returns the message shown to the user when they've switched to a user. * * @param WP_User $user The concerned user. * @return string The message. */ public static function switched_to_message( WP_User $user ): string { $message = sprintf( /* Translators: 1: user display name; 2: username; */ __( 'Switched to %1$s (%2$s).', 'user-switching' ), $user->display_name, $user->user_login ); // Removes the user login from this message without invalidating existing translations return str_replace( sprintf( ' (%s)', $user->user_login ), '', $message ); } /** * Returns the message shown to the user for the link to switch back to their original user. * * @param WP_User $user The concerned user. * @return string The message. */ public static function switch_back_message( WP_User $user ): string { $message = sprintf( /* Translators: 1: user display name; 2: username; */ __( 'Switch back to %1$s (%2$s)', 'user-switching' ), $user->display_name, $user->user_login ); // Removes the user login from this message without invalidating existing translations return str_replace( sprintf( ' (%s)', $user->user_login ), '', $message ); } /** * Returns the message shown to the user when they've switched back to their original user. * * @param WP_User $user The concerned user. * @return string The message. */ public static function switched_back_message( WP_User $user ): string { $message = sprintf( /* Translators: 1: user display name; 2: username; */ __( 'Switched back to %1$s (%2$s).', 'user-switching' ), $user->display_name, $user->user_login ); // Removes the user login from this message without invalidating existing translations return str_replace( sprintf( ' (%s)', $user->user_login ), '', $message ); } /** * Returns the current URL. * * @return string The current URL. */ public static function current_url(): string { return ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } /** * Removes a list of common confirmation-style query args from a URL. * * @param string $url A URL. * @return string The URL with query args removed. */ public static function remove_query_args( $url ): string { return remove_query_arg( wp_removable_query_args(), $url ); } /** * Returns whether User Switching's equivalent of the 'logged_in' cookie should be secure. * * This is used to set the 'secure' flag on the old user cookie, for enhanced security. * * @link https://core.trac.wordpress.org/ticket/15330 * * @return bool Should the old user cookie be secure? */ public static function secure_olduser_cookie(): bool { return ( is_ssl() && ( 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) ); } /** * Returns whether User Switching's equivalent of the 'auth' cookie should be secure. * * This is used to determine whether to set a secure auth cookie. * * @return bool Whether the auth cookie should be secure. */ public static function secure_auth_cookie(): bool { return ( is_ssl() && ( 'https' === wp_parse_url( wp_login_url(), PHP_URL_SCHEME ) ) ); } /** * Adds a 'Switch back to {user}' link to the WooCommerce login screen. */ public function action_woocommerce_login_form_start(): void { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->filter_login_message( '' ); } /** * Adds a 'Switch To' link to the WooCommerce order screen. * * @param WC_Order $order The WooCommerce order object. */ public function action_woocommerce_order_details( WC_Order $order ): void { $user = $order->get_user(); if ( ! $user || ! current_user_can( 'switch_to_user', $user->ID ) ) { return; } $url = add_query_arg( [ 'redirect_to' => rawurlencode( $order->get_view_order_url() ), ], self::switch_to_url( $user ) ); printf( '<p class="form-field form-field-wide"><a href="%1$s">%2$s</a></p>', esc_url( $url ), esc_html__( 'Switch To', 'user-switching' ) ); } /** * Adds a 'Switch back to {user}' link to the My Account screen in WooCommerce. * * @param array<string, string> $items Menu items. * @return array<string, string> Menu items. */ public function filter_woocommerce_account_menu_items( array $items ): array { $old_user = self::get_old_user(); if ( ! $old_user ) { return $items; } $items['user-switching-switch-back'] = self::switch_back_message( $old_user ); return $items; } /** * Sets the URL of the 'Switch back to {user}' link in the My Account screen in WooCommerce. * * @param string $url The URL for the menu item. * @param string $endpoint The endpoint slug for the menu item. * @return string The URL for the menu item. */ public function filter_woocommerce_get_endpoint_url( string $url, string $endpoint ): string { if ( 'user-switching-switch-back' !== $endpoint ) { return $url; } $old_user = self::get_old_user(); if ( ! $old_user ) { return $url; } return self::switch_back_url( $old_user ); } /** * Instructs WooCommerce to forget the session for the current user, without deleting it. */ public function forget_woocommerce_session(): void { if ( ! function_exists( 'WC' ) ) { return; } $wc = WC(); if ( ! property_exists( $wc, 'session' ) ) { return; } if ( ! method_exists( $wc->session, 'forget_session' ) ) { return; } $wc->session->forget_session(); } /** * Filters a user's capabilities so they can be altered at runtime. * * This is used to: * * - Grant the 'switch_to_user' capability to the user if they have the ability to edit the user they're trying to * switch to (and that user is not themselves). * - Grant the 'switch_off' capability to the user if they can edit other users. * * Important: This does not get called for Super Admins. See filter_map_meta_cap() below. * * @param array<string,bool> $user_caps Array of key/value pairs where keys represent a capability name and boolean values * represent whether the user has that capability. * @param array<int,string> $required_caps Array of required primitive capabilities for the requested capability. * @param array<int,mixed> $args { * Arguments that accompany the requested capability check. * * @type string $0 Requested capability. * @type int $1 Concerned user ID. * @type mixed ...$2 Optional second and further parameters. * } * @param WP_User $user Concerned user object. * @return array<string,bool> Array of concerned user's capabilities. */ public function filter_user_has_cap( array $user_caps, array $required_caps, array $args, WP_User $user ): array { if ( 'switch_to_user' === $args[0] ) { if ( empty( $args[2] ) ) { $user_caps['switch_to_user'] = false; return $user_caps; } if ( array_key_exists( 'switch_users', $user_caps ) ) { $user_caps['switch_to_user'] = $user_caps['switch_users']; return $user_caps; } $user_caps['switch_to_user'] = ( user_can( $user->ID, 'edit_user', $args[2] ) && ( $args[2] !== $user->ID ) ); } elseif ( 'switch_off' === $args[0] ) { if ( array_key_exists( 'switch_users', $user_caps ) ) { $user_caps['switch_off'] = $user_caps['switch_users']; return $user_caps; } $user_caps['switch_off'] = user_can( $user->ID, 'edit_users' ); } return $user_caps; } /** * Filters the required primitive capabilities for the given primitive or meta capability. * * This is used to: * * - Add the 'do_not_allow' capability to the list of required capabilities when a Super Admin is trying to switch * to themselves. * * It affects nothing else as Super Admins can do everything by default. * * @param array<int,string> $required_caps Array of required primitive capabilities for the requested capability. * @param string $cap Capability or meta capability being checked. * @param int $user_id Concerned user ID. * @param array<int,mixed> $args { * Arguments that accompany the requested capability check. * * @type mixed ...$0 Optional second and further parameters. * } * @return array<int,string> Array of required capabilities for the requested action. */ public function filter_map_meta_cap( array $required_caps, $cap, $user_id, array $args ): array { if ( 'switch_to_user' === $cap ) { if ( empty( $args[0] ) || $args[0] === $user_id ) { $required_caps[] = 'do_not_allow'; } } return $required_caps; } /** * Singleton instantiator. * * @return user_switching User Switching instance. */ public static function get_instance(): user_switching { static $instance; if ( ! isset( $instance ) ) { $instance = new user_switching(); } return $instance; } /** * Private class constructor. Use `get_instance()` to get the instance. */ private function __construct() {} } if ( ! function_exists( 'user_switching_set_olduser_cookie' ) ) { /** * Sets authorisation cookies containing the originating user information. * * @since 1.4.0 The `$token` parameter was added. * * @param int $old_user_id The ID of the originating user, usually the current logged in user. * @param bool $pop Optional. Pop the latest user off the auth cookie, instead of appending the new one. Default false. * @param string $token Optional. The old user's session token to store for later reuse. Default empty string. */ function user_switching_set_olduser_cookie( $old_user_id, bool $pop = false, string $token = '' ): void { $secure_auth_cookie = user_switching::secure_auth_cookie(); $secure_olduser_cookie = user_switching::secure_olduser_cookie(); $expiration = time() + 172800; // 48 hours $auth_cookie = user_switching_get_auth_cookie(); $olduser_cookie = wp_generate_auth_cookie( $old_user_id, $expiration, 'logged_in', $token ); if ( $secure_auth_cookie ) { $auth_cookie_name = USER_SWITCHING_SECURE_COOKIE; $scheme = 'secure_auth'; } else { $auth_cookie_name = USER_SWITCHING_COOKIE; $scheme = 'auth'; } if ( $pop ) { array_pop( $auth_cookie ); } else { array_push( $auth_cookie, wp_generate_auth_cookie( $old_user_id, $expiration, $scheme, $token ) ); } $auth_cookie = wp_json_encode( $auth_cookie ); if ( false === $auth_cookie ) { return; } /** * Fires immediately before the User Switching authentication cookie is set. * * @since 1.4.0 * * @param string $auth_cookie JSON-encoded array of authentication cookie values. * @param int $expiration The time when the authentication cookie expires as a UNIX timestamp. * @param int $old_user_id User ID. * @param string $scheme Authentication scheme. Values include 'auth' or 'secure_auth'. * @param string $token User's session token to use for the latest cookie. */ do_action( 'set_user_switching_cookie', $auth_cookie, $expiration, $old_user_id, $scheme, $token ); $scheme = 'logged_in'; /** * Fires immediately before the User Switching old user cookie is set. * * @since 1.4.0 * * @param string $olduser_cookie The old user cookie value. * @param int $expiration The time when the logged-in authentication cookie expires as a UNIX timestamp. * @param int $old_user_id User ID. * @param string $scheme Authentication scheme. Values include 'auth' or 'secure_auth'. * @param string $token User's session token to use for this cookie. */ do_action( 'set_olduser_cookie', $olduser_cookie, $expiration, $old_user_id, $scheme, $token ); /** * Allows preventing auth cookies from actually being sent to the client. * * @since 1.5.4 * * @param bool $send Whether to send auth cookies to the client. */ if ( ! apply_filters( 'user_switching_send_auth_cookies', true ) ) { return; } setcookie( $auth_cookie_name, $auth_cookie, $expiration, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_auth_cookie, true ); setcookie( USER_SWITCHING_OLDUSER_COOKIE, $olduser_cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure_olduser_cookie, true ); } } if ( ! function_exists( 'user_switching_clear_olduser_cookie' ) ) { /** * Clears the cookies containing the originating user, or pops the latest item off the end if there's more than one. * * @param bool $clear_all Optional. Whether to clear the cookies (as opposed to just popping the last user off the end). Default true. */ function user_switching_clear_olduser_cookie( bool $clear_all = true ): void { $auth_cookie = user_switching_get_auth_cookie(); if ( ! empty( $auth_cookie ) ) { array_pop( $auth_cookie ); } if ( $clear_all || empty( $auth_cookie ) ) { /** * Fires just before the user switching cookies are cleared. * * @since 1.4.0 */ do_action( 'clear_olduser_cookie' ); /** This filter is documented in user-switching.php */ if ( ! apply_filters( 'user_switching_send_auth_cookies', true ) ) { return; } $expire = time() - 31536000; setcookie( USER_SWITCHING_COOKIE, ' ', $expire, SITECOOKIEPATH, COOKIE_DOMAIN ); setcookie( USER_SWITCHING_SECURE_COOKIE, ' ', $expire, SITECOOKIEPATH, COOKIE_DOMAIN ); setcookie( USER_SWITCHING_OLDUSER_COOKIE, ' ', $expire, COOKIEPATH, COOKIE_DOMAIN ); } else { if ( user_switching::secure_auth_cookie() ) { $scheme = 'secure_auth'; } else { $scheme = 'auth'; } $old_cookie = end( $auth_cookie ); $old_user_id = wp_validate_auth_cookie( $old_cookie, $scheme ); if ( $old_user_id ) { $parts = wp_parse_auth_cookie( $old_cookie, $scheme ); if ( false !== $parts ) { user_switching_set_olduser_cookie( $old_user_id, true, $parts['token'] ); } } } } } if ( ! function_exists( 'user_switching_get_olduser_cookie' ) ) { /** * Gets the value of the cookie containing the originating user. * * @return string|false The old user cookie, or boolean false if there isn't one. */ function user_switching_get_olduser_cookie() { if ( isset( $_COOKIE[ USER_SWITCHING_OLDUSER_COOKIE ] ) ) { return wp_unslash( $_COOKIE[ USER_SWITCHING_OLDUSER_COOKIE ] ); } else { return false; } } } if ( ! function_exists( 'user_switching_get_auth_cookie' ) ) { /** * Gets the value of the auth cookie containing the list of originating users. * * @return array<int,string> Array of originating user authentication cookie values. Empty array if there are none. */ function user_switching_get_auth_cookie(): array { if ( user_switching::secure_auth_cookie() ) { $auth_cookie_name = USER_SWITCHING_SECURE_COOKIE; } else { $auth_cookie_name = USER_SWITCHING_COOKIE; } if ( isset( $_COOKIE[ $auth_cookie_name ] ) && is_string( $_COOKIE[ $auth_cookie_name ] ) ) { $cookie = json_decode( wp_unslash( $_COOKIE[ $auth_cookie_name ] ) ); } if ( ! isset( $cookie ) || ! is_array( $cookie ) ) { $cookie = []; } return $cookie; } } if ( ! function_exists( 'switch_to_user' ) ) { /** * Switches the current logged in user to the specified user. * * @param int $user_id The ID of the user to switch to. * @param bool $remember Optional. Whether to 'remember' the user in the form of a persistent browser cookie. Default false. * @param bool $set_old_user Optional. Whether to set the old user cookie. Default true. * @return false|WP_User WP_User object on success, false on failure. */ function switch_to_user( $user_id, bool $remember = false, bool $set_old_user = true ) { $user = get_userdata( $user_id ); if ( ! $user ) { return false; } $old_user_id = ( is_user_logged_in() ) ? get_current_user_id() : false; $old_token = wp_get_session_token(); $auth_cookies = user_switching_get_auth_cookie(); $auth_cookie = end( $auth_cookies ); $cookie_parts = $auth_cookie ? wp_parse_auth_cookie( $auth_cookie ) : false; if ( $set_old_user && $old_user_id ) { // Switching to another user $new_token = ''; user_switching_set_olduser_cookie( $old_user_id, false, $old_token ); } else { // Switching back, either after being switched off or after being switched to another user $new_token = $cookie_parts['token'] ?? ''; user_switching_clear_olduser_cookie( false ); } /** * Attaches the original user ID and session token to the new session when a user switches to another user. * * @param array<string, mixed> $session Array of extra data. * @return array<string, mixed> Array of extra data. */ $session_filter = static function ( array $session ) use ( $old_user_id, $old_token ): array { $session['switched_from_id'] = $old_user_id; $session['switched_from_session'] = $old_token; return $session; }; add_filter( 'attach_session_information', $session_filter, 99 ); wp_clear_auth_cookie(); wp_set_auth_cookie( $user_id, $remember, '', $new_token ); wp_set_current_user( $user_id ); remove_filter( 'attach_session_information', $session_filter, 99 ); if ( $set_old_user && $old_user_id ) { /** * Fires when a user switches to another user account. * * @since 0.6.0 * @since 1.4.0 The `$new_token` and `$old_token` parameters were added. * * @param int $user_id The ID of the user being switched to. * @param int $old_user_id The ID of the user being switched from. * @param string $new_token The token of the session of the user being switched to. Can be an empty string * or a token for a session that may or may not still be valid. * @param string $old_token The token of the session of the user being switched from. */ do_action( 'switch_to_user', $user_id, $old_user_id, $new_token, $old_token ); } else { /** * Fires when a user switches back to their originating account. * * @since 0.6.0 * @since 1.4.0 The `$new_token` and `$old_token` parameters were added. * * @param int $user_id The ID of the user being switched back to. * @param int|false $old_user_id The ID of the user being switched from, or false if the user is switching back * after having been switched off. * @param string $new_token The token of the session of the user being switched to. Can be an empty string * or a token for a session that may or may not still be valid. * @param string $old_token The token of the session of the user being switched from. */ do_action( 'switch_back_user', $user_id, $old_user_id, $new_token, $old_token ); } if ( $old_token && $old_user_id && ! $set_old_user ) { // When switching back, destroy the session for the old user $manager = WP_Session_Tokens::get_instance( $old_user_id ); $manager->destroy( $old_token ); } return $user; } } if ( ! function_exists( 'switch_off_user' ) ) { /** * Switches off the current logged in user. This logs the current user out while retaining a cookie allowing them to log * straight back in using the 'Switch back to {user}' system. * * @return bool True on success, false on failure. */ function switch_off_user(): bool { $old_user_id = get_current_user_id(); if ( ! $old_user_id ) { return false; } $old_token = wp_get_session_token(); user_switching_set_olduser_cookie( $old_user_id, false, $old_token ); wp_clear_auth_cookie(); wp_set_current_user( 0 ); /** * Fires when a user switches off. * * @since 0.6.0 * @since 1.4.0 The `$old_token` parameter was added. * * @param int $old_user_id The ID of the user switching off. * @param string $old_token The token of the session of the user switching off. */ do_action( 'switch_off_user', $old_user_id, $old_token ); return true; } } if ( ! function_exists( 'current_user_switched' ) ) { /** * Returns whether the current user switched into their account. * * @return false|WP_User False if the user isn't logged in or they didn't switch in; old user object (which evaluates to * true) if the user switched into the current user account. */ function current_user_switched() { if ( ! is_user_logged_in() ) { return false; } return user_switching::get_old_user(); } } $GLOBALS['user_switching'] = user_switching::get_instance(); $GLOBALS['user_switching']->init_hooks();