├── 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 |
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 '';
917 | printf(
918 | '- %s
',
919 | esc_url( $link ),
920 | esc_html__( 'Switch To', 'user-switching' )
921 | );
922 | 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 |
--------------------------------------------------------------------------------