├── SECURITY.md ├── composer.json ├── readme.txt ├── LICENSE └── user-switching.php /SECURITY.md: -------------------------------------------------------------------------------- 1 | # User Switching Security Policy 2 | 3 | ## How can I report a security bug? 4 | 5 | [You can report security bugs through the official User Switching Vulnerability Disclosure Program on Patchstack](https://patchstack.com/database/vdp/user-switching). The Patchstack team helps validate, triage, and handle any security vulnerabilities. 6 | 7 | Do not report security issues on GitHub or the WordPress.org support forums. Thank you. 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "johnbillion/user-switching", 3 | "description": "Instant switching between user accounts in WordPress.", 4 | "license": "GPL-2.0-or-later", 5 | "type": "wordpress-plugin", 6 | "authors": [ 7 | { 8 | "name": "John Blackbourn", 9 | "homepage": "https://johnblackbourn.com/" 10 | } 11 | ], 12 | "homepage": "https://github.com/johnbillion/user-switching/", 13 | "support": { 14 | "issues": "https://github.com/johnbillion/user-switching/issues", 15 | "forum": "https://wordpress.org/support/plugin/user-switching", 16 | "source": "https://github.com/johnbillion/user-switching", 17 | "security": "https://github.com/johnbillion/user-switching/security/policy" 18 | }, 19 | "funding": [ 20 | { 21 | "type": "github", 22 | "url": "https://github.com/sponsors/johnbillion" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=7.4", 27 | "composer/installers": "^1.0 || ^2.0" 28 | }, 29 | "require-dev": { 30 | "johnbillion/plugin-infrastructure": "2.5.1", 31 | "johnbillion/wp-compat": "1.3.0", 32 | "php-stubs/wordpress-stubs": "6.9.0", 33 | "phpcompatibility/phpcompatibility-wp": "2.1.8", 34 | "phpstan/phpstan": "2.1.33", 35 | "phpstan/phpstan-phpunit": "2.0.11", 36 | "swissspidy/phpstan-no-private": "1.0.0", 37 | "szepeviktor/phpstan-wordpress": "2.0.3", 38 | "wp-coding-standards/wpcs": "3.3.0" 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "UserSwitching\\Tests\\": "tests/integration" 43 | } 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "composer/installers": true, 48 | "roots/wordpress-core-installer": true, 49 | "dealerdirect/phpcodesniffer-composer-installer": true 50 | }, 51 | "classmap-authoritative": true, 52 | "preferred-install": "dist", 53 | "prepend-autoloader": false, 54 | "sort-packages": true 55 | }, 56 | "extra": { 57 | "wordpress-install-dir": "vendor/wordpress/wordpress" 58 | }, 59 | "scripts": { 60 | "test": [ 61 | "@composer validate --strict --no-check-lock", 62 | "@test:phpstan", 63 | "@test:start", 64 | "@test:integration", 65 | "@test:acceptance", 66 | "@test:phpcs", 67 | "@test:stop" 68 | ], 69 | "test:acceptance": [ 70 | "acceptance-tests" 71 | ], 72 | "test:destroy": [ 73 | "tests-destroy" 74 | ], 75 | "test:integration": [ 76 | "integration-tests" 77 | ], 78 | "test:phpcs": [ 79 | "phpcs -ps --colors --report-code --report-summary --report-width=80 --basepath='./' ." 80 | ], 81 | "test:phpstan": [ 82 | "phpstan analyze -v --memory-limit=1024M" 83 | ], 84 | "test:start": [ 85 | "tests-start" 86 | ], 87 | "test:stop": [ 88 | "tests-stop" 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | # User Switching 2 | 3 | Stable tag: 1.11.1 4 | Tested up to: 6.9 5 | License: GPL v2 or later 6 | Tags: users, user switching, fast user switching, multisite, woocommerce 7 | Contributors: johnbillion 8 | Donate link: https://github.com/sponsors/johnbillion 9 | 10 | Instant switching between user accounts in WordPress. 11 | 12 | ## Description 13 | 14 | This plugin allows you to quickly swap between user accounts in WordPress at the click of a button. You'll be instantly logged out and logged in as your desired user. This is handy for helping customers on WooCommerce sites, membership sites, testing environments, or for any site where administrators need to switch between multiple accounts. 15 | 16 | ### Features 17 | 18 | * Switch user: Instantly switch to any user account from the *Users* screen. 19 | * Switch back: Instantly switch back to your originating account. 20 | * Switch off: Log out of your account but retain the ability to instantly switch back in again. 21 | * Compatible with Multisite, WooCommerce, BuddyPress, and bbPress. 22 | * Compatible with most membership and user management plugins. 23 | * Compatible with most two-factor authentication solutions (see the [FAQ](https://wordpress.org/plugins/user-switching/faq/) for more info). 24 | * Approved for use on enterprise-grade WordPress platforms such as [Altis](https://www.altis-dxp.com/) and [WordPress VIP](https://wpvip.com/). 25 | 26 | Note: User Switching supports versions of WordPress up to three years old, and PHP version 7.4 or higher. 27 | 28 | ### Security 29 | 30 | * Only users with the ability to edit other users can switch user accounts. By default this is only Administrators on single site installations, and Super Admins on Multisite installations. 31 | * Passwords are not (and cannot be) revealed. 32 | * Uses the cookie authentication system in WordPress when remembering the account(s) you've switched from and when switching back. 33 | * Implements the nonce security system in WordPress, meaning only those who intend to switch users can switch. 34 | * Full support for user session validation where appropriate. 35 | * Full support for HTTPS. 36 | * Backed by [the Patchstack Vulnerability Disclosure Program](https://patchstack.com/database/vdp/user-switching) 37 | 38 | ### Usage 39 | 40 | 1. Visit the *Users* menu in WordPress and you'll see a *Switch To* link in the list of action links for each user. 41 | 2. Click this and you will immediately switch into that user account. 42 | 3. You can switch back to your originating account via the *Switch back* link on each dashboard screen or in your profile menu in the WordPress toolbar. 43 | 44 | See the [FAQ](https://wordpress.org/plugins/user-switching/faq/) for information about the *Switch Off* feature. 45 | 46 | ### Other Plugins 47 | 48 | I maintain several other plugins for developers. Check them out: 49 | 50 | * [Query Monitor](https://wordpress.org/plugins/query-monitor/) is the developer tools panel for WordPress 51 | * [WP Crontrol](https://wordpress.org/plugins/wp-crontrol/) lets you view and control what's happening in the WP-Cron system 52 | 53 | ### Privacy Statement 54 | 55 | User Switching does not send data to any third party, nor does it include any third party resources, nor will it ever do so. 56 | 57 | User Switching makes use of browser cookies in order to allow users to switch to another account. Its cookies operate using the same mechanism as the authentication cookies in WordPress core, which means their values contain the user's `user_login` field in plain text which should be treated as potentially personally identifiable information (PII) for privacy and regulatory reasons (GDPR, CCPA, etc). The names of the cookies are: 58 | 59 | * `wordpress_user_sw_{COOKIEHASH}` 60 | * `wordpress_user_sw_secure_{COOKIEHASH}` 61 | * `wordpress_user_sw_olduser_{COOKIEHASH}` 62 | 63 | See also the FAQ for some questions relating to privacy and safety when switching between users. 64 | 65 | ### Accessibility Statement 66 | 67 | User Switching aims to be fully accessible to all of its users. It implements best practices for web accessibility, outputs semantic and structured markup, adheres to the default styles and accessibility guidelines of WordPress, uses the accessibility APIs provided by WordPress and web browsers where appropriate, and is fully accessible via keyboard. 68 | 69 | User Switching should adhere to Web Content Accessibility Guidelines (WCAG) 2.0 at level AA when used with a recent version of WordPress where its admin area itself adheres to these guidelines. If you've experienced or identified an accessibility issue in User Switching, please open a thread in [the User Switching plugin support forum](https://wordpress.org/support/plugin/user-switching/) and I'll address it swiftly. 70 | 71 | ## Screenshots 72 | 73 | 1. The *Switch To* link on the Users screen 74 | 2. The *Switch To* link on a user's profile 75 | 76 | ## Frequently Asked Questions 77 | 78 | ### Does this plugin work with PHP 8? 79 | 80 | Yes, it's actively tested and working up to PHP 8.4. 81 | 82 | ### What does "Switch off" mean? 83 | 84 | Switching off logs you out of your account but retains your user ID in an authentication cookie so you can switch straight back without having to log in again manually. It's akin to switching to no user, and being able to switch back. 85 | 86 | The *Switch Off* link can be found in your profile menu in the WordPress toolbar. Once you've switched off you'll see a *Switch back* link in a few places: 87 | 88 | * In the footer of your site 89 | * On the Log In screen 90 | * In the "Meta" widget 91 | 92 | ### Does this plugin work with WordPress Multisite? 93 | 94 | Yes, and you'll also be able to switch users from the Users screen in Network Admin. 95 | 96 | ### Does this plugin work with WooCommerce? 97 | 98 | Yes, and you'll also be able to switch users from various WooCommerce administration screens while logged in as a Shop Manager or an administrative user. 99 | 100 | ### Does this plugin work with BuddyPress? 101 | 102 | Yes, and you'll also be able to switch users from member profile screens and the member listing screen. 103 | 104 | ### Does this plugin work with bbPress? 105 | 106 | Yes, and you'll also be able to switch users from member profile screens. 107 | 108 | ### Does this plugin work if my site is using a two-factor authentication plugin? 109 | 110 | Yes, mostly. 111 | 112 | One exception I'm aware of is [Duo Security](https://wordpress.org/plugins/duo-wordpress/). If you're using this plugin, you should install the [User Switching for Duo Security](https://github.com/johnbillion/user-switching-duo-security) add-on plugin which will prevent the two-factor authentication prompt from appearing when you switch between users. 113 | 114 | ### What capability does a user need in order to switch accounts? 115 | 116 | A user needs the `edit_users` capability in order to switch user accounts. By default only Administrators have this capability, and with Multisite enabled only Super Admins have this capability. 117 | 118 | Specifically, a user needs the ability to edit the target user in order to switch to them. This means if you have custom user capability mapping in place which uses the `edit_users` or `edit_user` capabilities to affect ability of users to edit others, then User Switching should respect that. 119 | 120 | ### Can regular admins on Multisite installations switch accounts? 121 | 122 | No. This can be enabled though by installing the [User Switching for Regular Admins](https://github.com/johnbillion/user-switching-for-regular-admins) plugin. 123 | 124 | ### Can the ability to switch accounts be granted to other users or roles? 125 | 126 | Yes. The `switch_users` meta capability can be explicitly granted to a user or a role to allow them to switch users regardless of whether or not they have the `edit_users` capability. For practical purposes, the user or role will also need the `list_users` capability so they can access the Users menu in the WordPress admin area. 127 | 128 | ~~~php 129 | add_filter( 'user_has_cap', function( $allcaps, $caps, $args, $user ) { 130 | if ( 'switch_to_user' === $args[0] ) { 131 | if ( my_condition( $user ) ) { 132 | $allcaps['switch_users'] = true; 133 | } 134 | } 135 | return $allcaps; 136 | }, 9, 4 ); 137 | ~~~ 138 | 139 | Note that this needs to happen before User Switching's own capability filtering, hence the priority of `9`. 140 | 141 | ### Can the ability to switch accounts be denied from users? 142 | 143 | Yes. User capabilities in WordPress can be set to `false` to deny them from a user. Denying the `switch_users` capability prevents the user from switching users, even if they have the `edit_users` capability. 144 | 145 | ~~~php 146 | add_filter( 'user_has_cap', function( $allcaps, $caps, $args, $user ) { 147 | if ( 'switch_to_user' === $args[0] ) { 148 | if ( my_condition( $user ) ) { 149 | $allcaps['switch_users'] = false; 150 | } 151 | } 152 | return $allcaps; 153 | }, 9, 4 ); 154 | ~~~ 155 | 156 | Notes: 157 | 158 | * This needs to happen before User Switching's own capability filtering, hence the priority of `9`. 159 | * The ID of the target user can be found in `$args[2]`. 160 | 161 | ### Can I add a custom "Switch To" link to my own plugin or theme? 162 | 163 | Yes. Use the `user_switching::maybe_switch_url()` method for this. It takes care of authentication and returns a nonce-protected URL for the current user to switch into the provided user account. 164 | 165 | ~~~php 166 | if ( method_exists( 'user_switching', 'maybe_switch_url' ) ) { 167 | $url = user_switching::maybe_switch_url( $target_user ); 168 | if ( $url ) { 169 | printf( 170 | 'Switch to %2$s', 171 | esc_url( $url ), 172 | esc_html( $target_user->display_name ) 173 | ); 174 | } 175 | } 176 | ~~~ 177 | 178 | If you want to specify the URL that the user gets redirected to after switching, add a `redirect_to` parameter to the URL like so: 179 | 180 | ~~~php 181 | if ( method_exists( 'user_switching', 'maybe_switch_url' ) ) { 182 | $url = user_switching::maybe_switch_url( $target_user ); 183 | if ( $url ) { 184 | // Redirect to the home page after switching: 185 | $redirect_to = home_url(); 186 | printf( 187 | 'Switch to %2$s', 188 | esc_url( add_query_arg( 189 | 'redirect_to', 190 | rawurlencode( $redirect_to ), 191 | $url 192 | ) ), 193 | esc_html( $target_user->display_name ) 194 | ); 195 | } 196 | } 197 | ~~~ 198 | 199 | The above code also works for displaying a link to switch back to the original user, but if you want an explicit link for this you can use the following code: 200 | 201 | ~~~php 202 | if ( method_exists( 'user_switching', 'get_old_user' ) ) { 203 | $old_user = user_switching::get_old_user(); 204 | if ( $old_user ) { 205 | printf( 206 | 'Switch back to %2$s', 207 | esc_url( user_switching::switch_back_url( $old_user ) ), 208 | esc_html( $old_user->display_name ) 209 | ); 210 | } 211 | } 212 | ~~~ 213 | 214 | ### Can I determine whether the current user switched into their account? 215 | 216 | Yes. Use the `current_user_switched()` function for this. If the current user switched into their account from another then it returns a `WP_User` object for their originating user, otherwise it returns false. 217 | 218 | ~~~php 219 | if ( function_exists( 'current_user_switched' ) ) { 220 | $switched_user = current_user_switched(); 221 | if ( $switched_user ) { 222 | // User is logged in and has switched into their account. 223 | // $switched_user is the WP_User object for their originating user. 224 | } 225 | } 226 | ~~~ 227 | 228 | ### Can I log each time a user switches to another account? 229 | 230 | You can install an audit trail plugin such as [Simple History](https://wordpress.org/plugins/simple-history/), [WP Activity Log](https://wordpress.org/plugins/wp-security-audit-log/), or [Stream](https://wordpress.org/plugins/stream/), all of which have built-in support for User Switching and all of which log an entry when a user switches into another account. 231 | 232 | ### Does this plugin allow a user to frame another user for an action? 233 | 234 | Potentially yes, but User Switching includes some safety protections for this and there are further precautions you can take as a site administrator: 235 | 236 | * You can install an audit trail plugin such as [Simple History](https://wordpress.org/plugins/simple-history/), [WP Activity Log](https://wordpress.org/plugins/wp-security-audit-log/), or [Stream](https://wordpress.org/plugins/stream/), all of which have built-in support for User Switching and all of which log an entry when a user switches into another account. 237 | * User Switching stores the ID of the originating user in the new WordPress user session for the user they switch to. Although this session does not persist by default when they subsequently switch back, there will be a record of this ID if your database server has query logging enabled. 238 | * User Switching stores the login name of the originating user in an authentication cookie (see the Privacy Statement for more information). If your server access logs store cookie data, there will be a record of this login name (along with the IP address) for each access request. 239 | * User Switching triggers an action when a user switches account, switches off, or switches back (see below). You can use these actions to perform additional logging for safety purposes depending on your requirements. 240 | 241 | One or more of the above should allow you to correlate an action with the originating user when a user switches account, should you need to. 242 | 243 | Bear in mind that even without the User Switching plugin in use, any user who has the ability to edit another user can still frame another user for an action by, for example, changing their password and manually logging into that account. If you are concerned about users abusing others, you should take great care when granting users administrative rights. 244 | 245 | ### Does this plugin warn me if I attempt to switch into an account which somebody else is already switched into? 246 | 247 | Yes. When this happens you'll be shown a prompt asking you to confirm that you would like to continue switching to the affected account. 248 | 249 | This feature is useful if you have multiple users on your site who may be switching into other user accounts at the same time, for example a team of support agents. 250 | 251 | ### Can I switch users directly from the admin toolbar? 252 | 253 | Yes, there's a third party add-on plugin for this: [Admin Bar User Switching](https://wordpress.org/plugins/admin-bar-user-switching/). 254 | 255 | ### Are any plugin actions called when a user switches account? 256 | 257 | Yes. When a user switches to another account, the `switch_to_user` hook is called: 258 | 259 | ~~~php 260 | /** 261 | * Fires when a user switches to another user account. 262 | * 263 | * @since 0.6.0 264 | * @since 1.4.0 The `$new_token` and `$old_token` parameters were added. 265 | * 266 | * @param int $user_id The ID of the user being switched to. 267 | * @param int $old_user_id The ID of the user being switched from. 268 | * @param string $new_token The token of the session of the user being switched to. Can be an empty string 269 | * or a token for a session that may or may not still be valid. 270 | * @param string $old_token The token of the session of the user being switched from. 271 | */ 272 | do_action( 'switch_to_user', $user_id, $old_user_id, $new_token, $old_token ); 273 | ~~~ 274 | 275 | When a user switches back to their originating account, the `switch_back_user` hook is called: 276 | 277 | ~~~php 278 | /** 279 | * Fires when a user switches back to their originating account. 280 | * 281 | * @since 0.6.0 282 | * @since 1.4.0 The `$new_token` and `$old_token` parameters were added. 283 | * 284 | * @param int $user_id The ID of the user being switched back to. 285 | * @param int|false $old_user_id The ID of the user being switched from, or false if the user is switching back 286 | * after having been switched off. 287 | * @param string $new_token The token of the session of the user being switched to. Can be an empty string 288 | * or a token for a session that may or may not still be valid. 289 | * @param string $old_token The token of the session of the user being switched from. 290 | */ 291 | do_action( 'switch_back_user', $user_id, $old_user_id, $new_token, $old_token ); 292 | ~~~ 293 | 294 | When a user switches off, the `switch_off_user` hook is called: 295 | 296 | ~~~php 297 | /** 298 | * Fires when a user switches off. 299 | * 300 | * @since 0.6.0 301 | * @since 1.4.0 The `$old_token` parameter was added. 302 | * 303 | * @param int $old_user_id The ID of the user switching off. 304 | * @param string $old_token The token of the session of the user switching off. 305 | */ 306 | do_action( 'switch_off_user', $old_user_id, $old_token ); 307 | ~~~ 308 | 309 | When a user switches to another account, switches off, or switches back, the `user_switching_redirect_to` filter is applied to the location that they get redirected to: 310 | 311 | ~~~php 312 | /** 313 | * Filters the redirect location after a user switches to another account or switches off. 314 | * 315 | * @since 1.7.0 316 | * 317 | * @param string $redirect_to The target redirect location, or an empty string if none is specified. 318 | * @param string|null $redirect_type The redirect type, see the `user_switching::REDIRECT_*` constants. 319 | * @param WP_User|null $new_user The user being switched to, or null if there is none. 320 | * @param WP_User|null $old_user The user being switched from, or null if there is none. 321 | */ 322 | return apply_filters( 'user_switching_redirect_to', $redirect_to, $redirect_type, $new_user, $old_user ); 323 | ~~~ 324 | 325 | In addition, User Switching respects the following filters from WordPress core when appropriate: 326 | 327 | * `login_redirect` when switching to another user. 328 | * `logout_redirect` when switching off. 329 | 330 | ### How can I report a security bug? 331 | 332 | [You can report security bugs through the official User Switching Vulnerability Disclosure Program on Patchstack](https://patchstack.com/database/vdp/user-switching). The Patchstack team helps validate, triage, and handle any security vulnerabilities. 333 | 334 | ### Do you accept donations? 335 | 336 | [I am accepting sponsorships via the GitHub Sponsors program](https://github.com/sponsors/johnbillion) and any support you can give will help me maintain this plugin and keep it free for everyone. 337 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {{description}} 294 | Copyright (C) {{year}} {{fullname}} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /user-switching.php: -------------------------------------------------------------------------------- 1 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | $cookie_life ); 164 | } 165 | 166 | /** 167 | * Loads localisation files and routes actions depending on the 'action' query var. 168 | */ 169 | public function action_init(): void { 170 | load_plugin_textdomain( 'user-switching', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); 171 | 172 | if ( ! isset( $_REQUEST['action'] ) ) { 173 | return; 174 | } 175 | 176 | switch ( $_REQUEST['action'] ) { 177 | 178 | // We're attempting to switch to another user: 179 | case 'switch_to_user': 180 | $user_id = absint( $_REQUEST['user_id'] ?? 0 ); 181 | 182 | // Check authentication: 183 | if ( ! current_user_can( 'switch_to_user', $user_id ) ) { 184 | wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 403 ); 185 | } 186 | 187 | // Check intent: 188 | check_admin_referer( "switch_to_user_{$user_id}" ); 189 | 190 | $current_user = wp_get_current_user(); 191 | $target = get_userdata( $user_id ); 192 | 193 | if ( ! $target ) { 194 | wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 404 ); 195 | } 196 | 197 | $duplicate = self::get_duplicated_switch( $target, $current_user ); 198 | 199 | if ( $duplicate && ! isset( $_GET['force_switch_user'] ) ) { 200 | // Prevent Query Monitor from showing a stack trace for the wp_die() call: 201 | // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 202 | do_action( 'qm/cease' ); 203 | 204 | $message = sprintf( 205 | /* 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") */ 206 | __( '%1$s is currently switched to %2$s. They switched %3$s ago. Do you want to continue switching?', 'user-switching' ), 207 | $duplicate['user']->display_name, 208 | $target->display_name, 209 | human_time_diff( $duplicate['login'] ), 210 | ); 211 | $yes = sprintf( 212 | /* Translators: %s is the name of the target user */ 213 | __( 'Yes, switch to %s', 'user-switching' ), 214 | $target->display_name, 215 | ); 216 | $no = __( 'No, go back', 'user-switching' ); 217 | 218 | wp_die( 219 | sprintf( 220 | '%1$s

%3$s   %5$s', 221 | esc_html( $message ), 222 | esc_url( add_query_arg( 'force_switch_user', '1' ) ), 223 | esc_html( $yes ), 224 | 'javascript:history.back()', 225 | esc_html( $no ), 226 | ), 227 | '', 228 | [ 229 | 'response' => 409, 230 | 'back_link' => false, 231 | ], 232 | ); 233 | } 234 | 235 | // Switch user: 236 | $user = switch_to_user( $target->ID, self::remember() ); 237 | if ( $user ) { 238 | $redirect_to = self::get_redirect( $user, $current_user ); 239 | 240 | // Redirect to the dashboard or the home URL depending on capabilities: 241 | $args = [ 242 | 'user_switched' => 'true', 243 | ]; 244 | 245 | if ( ! $redirect_to ) { 246 | $redirect_to = current_user_can( 'read' ) ? admin_url() : home_url(); 247 | } 248 | 249 | wp_safe_redirect( add_query_arg( $args, $redirect_to ), 302, self::$application ); 250 | exit; 251 | } else { 252 | wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 404 ); 253 | } 254 | break; 255 | 256 | // We're attempting to switch back to the originating user: 257 | case 'switch_to_olduser': 258 | // Fetch the originating user data: 259 | $old_user = self::get_old_user(); 260 | if ( ! ( $old_user instanceof WP_User ) ) { 261 | wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 400 ); 262 | } 263 | 264 | // Check authentication: 265 | if ( ! self::authenticate_old_user( $old_user ) ) { 266 | wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 403 ); 267 | } 268 | 269 | // Check intent: 270 | check_admin_referer( "switch_to_olduser_{$old_user->ID}" ); 271 | 272 | $current_user = wp_get_current_user(); 273 | 274 | // Switch user: 275 | if ( switch_to_user( $old_user->ID, self::remember(), false ) ) { 276 | if ( ! empty( $_REQUEST['interim-login'] ) && function_exists( 'login_header' ) ) { 277 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 278 | $GLOBALS['interim_login'] = 'success'; 279 | login_header( '', '' ); 280 | exit; 281 | } 282 | 283 | $redirect_to = self::get_redirect( $old_user, $current_user ); 284 | $args = [ 285 | 'user_switched' => 'true', 286 | 'switched_back' => 'true', 287 | ]; 288 | 289 | if ( ! $redirect_to ) { 290 | $redirect_to = admin_url( 'users.php' ); 291 | } 292 | 293 | wp_safe_redirect( add_query_arg( $args, $redirect_to ), 302, self::$application ); 294 | exit; 295 | } else { 296 | wp_die( esc_html__( 'Could not switch users.', 'user-switching' ), 404 ); 297 | } 298 | break; 299 | 300 | // We're attempting to switch off the current user: 301 | case 'switch_off': 302 | // Check authentication: 303 | if ( ! current_user_can( 'switch_off' ) ) { 304 | /* Translators: "switch off" means to temporarily log out */ 305 | wp_die( esc_html__( 'Could not switch off.', 'user-switching' ), 403 ); 306 | } 307 | 308 | $current_user = wp_get_current_user(); 309 | 310 | // Check intent: 311 | check_admin_referer( 'switch_off' ); 312 | 313 | // Switch off: 314 | if ( switch_off_user() ) { 315 | $redirect_to = self::get_redirect( null, $current_user ); 316 | $args = [ 317 | 'switched_off' => 'true', 318 | ]; 319 | 320 | if ( ! $redirect_to ) { 321 | $redirect_to = home_url(); 322 | } 323 | 324 | wp_safe_redirect( add_query_arg( $args, $redirect_to ), 302, self::$application ); 325 | exit; 326 | } else { 327 | /* Translators: "switch off" means to temporarily log out */ 328 | wp_die( esc_html__( 'Could not switch off.', 'user-switching' ), 403 ); 329 | } 330 | break; 331 | 332 | } 333 | } 334 | 335 | /** 336 | * Detects if the target user has any sessions that originated from another user switching into their account. 337 | * 338 | * Returns information about the first such session, if any. 339 | * 340 | * @param WP_User $target Target user. 341 | * @param WP_User $ignore User to ignore when checking for duplicate switches. 342 | * @return array|null { 343 | * Information about the duplicate, or null if there is none. 344 | * 345 | * @type int $login The login time of the session that originated from another user. 346 | * @type WP_User $user The user who switched into the target user's account. 347 | * } 348 | * @phpstan-return array{ 349 | * login: int, 350 | * user: WP_User, 351 | * }|null 352 | */ 353 | public static function get_duplicated_switch( WP_User $target, WP_User $ignore ): ?array { 354 | // Fetch the user sessions for the target user: 355 | $sessions = WP_Session_Tokens::get_instance( $target->ID ); 356 | 357 | // Determine if any of the target user's sessions originated from another user: 358 | $other_user_sessions = array_filter( 359 | $sessions->get_all(), 360 | static fn ( array $session ): bool => ( 361 | isset( $session['switched_from_id'] ) && $session['switched_from_id'] !== $ignore->ID 362 | ) 363 | ); 364 | 365 | if ( empty( $other_user_sessions ) ) { 366 | return null; 367 | } 368 | 369 | $session = reset( $other_user_sessions ); 370 | $switched_from_user = get_userdata( $session['switched_from_id'] ); 371 | 372 | if ( ! $switched_from_user ) { 373 | return null; 374 | } 375 | 376 | return [ 377 | 'login' => $session['login'], 378 | 'user' => $switched_from_user, 379 | ]; 380 | } 381 | 382 | /** 383 | * Fetches the URL to redirect to for a given user (used after switching). 384 | * 385 | * @param WP_User $new_user Optional. The new user's WP_User object. 386 | * @param WP_User $old_user Optional. The old user's WP_User object. 387 | * @return string The URL to redirect to. 388 | */ 389 | protected static function get_redirect( ?WP_User $new_user = null, ?WP_User $old_user = null ): string { 390 | $redirect_to = ''; 391 | $requested_redirect_to = ''; 392 | $redirect_type = self::REDIRECT_TYPE_NONE; 393 | 394 | if ( ! empty( $_REQUEST['redirect_to'] ) ) { 395 | // URL 396 | $redirect_to = self::remove_query_args( wp_unslash( (string) $_REQUEST['redirect_to'] ) ); 397 | $requested_redirect_to = wp_unslash( $_REQUEST['redirect_to'] ); 398 | $redirect_type = self::REDIRECT_TYPE_URL; 399 | } elseif ( ! empty( $_GET['redirect_to_post'] ) ) { 400 | // Post 401 | $post_id = absint( $_GET['redirect_to_post'] ); 402 | $redirect_type = self::REDIRECT_TYPE_POST; 403 | 404 | if ( is_post_publicly_viewable( $post_id ) ) { 405 | $link = get_permalink( $post_id ); 406 | $redirect_to = $link; 407 | $requested_redirect_to = $link; 408 | } 409 | } elseif ( ! empty( $_GET['redirect_to_term'] ) ) { 410 | // Term 411 | $term = get_term( absint( $_GET['redirect_to_term'] ) ); 412 | $redirect_type = self::REDIRECT_TYPE_TERM; 413 | 414 | if ( ( $term instanceof WP_Term ) && is_taxonomy_viewable( $term->taxonomy ) ) { 415 | $link = get_term_link( $term ); 416 | $redirect_to = $link; 417 | $requested_redirect_to = $link; 418 | } 419 | } elseif ( ! empty( $_GET['redirect_to_user'] ) ) { 420 | // User 421 | $user = get_userdata( absint( $_GET['redirect_to_user'] ) ); 422 | $redirect_type = self::REDIRECT_TYPE_USER; 423 | 424 | if ( $user instanceof WP_User ) { 425 | $link = get_author_posts_url( $user->ID ); 426 | $redirect_to = $link; 427 | $requested_redirect_to = $link; 428 | } 429 | } elseif ( ! empty( $_GET['redirect_to_comment'] ) ) { 430 | // Comment 431 | $comment = get_comment( absint( $_GET['redirect_to_comment'] ) ); 432 | $redirect_type = self::REDIRECT_TYPE_COMMENT; 433 | 434 | if ( $comment instanceof WP_Comment ) { 435 | if ( 'approved' === wp_get_comment_status( $comment ) ) { 436 | $link = get_comment_link( $comment ); 437 | $redirect_to = $link; 438 | $requested_redirect_to = $link; 439 | } elseif ( is_post_publicly_viewable( (int) $comment->comment_post_ID ) ) { 440 | $link = get_permalink( (int) $comment->comment_post_ID ); 441 | $redirect_to = $link; 442 | $requested_redirect_to = $link; 443 | } 444 | } 445 | } 446 | 447 | if ( ! $new_user ) { 448 | /** This filter is documented in wp-login.php */ 449 | $redirect_to = apply_filters( 'logout_redirect', $redirect_to, $requested_redirect_to, $old_user ); 450 | } else { 451 | /** This filter is documented in wp-login.php */ 452 | $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $new_user ); 453 | } 454 | 455 | /** 456 | * Filters the redirect location after a user switches to another account or switches off. 457 | * 458 | * @since 1.7.0 459 | * 460 | * @param string $redirect_to The target redirect location, or an empty string if none is specified. 461 | * @param string|null $redirect_type The redirect type, see the `user_switching::REDIRECT_*` constants. 462 | * @param WP_User|null $new_user The user being switched to, or null if there is none. 463 | * @param WP_User|null $old_user The user being switched from, or null if there is none. 464 | */ 465 | return apply_filters( 'user_switching_redirect_to', $redirect_to, $redirect_type, $new_user, $old_user ); 466 | } 467 | 468 | /** 469 | * Displays the 'Switched to {user}' and 'Switch back to {user}' messages in the admin area. 470 | */ 471 | public function action_admin_notices(): void { 472 | $user = wp_get_current_user(); 473 | $old_user = self::get_old_user(); 474 | 475 | if ( $old_user instanceof WP_User ) { 476 | $locale = get_user_locale( $old_user ); 477 | $switched_locale = switch_to_locale( $locale ); 478 | $lang_attr = str_replace( '_', '-', $locale ); 479 | 480 | ?> 481 |
482 | ', 486 | esc_attr( $lang_attr ) 487 | ); 488 | } else { 489 | echo '

'; 490 | } 491 | ?> 492 | 493 | rawurlencode( self::current_url() ), 501 | ], self::switch_back_url( $old_user ) ); 502 | 503 | $message .= sprintf( 504 | ' %s.', 505 | esc_url( $switch_back_url ), 506 | esc_html( self::switch_back_message( $old_user ) ) 507 | ); 508 | 509 | /** 510 | * Filters the contents of the message that's displayed to switched users in the admin area. 511 | * 512 | * @since 1.1.0 513 | * 514 | * @param string $message The message displayed to the switched user. 515 | * @param WP_User $user The current user object. 516 | * @param WP_User $old_user The old user object. 517 | * @param string $switch_back_url The switch back URL. 518 | * @param bool $just_switched Whether the user made the switch on this page request. 519 | */ 520 | $message = apply_filters( 'user_switching_switched_message', $message, $user, $old_user, $switch_back_url, $just_switched ); 521 | 522 | echo wp_kses( $message, [ 523 | 'a' => [ 524 | 'href' => [], 525 | ], 526 | ] ); 527 | ?> 528 |

529 |
530 | 536 |
537 |

538 | 545 |

546 |
547 | ID === $old_user_id ); 587 | } 588 | } 589 | return false; 590 | } 591 | 592 | /** 593 | * Adds a 'Switch back to {user}' link to the account menu, and a `Switch To` link to the user edit menu. 594 | * 595 | * @param WP_Admin_Bar $wp_admin_bar The admin bar object. 596 | */ 597 | public function action_admin_bar_menu( WP_Admin_Bar $wp_admin_bar ): void { 598 | if ( ! is_admin_bar_showing() ) { 599 | return; 600 | } 601 | 602 | if ( $wp_admin_bar->get_node( 'user-actions' ) ) { 603 | $parent = 'user-actions'; 604 | } else { 605 | return; 606 | } 607 | 608 | $old_user = self::get_old_user(); 609 | 610 | if ( $old_user instanceof WP_User ) { 611 | $wp_admin_bar->add_node( [ 612 | 'parent' => $parent, 613 | 'id' => 'switch-back', 614 | 'title' => esc_html( self::switch_back_message( $old_user ) ), 615 | 'href' => add_query_arg( [ 616 | 'redirect_to' => rawurlencode( self::current_url() ), 617 | ], self::switch_back_url( $old_user ) ), 618 | ] ); 619 | } 620 | 621 | if ( current_user_can( 'switch_off' ) ) { 622 | $url = self::switch_off_url(); 623 | $redirect_to = is_admin() ? self::get_admin_redirect_to() : [ 624 | 'redirect_to' => rawurlencode( self::current_url() ), 625 | ]; 626 | 627 | if ( is_array( $redirect_to ) ) { 628 | $url = add_query_arg( $redirect_to, $url ); 629 | } 630 | 631 | $wp_admin_bar->add_node( [ 632 | 'parent' => $parent, 633 | 'id' => 'switch-off', 634 | /* Translators: "switch off" means to temporarily log out */ 635 | 'title' => esc_html__( 'Switch Off', 'user-switching' ), 636 | 'href' => $url, 637 | ] ); 638 | } 639 | 640 | if ( ! is_admin() && is_author() && ( get_queried_object() instanceof WP_User ) ) { 641 | if ( $old_user ) { 642 | $wp_admin_bar->add_node( [ 643 | 'parent' => 'edit', 644 | 'id' => 'author-switch-back', 645 | 'title' => esc_html( self::switch_back_message( $old_user ) ), 646 | 'href' => add_query_arg( [ 647 | 'redirect_to' => rawurlencode( self::current_url() ), 648 | ], self::switch_back_url( $old_user ) ), 649 | ] ); 650 | } elseif ( current_user_can( 'switch_to_user', get_queried_object_id() ) ) { 651 | $wp_admin_bar->add_node( [ 652 | 'parent' => 'edit', 653 | 'id' => 'author-switch-to', 654 | 'title' => esc_html__( 'Switch To', 'user-switching' ), 655 | 'href' => add_query_arg( [ 656 | 'redirect_to' => rawurlencode( self::current_url() ), 657 | ], self::switch_to_url( get_queried_object() ) ), 658 | ] ); 659 | } 660 | } 661 | } 662 | 663 | /** 664 | * Adds a 'Switch back to {user}' link to access denied messages within the admin area. 665 | */ 666 | public function action_shutdown_for_wp_die(): void { 667 | if ( ! did_action( 'admin_page_access_denied' ) && ! ( function_exists( 'did_filter' ) && did_filter( 'wp_die_handler' ) ) ) { 668 | return; 669 | } 670 | 671 | $old_user = self::get_old_user(); 672 | 673 | if ( ! ( $old_user instanceof WP_User ) ) { 674 | return; 675 | } 676 | 677 | $url = add_query_arg( [ 678 | 'redirect_to' => rawurlencode( self::current_url() ), 679 | ], self::switch_back_url( $old_user ) ); 680 | 681 | printf( 682 | '

%s

', 683 | 'border-top: 1px solid #dadada; margin-top: 3em; padding-top: 2em', 684 | esc_url( $url ), 685 | esc_html( self::switch_back_message( $old_user ) ) 686 | ); 687 | 688 | ?> 689 | 697 | 706 | */ 707 | public static function get_admin_redirect_to(): ?array { 708 | if ( ! empty( $_GET['post'] ) ) { 709 | // Post 710 | return [ 711 | 'redirect_to_post' => absint( $_GET['post'] ), 712 | ]; 713 | } elseif ( ! empty( $_GET['tag_ID'] ) ) { 714 | // Term 715 | return [ 716 | 'redirect_to_term' => absint( $_GET['tag_ID'] ), 717 | ]; 718 | } elseif ( ! empty( $_GET['user_id'] ) ) { 719 | // User 720 | return [ 721 | 'redirect_to_user' => absint( $_GET['user_id'] ), 722 | ]; 723 | } elseif ( ! empty( $_GET['c'] ) ) { 724 | // Comment 725 | return [ 726 | 'redirect_to_comment' => absint( $_GET['c'] ), 727 | ]; 728 | } 729 | 730 | return null; 731 | } 732 | 733 | /** 734 | * Adds a 'Switch back to {user}' link to the Meta sidebar widget. 735 | */ 736 | public function action_wp_meta(): void { 737 | $old_user = self::get_old_user(); 738 | 739 | if ( ! ( $old_user instanceof WP_User ) ) { 740 | return; 741 | } 742 | 743 | $url = add_query_arg( [ 744 | 'redirect_to' => rawurlencode( self::current_url() ), 745 | ], self::switch_back_url( $old_user ) ); 746 | printf( 747 | '
  • %s
  • ', 748 | esc_url( $url ), 749 | esc_html( self::switch_back_message( $old_user ) ) 750 | ); 751 | } 752 | 753 | /** 754 | * Adds a 'Switch back to {user}' link to the WordPress footer if the admin toolbar isn't showing. 755 | */ 756 | public function action_wp_footer(): void { 757 | if ( is_admin_bar_showing() || did_action( 'wp_meta' ) ) { 758 | return; 759 | } 760 | 761 | /** 762 | * Allows the 'Switch back to {user}' link in the WordPress footer to be disabled. 763 | * 764 | * @since 1.5.5 765 | * 766 | * @param bool $show_in_footer Whether to show the 'Switch back to {user}' link in footer. 767 | */ 768 | if ( ! apply_filters( 'user_switching_in_footer', true ) ) { 769 | return; 770 | } 771 | 772 | $old_user = self::get_old_user(); 773 | 774 | if ( ! ( $old_user instanceof WP_User ) ) { 775 | return; 776 | } 777 | 778 | $url = add_query_arg( [ 779 | 'redirect_to' => rawurlencode( self::current_url() ), 780 | ], self::switch_back_url( $old_user ) ); 781 | 782 | printf( 783 | '

    %s

    ', 784 | 'position: fixed; bottom: 40px; padding: 0; margin: 0; left: 10px; font-size: 13px; z-index:99999;', 785 | esc_url( $url ), 786 | 'padding: 8px 10px; background: #fff; color: #3858e9;', 787 | esc_html( self::switch_back_message( $old_user ) ) 788 | ); 789 | } 790 | 791 | /** 792 | * Adds a 'Switch back to {user}' link to the WordPress login screen. 793 | * 794 | * @param string $message The login screen message. 795 | * @return string The login screen message. 796 | */ 797 | public function filter_login_message( string $message ): string { 798 | $old_user = self::get_old_user(); 799 | 800 | if ( ! ( $old_user instanceof WP_User ) ) { 801 | return $message; 802 | } 803 | 804 | $url = self::switch_back_url( $old_user ); 805 | 806 | if ( ! empty( $_REQUEST['interim-login'] ) ) { 807 | $url = add_query_arg( [ 808 | 'interim-login' => '1', 809 | ], $url ); 810 | } elseif ( ! empty( $_REQUEST['redirect_to'] ) ) { 811 | $url = add_query_arg( [ 812 | 'redirect_to' => rawurlencode( wp_unslash( (string) $_REQUEST['redirect_to'] ) ), 813 | ], $url ); 814 | } 815 | 816 | $message .= '

    '; 817 | $message .= ' '; 818 | $message .= sprintf( 819 | '%2$s', 820 | esc_url( $url ), 821 | esc_html( self::switch_back_message( $old_user ) ) 822 | ); 823 | $message .= '

    '; 824 | 825 | return $message; 826 | } 827 | 828 | /** 829 | * Adds a 'Switch To' link to each list of user actions on the Users screen. 830 | * 831 | * @param array $actions Array of actions to display for this user row. 832 | * @param WP_User $user The user object displayed in this row. 833 | * @return array Array of actions to display for this user row. 834 | */ 835 | public function filter_user_row_actions( array $actions, WP_User $user ): array { 836 | $link = self::maybe_switch_url( $user ); 837 | 838 | if ( ! $link ) { 839 | return $actions; 840 | } 841 | 842 | $actions['switch_to_user'] = sprintf( 843 | '%s', 844 | esc_url( $link ), 845 | esc_html__( 'Switch To', 'user-switching' ) 846 | ); 847 | 848 | return $actions; 849 | } 850 | 851 | /** 852 | * Adds a 'Switch To' link to each member's profile page and profile listings in BuddyPress. 853 | */ 854 | public function action_bp_button(): void { 855 | $user = null; 856 | 857 | if ( bp_is_user() ) { 858 | $user = get_userdata( bp_displayed_user_id() ); 859 | } elseif ( bp_is_members_directory() ) { 860 | $user = get_userdata( bp_get_member_user_id() ); 861 | } 862 | 863 | if ( ! $user ) { 864 | return; 865 | } 866 | 867 | $link = self::maybe_switch_url( $user ); 868 | 869 | if ( ! $link ) { 870 | return; 871 | } 872 | 873 | if ( function_exists( 'bp_members_get_user_url' ) ) { 874 | $redirect_to = bp_members_get_user_url( $user->ID ); 875 | } elseif ( function_exists( 'bp_core_get_user_domain' ) ) { 876 | $redirect_to = bp_core_get_user_domain( $user->ID ); 877 | } else { 878 | $redirect_to = home_url(); 879 | } 880 | 881 | $link = add_query_arg( [ 882 | 'redirect_to' => rawurlencode( $redirect_to ), 883 | ], $link ); 884 | 885 | $components = array_keys( buddypress()->active_components ); 886 | 887 | echo bp_get_button( [ 888 | 'id' => 'user_switching', 889 | 'component' => reset( $components ), 890 | 'link_href' => esc_url( $link ), 891 | 'link_text' => esc_html__( 'Switch To', 'user-switching' ), 892 | 'wrapper_id' => 'user_switching_switch_to', 893 | ] ); 894 | } 895 | 896 | /** 897 | * Adds a 'Switch To' link to each member's profile page in bbPress. 898 | */ 899 | public function action_bbpress_button(): void { 900 | $user = get_userdata( bbp_get_user_id() ); 901 | 902 | if ( ! $user ) { 903 | return; 904 | } 905 | 906 | $link = self::maybe_switch_url( $user ); 907 | 908 | if ( ! $link ) { 909 | return; 910 | } 911 | 912 | $link = add_query_arg( [ 913 | 'redirect_to' => rawurlencode( bbp_get_user_profile_url( $user->ID ) ), 914 | ], $link ); 915 | 916 | echo ''; 923 | } 924 | 925 | /** 926 | * Filters the array of row meta for each plugin in the Plugins list table. 927 | * 928 | * @param array $plugin_meta An array of the plugin row's meta data. 929 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 930 | * @return array An array of the plugin row's meta data. 931 | */ 932 | public function filter_plugin_row_meta( array $plugin_meta, $plugin_file ): array { 933 | if ( 'user-switching/user-switching.php' !== $plugin_file ) { 934 | return $plugin_meta; 935 | } 936 | 937 | $plugin_meta[] = sprintf( 938 | '%2$s', 939 | 'https://github.com/sponsors/johnbillion', 940 | esc_html_x( 'Sponsor', 'verb', 'user-switching' ) 941 | ); 942 | 943 | return $plugin_meta; 944 | } 945 | 946 | /** 947 | * Filters the list of query arguments which get removed from admin area URLs in WordPress. 948 | * 949 | * @param array $args Array of removable query arguments. 950 | * @return array Updated array of removable query arguments. 951 | */ 952 | public function filter_removable_query_args( array $args ): array { 953 | return array_merge( $args, [ 954 | 'user_switched', 955 | 'switched_off', 956 | 'switched_back', 957 | ] ); 958 | } 959 | 960 | /** 961 | * Returns the switch to or switch back URL for a given user. 962 | * 963 | * @param WP_User $user The user to be switched to. 964 | * @return string|false The required URL, or false if there's no old user or the user doesn't have the required capability. 965 | */ 966 | public static function maybe_switch_url( WP_User $user ) { 967 | $old_user = self::get_old_user(); 968 | 969 | if ( ( $old_user instanceof WP_User ) && ( $old_user->ID === $user->ID ) ) { 970 | return self::switch_back_url( $old_user ); 971 | } elseif ( current_user_can( 'switch_to_user', $user->ID ) ) { 972 | return self::switch_to_url( $user ); 973 | } else { 974 | return false; 975 | } 976 | } 977 | 978 | /** 979 | * Returns the nonce-secured URL needed to switch to a given user ID. 980 | * 981 | * @param WP_User $user The user to be switched to. 982 | * @return string The required URL. 983 | */ 984 | public static function switch_to_url( WP_User $user ): string { 985 | return wp_nonce_url( add_query_arg( [ 986 | 'action' => 'switch_to_user', 987 | 'user_id' => $user->ID, 988 | 'nr' => 1, 989 | ], wp_login_url() ), "switch_to_user_{$user->ID}" ); 990 | } 991 | 992 | /** 993 | * Returns the nonce-secured URL needed to switch back to the originating user. 994 | * 995 | * @param WP_User $user The old user. 996 | * @return string The required URL. 997 | */ 998 | public static function switch_back_url( WP_User $user ): string { 999 | return wp_nonce_url( add_query_arg( [ 1000 | 'action' => 'switch_to_olduser', 1001 | 'nr' => 1, 1002 | ], wp_login_url() ), "switch_to_olduser_{$user->ID}" ); 1003 | } 1004 | 1005 | /** 1006 | * Returns the nonce-secured URL needed to switch off the current user. 1007 | * 1008 | * @since 1.9.0 The `$user` parameter has been removed as it's no longer needed. 1009 | * 1010 | * @return string The required URL. 1011 | */ 1012 | public static function switch_off_url(): string { 1013 | return wp_nonce_url( add_query_arg( [ 1014 | 'action' => 'switch_off', 1015 | 'nr' => 1, 1016 | ], wp_login_url() ), 'switch_off' ); 1017 | } 1018 | 1019 | /** 1020 | * Returns the message shown to the user when they've switched to a user. 1021 | * 1022 | * @param WP_User $user The concerned user. 1023 | * @return string The message. 1024 | */ 1025 | public static function switched_to_message( WP_User $user ): string { 1026 | $message = sprintf( 1027 | /* Translators: 1: user display name; 2: username; */ 1028 | __( 'Switched to %1$s (%2$s).', 'user-switching' ), 1029 | $user->display_name, 1030 | $user->user_login 1031 | ); 1032 | 1033 | // Removes the user login from this message without invalidating existing translations 1034 | return str_replace( sprintf( 1035 | ' (%s)', 1036 | $user->user_login 1037 | ), '', $message ); 1038 | } 1039 | 1040 | /** 1041 | * Returns the message shown to the user for the link to switch back to their original user. 1042 | * 1043 | * @param WP_User $user The concerned user. 1044 | * @return string The message. 1045 | */ 1046 | public static function switch_back_message( WP_User $user ): string { 1047 | $switched_locale = switch_to_locale( get_user_locale( $user ) ); 1048 | 1049 | $message = sprintf( 1050 | /* Translators: 1: user display name; 2: username; */ 1051 | __( 'Switch back to %1$s (%2$s)', 'user-switching' ), 1052 | $user->display_name, 1053 | $user->user_login 1054 | ); 1055 | 1056 | if ( $switched_locale ) { 1057 | restore_previous_locale(); 1058 | } 1059 | 1060 | // Removes the user login from this message without invalidating existing translations 1061 | return str_replace( sprintf( 1062 | ' (%s)', 1063 | $user->user_login 1064 | ), '', $message ); 1065 | } 1066 | 1067 | /** 1068 | * Returns the message shown to the user when they've switched back to their original user. 1069 | * 1070 | * @param WP_User $user The concerned user. 1071 | * @return string The message. 1072 | */ 1073 | public static function switched_back_message( WP_User $user ): string { 1074 | $message = sprintf( 1075 | /* Translators: 1: user display name; 2: username; */ 1076 | __( 'Switched back to %1$s (%2$s).', 'user-switching' ), 1077 | $user->display_name, 1078 | $user->user_login 1079 | ); 1080 | 1081 | // Removes the user login from this message without invalidating existing translations 1082 | return str_replace( sprintf( 1083 | ' (%s)', 1084 | $user->user_login 1085 | ), '', $message ); 1086 | } 1087 | 1088 | /** 1089 | * Returns the current URL. 1090 | * 1091 | * @return string The current URL. 1092 | */ 1093 | public static function current_url(): string { 1094 | $scheme = is_ssl() ? 'https' : 'http'; 1095 | return "{$scheme}://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; 1096 | } 1097 | 1098 | /** 1099 | * Removes a list of common confirmation-style query args from a URL. 1100 | * 1101 | * @param string $url A URL. 1102 | * @return string The URL with query args removed. 1103 | */ 1104 | public static function remove_query_args( $url ): string { 1105 | return remove_query_arg( wp_removable_query_args(), $url ); 1106 | } 1107 | 1108 | /** 1109 | * Returns whether User Switching's equivalent of the 'logged_in' cookie should be secure. 1110 | * 1111 | * This is used to set the 'secure' flag on the old user cookie, for enhanced security. 1112 | * 1113 | * @return bool Should the old user cookie be secure? 1114 | */ 1115 | public static function secure_olduser_cookie(): bool { 1116 | return ( is_ssl() && ( 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) ); 1117 | } 1118 | 1119 | /** 1120 | * Returns whether User Switching's equivalent of the 'auth' cookie should be secure. 1121 | * 1122 | * This is used to determine whether to set a secure auth cookie. 1123 | * 1124 | * @return bool Whether the auth cookie should be secure. 1125 | */ 1126 | public static function secure_auth_cookie(): bool { 1127 | return ( is_ssl() && ( 'https' === wp_parse_url( wp_login_url(), PHP_URL_SCHEME ) ) ); 1128 | } 1129 | 1130 | /** 1131 | * Adds a 'Switch back to {user}' link to the WooCommerce login screen. 1132 | */ 1133 | public function action_woocommerce_login_form_start(): void { 1134 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1135 | echo $this->filter_login_message( '' ); 1136 | } 1137 | 1138 | /** 1139 | * Adds a 'Switch To' link to the WooCommerce order screen. 1140 | * 1141 | * @param WC_Order $order The WooCommerce order object. 1142 | */ 1143 | public function action_woocommerce_order_details( WC_Order $order ): void { 1144 | $user = $order->get_user(); 1145 | 1146 | if ( ! $user || ! current_user_can( 'switch_to_user', $user->ID ) ) { 1147 | return; 1148 | } 1149 | 1150 | $url = add_query_arg( [ 1151 | 'redirect_to' => rawurlencode( $order->get_view_order_url() ), 1152 | ], self::switch_to_url( $user ) ); 1153 | 1154 | printf( 1155 | '

    %2$s

    ', 1156 | esc_url( $url ), 1157 | esc_html__( 'Switch To', 'user-switching' ) 1158 | ); 1159 | } 1160 | 1161 | /** 1162 | * Adds a 'Switch back to {user}' link to the My Account screen in WooCommerce. 1163 | * 1164 | * @param array $items Menu items. 1165 | * @return array Menu items. 1166 | */ 1167 | public function filter_woocommerce_account_menu_items( array $items ): array { 1168 | $old_user = self::get_old_user(); 1169 | 1170 | if ( ! ( $old_user instanceof WP_User ) ) { 1171 | return $items; 1172 | } 1173 | 1174 | $items['user-switching-switch-back'] = self::switch_back_message( $old_user ); 1175 | 1176 | return $items; 1177 | } 1178 | 1179 | /** 1180 | * Sets the URL of the 'Switch back to {user}' link in the My Account screen in WooCommerce. 1181 | * 1182 | * @param string $url The URL for the menu item. 1183 | * @param string $endpoint The endpoint slug for the menu item. 1184 | * @return string The URL for the menu item. 1185 | */ 1186 | public function filter_woocommerce_get_endpoint_url( string $url, string $endpoint ): string { 1187 | if ( 'user-switching-switch-back' !== $endpoint ) { 1188 | return $url; 1189 | } 1190 | 1191 | $old_user = self::get_old_user(); 1192 | 1193 | if ( ! ( $old_user instanceof WP_User ) ) { 1194 | return $url; 1195 | } 1196 | 1197 | return self::switch_back_url( $old_user ); 1198 | } 1199 | 1200 | /** 1201 | * Instructs WooCommerce to forget the session for the current user, without deleting it. 1202 | */ 1203 | public function forget_woocommerce_session(): void { 1204 | if ( ! function_exists( 'WC' ) ) { 1205 | return; 1206 | } 1207 | 1208 | $wc = WC(); 1209 | 1210 | if ( ! property_exists( $wc, 'session' ) ) { 1211 | return; 1212 | } 1213 | 1214 | if ( ! method_exists( $wc->session, 'forget_session' ) ) { 1215 | return; 1216 | } 1217 | 1218 | $wc->session->forget_session(); 1219 | } 1220 | 1221 | /** 1222 | * Filters a user's capabilities so they can be altered at runtime. 1223 | * 1224 | * This is used to: 1225 | * 1226 | * - Grant the 'switch_to_user' capability to the user if they have the ability to edit the user they're trying to 1227 | * switch to (and that user is not themselves). 1228 | * - Grant the 'switch_off' capability to the user if they can edit other users. 1229 | * 1230 | * Important: This does not get called for Super Admins. See filter_map_meta_cap() below. 1231 | * 1232 | * @param array $user_caps Array of key/value pairs where keys represent a capability name and boolean values 1233 | * represent whether the user has that capability. 1234 | * @param array $required_caps Array of required primitive capabilities for the requested capability. 1235 | * @param array $args { 1236 | * Arguments that accompany the requested capability check. 1237 | * 1238 | * @type string $0 Requested capability. 1239 | * @type int $1 Concerned user ID. 1240 | * @type mixed ...$2 Optional second and further parameters. 1241 | * } 1242 | * @param WP_User $user Concerned user object. 1243 | * @return array Array of concerned user's capabilities. 1244 | */ 1245 | public function filter_user_has_cap( array $user_caps, array $required_caps, array $args, WP_User $user ): array { 1246 | if ( 'switch_to_user' === $args[0] ) { 1247 | if ( empty( $args[2] ) ) { 1248 | $user_caps['switch_to_user'] = false; 1249 | return $user_caps; 1250 | } 1251 | if ( array_key_exists( 'switch_users', $user_caps ) ) { 1252 | $user_caps['switch_to_user'] = $user_caps['switch_users']; 1253 | return $user_caps; 1254 | } 1255 | 1256 | $user_caps['switch_to_user'] = ( user_can( $user->ID, 'edit_user', $args[2] ) && ( $args[2] !== $user->ID ) ); 1257 | } elseif ( 'switch_off' === $args[0] ) { 1258 | if ( array_key_exists( 'switch_users', $user_caps ) ) { 1259 | $user_caps['switch_off'] = $user_caps['switch_users']; 1260 | return $user_caps; 1261 | } 1262 | 1263 | $user_caps['switch_off'] = user_can( $user->ID, 'edit_users' ); 1264 | } 1265 | 1266 | return $user_caps; 1267 | } 1268 | 1269 | /** 1270 | * Filters the required primitive capabilities for the given primitive or meta capability. 1271 | * 1272 | * This is used to: 1273 | * 1274 | * - Add the 'do_not_allow' capability to the list of required capabilities when a Super Admin is trying to switch 1275 | * to themselves. 1276 | * 1277 | * It affects nothing else as Super Admins can do everything by default. 1278 | * 1279 | * @param array $required_caps Array of required primitive capabilities for the requested capability. 1280 | * @param string $cap Capability or meta capability being checked. 1281 | * @param int $user_id Concerned user ID. 1282 | * @param array $args { 1283 | * Arguments that accompany the requested capability check. 1284 | * 1285 | * @type mixed ...$0 Optional second and further parameters. 1286 | * } 1287 | * @return array Array of required capabilities for the requested action. 1288 | */ 1289 | public function filter_map_meta_cap( array $required_caps, $cap, $user_id, array $args ): array { 1290 | if ( 'switch_to_user' === $cap ) { 1291 | if ( empty( $args[0] ) || $args[0] === $user_id ) { 1292 | $required_caps[] = 'do_not_allow'; 1293 | } 1294 | } 1295 | return $required_caps; 1296 | } 1297 | 1298 | /** 1299 | * Singleton instantiator. 1300 | * 1301 | * @return user_switching User Switching instance. 1302 | */ 1303 | public static function get_instance(): user_switching { 1304 | static $instance; 1305 | 1306 | if ( ! isset( $instance ) ) { 1307 | $instance = new user_switching(); 1308 | } 1309 | 1310 | return $instance; 1311 | } 1312 | 1313 | /** 1314 | * Private class constructor. Use `get_instance()` to get the instance. 1315 | */ 1316 | private function __construct() {} 1317 | } 1318 | 1319 | if ( ! function_exists( 'user_switching_set_olduser_cookie' ) ) { 1320 | /** 1321 | * Sets authorisation cookies containing the originating user information. 1322 | * 1323 | * @since 1.4.0 The `$token` parameter was added. 1324 | * 1325 | * @param int $old_user_id The ID of the originating user, usually the current logged in user. 1326 | * @param bool $pop Optional. Pop the latest user off the auth cookie, instead of appending the new one. Default false. 1327 | * @param string $token Optional. The old user's session token to store for later reuse. Default empty string. 1328 | */ 1329 | function user_switching_set_olduser_cookie( $old_user_id, bool $pop = false, string $token = '' ): void { 1330 | $secure_auth_cookie = user_switching::secure_auth_cookie(); 1331 | $secure_olduser_cookie = user_switching::secure_olduser_cookie(); 1332 | $expiration = time() + 172800; // 48 hours 1333 | $auth_cookie = user_switching_get_auth_cookie(); 1334 | $olduser_cookie = wp_generate_auth_cookie( $old_user_id, $expiration, 'logged_in', $token ); 1335 | 1336 | if ( $secure_auth_cookie ) { 1337 | $auth_cookie_name = USER_SWITCHING_SECURE_COOKIE; 1338 | $scheme = 'secure_auth'; 1339 | } else { 1340 | $auth_cookie_name = USER_SWITCHING_COOKIE; 1341 | $scheme = 'auth'; 1342 | } 1343 | 1344 | if ( $pop ) { 1345 | array_pop( $auth_cookie ); 1346 | } else { 1347 | array_push( $auth_cookie, wp_generate_auth_cookie( $old_user_id, $expiration, $scheme, $token ) ); 1348 | } 1349 | 1350 | $auth_cookie = wp_json_encode( $auth_cookie ); 1351 | 1352 | if ( false === $auth_cookie ) { 1353 | return; 1354 | } 1355 | 1356 | /** 1357 | * Fires immediately before the User Switching authentication cookie is set. 1358 | * 1359 | * @since 1.4.0 1360 | * 1361 | * @param string $auth_cookie JSON-encoded array of authentication cookie values. 1362 | * @param int $expiration The time when the authentication cookie expires as a UNIX timestamp. 1363 | * @param int $old_user_id User ID. 1364 | * @param string $scheme Authentication scheme. Values include 'auth' or 'secure_auth'. 1365 | * @param string $token User's session token to use for the latest cookie. 1366 | */ 1367 | do_action( 'set_user_switching_cookie', $auth_cookie, $expiration, $old_user_id, $scheme, $token ); 1368 | 1369 | $scheme = 'logged_in'; 1370 | 1371 | /** 1372 | * Fires immediately before the User Switching old user cookie is set. 1373 | * 1374 | * @since 1.4.0 1375 | * 1376 | * @param string $olduser_cookie The old user cookie value. 1377 | * @param int $expiration The time when the logged-in authentication cookie expires as a UNIX timestamp. 1378 | * @param int $old_user_id User ID. 1379 | * @param string $scheme Authentication scheme. Values include 'auth' or 'secure_auth'. 1380 | * @param string $token User's session token to use for this cookie. 1381 | */ 1382 | do_action( 'set_olduser_cookie', $olduser_cookie, $expiration, $old_user_id, $scheme, $token ); 1383 | 1384 | /** 1385 | * Allows preventing auth cookies from actually being sent to the client. 1386 | * 1387 | * @since 1.5.4 1388 | * 1389 | * @param bool $send Whether to send auth cookies to the client. 1390 | */ 1391 | if ( ! apply_filters( 'user_switching_send_auth_cookies', true ) ) { 1392 | return; 1393 | } 1394 | 1395 | setcookie( $auth_cookie_name, $auth_cookie, $expiration, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_auth_cookie, true ); 1396 | setcookie( USER_SWITCHING_OLDUSER_COOKIE, $olduser_cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure_olduser_cookie, true ); 1397 | } 1398 | } 1399 | 1400 | if ( ! function_exists( 'user_switching_clear_olduser_cookie' ) ) { 1401 | /** 1402 | * Clears the cookies containing the originating user, or pops the latest item off the end if there's more than one. 1403 | * 1404 | * @param bool $clear_all Optional. Whether to clear the cookies (as opposed to just popping the last user off the end). Default true. 1405 | */ 1406 | function user_switching_clear_olduser_cookie( bool $clear_all = true ): void { 1407 | $auth_cookie = user_switching_get_auth_cookie(); 1408 | if ( ! empty( $auth_cookie ) ) { 1409 | array_pop( $auth_cookie ); 1410 | } 1411 | if ( $clear_all || empty( $auth_cookie ) ) { 1412 | /** 1413 | * Fires just before the user switching cookies are cleared. 1414 | * 1415 | * @since 1.4.0 1416 | */ 1417 | do_action( 'clear_olduser_cookie' ); 1418 | 1419 | /** This filter is documented in user-switching.php */ 1420 | if ( ! apply_filters( 'user_switching_send_auth_cookies', true ) ) { 1421 | return; 1422 | } 1423 | 1424 | $expire = time() - 31536000; 1425 | setcookie( USER_SWITCHING_COOKIE, ' ', $expire, SITECOOKIEPATH, COOKIE_DOMAIN ); 1426 | setcookie( USER_SWITCHING_SECURE_COOKIE, ' ', $expire, SITECOOKIEPATH, COOKIE_DOMAIN ); 1427 | setcookie( USER_SWITCHING_OLDUSER_COOKIE, ' ', $expire, COOKIEPATH, COOKIE_DOMAIN ); 1428 | } else { 1429 | if ( user_switching::secure_auth_cookie() ) { 1430 | $scheme = 'secure_auth'; 1431 | } else { 1432 | $scheme = 'auth'; 1433 | } 1434 | 1435 | $old_cookie = end( $auth_cookie ); 1436 | 1437 | $old_user_id = wp_validate_auth_cookie( $old_cookie, $scheme ); 1438 | if ( $old_user_id ) { 1439 | $parts = wp_parse_auth_cookie( $old_cookie, $scheme ); 1440 | 1441 | if ( false !== $parts ) { 1442 | user_switching_set_olduser_cookie( $old_user_id, true, $parts['token'] ); 1443 | } 1444 | } 1445 | } 1446 | } 1447 | } 1448 | 1449 | if ( ! function_exists( 'user_switching_get_olduser_cookie' ) ) { 1450 | /** 1451 | * Gets the value of the cookie containing the originating user. 1452 | * 1453 | * @return string|false The old user cookie, or boolean false if there isn't one. 1454 | */ 1455 | function user_switching_get_olduser_cookie() { 1456 | if ( isset( $_COOKIE[ USER_SWITCHING_OLDUSER_COOKIE ] ) ) { 1457 | return wp_unslash( (string) $_COOKIE[ USER_SWITCHING_OLDUSER_COOKIE ] ); 1458 | } 1459 | 1460 | return false; 1461 | } 1462 | } 1463 | 1464 | if ( ! function_exists( 'user_switching_get_auth_cookie' ) ) { 1465 | /** 1466 | * Gets the value of the auth cookie containing the list of originating users. 1467 | * 1468 | * @return list Array of originating user authentication cookie values. Empty array if there are none. 1469 | */ 1470 | function user_switching_get_auth_cookie(): array { 1471 | if ( user_switching::secure_auth_cookie() ) { 1472 | $auth_cookie_name = USER_SWITCHING_SECURE_COOKIE; 1473 | } else { 1474 | $auth_cookie_name = USER_SWITCHING_COOKIE; 1475 | } 1476 | 1477 | if ( isset( $_COOKIE[ $auth_cookie_name ] ) && is_string( $_COOKIE[ $auth_cookie_name ] ) ) { 1478 | $cookie = json_decode( wp_unslash( $_COOKIE[ $auth_cookie_name ] ) ); 1479 | } 1480 | if ( ! isset( $cookie ) || ! is_array( $cookie ) ) { 1481 | $cookie = []; 1482 | } 1483 | 1484 | return array_values( array_filter( $cookie, 'is_string' ) ); 1485 | } 1486 | } 1487 | 1488 | if ( ! function_exists( 'switch_to_user' ) ) { 1489 | /** 1490 | * Switches the current logged in user to the specified user. 1491 | * 1492 | * @param int $user_id The ID of the user to switch to. 1493 | * @param bool $remember Optional. Whether to 'remember' the user in the form of a persistent browser cookie. Default false. 1494 | * @param bool $set_old_user Optional. Whether to set the old user cookie. Default true. 1495 | * @return false|WP_User WP_User object on success, false on failure. 1496 | */ 1497 | function switch_to_user( $user_id, bool $remember = false, bool $set_old_user = true ) { 1498 | $user = get_userdata( $user_id ); 1499 | 1500 | if ( ! $user ) { 1501 | return false; 1502 | } 1503 | 1504 | $old_user_id = ( is_user_logged_in() ) ? get_current_user_id() : false; 1505 | $old_token = wp_get_session_token(); 1506 | $auth_cookies = user_switching_get_auth_cookie(); 1507 | $auth_cookie = end( $auth_cookies ); 1508 | $cookie_parts = $auth_cookie ? wp_parse_auth_cookie( $auth_cookie ) : false; 1509 | 1510 | if ( $set_old_user && $old_user_id ) { 1511 | // Switching to another user 1512 | $new_token = ''; 1513 | user_switching_set_olduser_cookie( $old_user_id, false, $old_token ); 1514 | } else { 1515 | // Switching back, either after being switched off or after being switched to another user 1516 | $new_token = $cookie_parts['token'] ?? ''; 1517 | user_switching_clear_olduser_cookie( false ); 1518 | } 1519 | 1520 | /** 1521 | * Attaches the original user ID and session token to the new session when a user switches to another user. 1522 | * 1523 | * @param array $session Array of extra data. 1524 | * @return array Array of extra data. 1525 | */ 1526 | $session_filter = static function ( array $session ) use ( $old_user_id, $old_token ): array { 1527 | $session['switched_from_id'] = $old_user_id; 1528 | $session['switched_from_session'] = $old_token; 1529 | return $session; 1530 | }; 1531 | 1532 | add_filter( 'attach_session_information', $session_filter, 99 ); 1533 | 1534 | wp_clear_auth_cookie(); 1535 | wp_set_auth_cookie( $user_id, $remember, '', $new_token ); 1536 | wp_set_current_user( $user_id ); 1537 | 1538 | remove_filter( 'attach_session_information', $session_filter, 99 ); 1539 | 1540 | if ( $set_old_user && $old_user_id ) { 1541 | /** 1542 | * Fires when a user switches to another user account. 1543 | * 1544 | * @since 0.6.0 1545 | * @since 1.4.0 The `$new_token` and `$old_token` parameters were added. 1546 | * 1547 | * @param int $user_id The ID of the user being switched to. 1548 | * @param int $old_user_id The ID of the user being switched from. 1549 | * @param string $new_token The token of the session of the user being switched to. Can be an empty string 1550 | * or a token for a session that may or may not still be valid. 1551 | * @param string $old_token The token of the session of the user being switched from. 1552 | */ 1553 | do_action( 'switch_to_user', $user_id, $old_user_id, $new_token, $old_token ); 1554 | } else { 1555 | /** 1556 | * Fires when a user switches back to their originating account. 1557 | * 1558 | * @since 0.6.0 1559 | * @since 1.4.0 The `$new_token` and `$old_token` parameters were added. 1560 | * 1561 | * @param int $user_id The ID of the user being switched back to. 1562 | * @param int|false $old_user_id The ID of the user being switched from, or false if the user is switching back 1563 | * after having been switched off. 1564 | * @param string $new_token The token of the session of the user being switched to. Can be an empty string 1565 | * or a token for a session that may or may not still be valid. 1566 | * @param string $old_token The token of the session of the user being switched from. 1567 | */ 1568 | do_action( 'switch_back_user', $user_id, $old_user_id, $new_token, $old_token ); 1569 | } 1570 | 1571 | if ( $old_token && $old_user_id && ! $set_old_user ) { 1572 | // When switching back, destroy the session for the old user 1573 | $manager = WP_Session_Tokens::get_instance( $old_user_id ); 1574 | $manager->destroy( $old_token ); 1575 | } 1576 | 1577 | return $user; 1578 | } 1579 | } 1580 | 1581 | if ( ! function_exists( 'switch_off_user' ) ) { 1582 | /** 1583 | * Switches off the current logged in user. This logs the current user out while retaining a cookie allowing them to log 1584 | * straight back in using the 'Switch back to {user}' system. 1585 | * 1586 | * @return bool True on success, false on failure. 1587 | */ 1588 | function switch_off_user(): bool { 1589 | $old_user_id = get_current_user_id(); 1590 | 1591 | if ( ! $old_user_id ) { 1592 | return false; 1593 | } 1594 | 1595 | $old_token = wp_get_session_token(); 1596 | 1597 | user_switching_set_olduser_cookie( $old_user_id, false, $old_token ); 1598 | wp_clear_auth_cookie(); 1599 | wp_set_current_user( 0 ); 1600 | 1601 | /** 1602 | * Fires when a user switches off. 1603 | * 1604 | * @since 0.6.0 1605 | * @since 1.4.0 The `$old_token` parameter was added. 1606 | * 1607 | * @param int $old_user_id The ID of the user switching off. 1608 | * @param string $old_token The token of the session of the user switching off. 1609 | */ 1610 | do_action( 'switch_off_user', $old_user_id, $old_token ); 1611 | 1612 | return true; 1613 | } 1614 | } 1615 | 1616 | if ( ! function_exists( 'current_user_switched' ) ) { 1617 | /** 1618 | * Returns whether the current user switched into their account. 1619 | * 1620 | * @return false|WP_User False if the user isn't logged in or they didn't switch in; old user object (which evaluates to 1621 | * true) if the user switched into the current user account. 1622 | */ 1623 | function current_user_switched() { 1624 | if ( ! is_user_logged_in() ) { 1625 | return false; 1626 | } 1627 | 1628 | return user_switching::get_old_user(); 1629 | } 1630 | } 1631 | 1632 | $GLOBALS['user_switching'] = user_switching::get_instance(); 1633 | $GLOBALS['user_switching']->init_hooks(); 1634 | --------------------------------------------------------------------------------