From 153ba4e1eb5cb1497c1d3b326cf0f19245bf36fc Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 4 Aug 2013 19:44:26 +0100 Subject: [PATCH] Nested switching and switching back is now supported (capability permitting). Fix for BuddyPress 1.7 member profile pages. Props nat0n. Updated Arabic translation by Hassan Hisham. A little code refactoring and improving, completed inline docs. --- languages/user_switching-ar.mo | Bin 838 -> 838 bytes readme.txt | 22 ++- user-switching.php | 292 ++++++++++++++++++++------------- 3 files changed, 190 insertions(+), 124 deletions(-) diff --git a/languages/user_switching-ar.mo b/languages/user_switching-ar.mo index ea3eab8bbf12e45a3c59290014b2831bc652bb83..a62afffbbb0514bdb0db5a0470c00aaa05ea50bc 100644 GIT binary patch delta 32 qcmV+*0N?+{2F3=kaRLdY*tgk%*r(W`lX?O#0;T|y3Q0rT<=p8x;= diff --git a/readme.txt b/readme.txt index c87c2a3..bbdd90b 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: johnbillion Donate link: http://lud.icro.us/donations/ Tags: user, users, profiles, switching, wpmu, multisite, buddypress, become, user control, user management, user access, developer Requires at least: 3.1 -Tested up to: 3.5 +Tested up to: 3.6 Stable tag: trunk License: GPL v2 or later @@ -23,10 +23,11 @@ This plugin allows you to quickly swap between user accounts in WordPress at the = Security = - * Only users with the ability to edit other users can switch user accounts (by default this is only Administrators). Lower level users cannot switch accounts. - * User switching is protected with the WordPress nonce security system, meaning only those who are allowed to switch users can switch. + * Only users with the ability to edit other users can switch user accounts (by default this is only Administrators on single site installs, and Super Admins on Multisite installs). Lower level users cannot switch accounts. + * User switching is protected with WordPress' nonce security system, meaning only those who intend to switch users can switch. * Full support for administration over SSL (if applicable). * Passwords are not (and cannot be) revealed. + * Originating user information is stored in the same secure manner as authentication cookies. = Translations Included = @@ -69,7 +70,7 @@ Yes, and you'll also be able to switch users from the Users screen in Network Ad = Does this plugin work with BuddyPress? = -Yes, and you'll also be able to switch users from the Members screens. +Yes, and you'll also be able to switch users from member profile screens and the member listing screen. = Does this work as a mu-plugin? = @@ -98,14 +99,17 @@ When a user switches off, the `switch_off_user` hook is called with the old user == Upgrade Notice == -= 0.7.1 = -* Arabic translation by Hassan Hisham. Minor code tweaks. - -= 0.7 = -* More intuitive redirecting after switching. Always show a 'Switch back' link in the footer when the admin toolbar isn't showing. += 0.8 = +* Nested switching and switching back is now supported (capability permitting). Switch, switch again, switch back, switch back! == Changelog == += 0.8 = +* Nested switching and switching back is now supported (capability permitting). Switch, switch again, switch back, switch back! +* Fix for BuddyPress 1.7 member profile pages. Props nat0n. +* Updated Arabic translation by Hassan Hisham. +* A little code refactoring and improving, completed inline docs. + = 0.7.1 = * Arabic translation by Hassan Hisham. * Minor code tweaks. diff --git a/user-switching.php b/user-switching.php index 8fcc213..6cfb169 100644 --- a/user-switching.php +++ b/user-switching.php @@ -2,7 +2,7 @@ /* Plugin Name: User Switching Description: Instant switching between user accounts in WordPress -Version: 0.7.1 +Version: 0.8 Plugin URI: http://lud.icro.us/wordpress-plugin-user-switching/ Author: John Blackbourn Author URI: http://johnblackbourn.com/ @@ -31,37 +31,38 @@ class user_switching { * * @return null */ - function __construct() { + public function __construct() { # Required functionality: - add_filter( 'user_has_cap', array( $this, 'user_cap_filter' ), 10, 3 ); - add_filter( 'map_meta_cap', array( $this, 'map_meta_cap' ), 10, 4 ); - add_filter( 'user_row_actions', array( $this, 'user_row' ), 10, 2 ); - add_action( 'plugins_loaded', array( $this, 'set_old_cookie' ) ); - add_action( 'init', array( $this, 'init' ) ); - add_action( 'admin_notices', array( $this, 'admin_notice' ), 1 ); + add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 10, 3 ); + add_filter( 'map_meta_cap', array( $this, 'filter_map_meta_cap' ), 10, 4 ); + add_filter( 'user_row_actions', array( $this, 'filter_user_row_actions' ), 10, 2 ); + add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) ); + add_action( 'init', array( $this, 'action_init' ) ); + add_action( 'admin_notices', array( $this, 'action_admin_notices' ), 1 ); add_action( 'wp_logout', 'wp_clear_olduser_cookie' ); add_action( 'wp_login', 'wp_clear_olduser_cookie' ); # Nice-to-haves: - add_filter( 'ms_user_row_actions', array( $this, 'user_row' ), 10, 2 ); - add_action( 'wp_footer', array( $this, 'switch_on' ) ); - add_action( 'personal_options', array( $this, 'personal_options' ) ); - add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 11 ); - add_action( 'bp_adminbar_menus', array( $this, 'bp_menu' ), 9 ); - add_action( 'bp_member_header_actions', array( $this, 'bp_button' ), 11 ); - add_action( 'network_admin_notices', array( $this, 'admin_notice' ), 1 ); - add_action( 'login_message', array( $this, 'login_message' ), 1 ); - add_action( 'bp_directory_members_actions', array( $this, 'bp_button' ), 11 ); + add_filter( 'ms_user_row_actions', array( $this, 'filter_user_row_actions' ), 10, 2 ); + add_action( 'wp_footer', array( $this, 'action_wp_footer' ) ); + add_action( 'personal_options', array( $this, 'action_personal_options' ) ); + add_action( 'admin_bar_menu', array( $this, 'action_admin_bar_menu' ), 11 ); + add_action( 'bp_adminbar_menus', array( $this, 'action_bp_menus' ), 9 ); + add_action( 'bp_member_header_actions', array( $this, 'action_bp_button' ), 11 ); + add_action( 'network_admin_notices', array( $this, 'action_admin_notices' ), 1 ); + add_filter( 'login_message', array( $this, 'filter_login_message' ), 1 ); + add_action( 'bp_directory_members_actions', array( $this, 'action_bp_button' ), 11 ); } /** * Define the name of the old user cookie. Uses WordPress' cookie hash for increased security. * + * @action plugins_loaded * @return null */ - function set_old_cookie() { + public function action_plugins_loaded() { if ( !defined( 'OLDUSER_COOKIE' ) ) define( 'OLDUSER_COOKIE', 'wordpress_olduser_' . COOKIEHASH ); } @@ -69,16 +70,19 @@ function set_old_cookie() { /** * Output the 'Switch To' link on the user editing screen if we have permission to switch to this user. * - * @param object $user User object for this screen + * @action personal_options + * @param WP_User $user User object for this screen * @return null */ - function personal_options( $user ) { - if ( !current_user_can( 'switch_to_user', $user->ID ) ) + public function action_personal_options( WP_User $user ) { + + if ( ! $link = self::maybe_switch_url( $user->ID ) ) return; + ?> - + remember() ) ) { + if ( switch_to_user( $user_id, self::remember() ) ) { # Redirect to the dashboard or the home URL depending on capabilities: if ( $redirect_to ) @@ -149,15 +154,15 @@ function init() { check_admin_referer( 'switch_to_olduser' ); # Fetch the originating user data: - if ( !$old_user = $this->get_old_user() ) + if ( !$old_user = self::get_old_user() ) wp_die( __( 'Could not switch users.', 'user_switching' ) ); # Switch user: - if ( switch_to_user( $old_user->ID, $this->remember(), false ) ) { + if ( switch_to_user( $old_user->ID, self::remember(), false ) ) { if ( $redirect_to ) wp_safe_redirect( add_query_arg( array( 'user_switched' => 'true', 'switched_back' => 'true' ), $redirect_to ) ); else - wp_redirect( add_query_arg( array( 'user_switched' => 'true', 'switched_back' => 'true' ), admin_url('users.php') ) ); + wp_redirect( add_query_arg( array( 'user_switched' => 'true', 'switched_back' => 'true' ), admin_url( 'users.php' ) ) ); die(); } else { wp_die( __( 'Could not switch users.', 'user_switching' ) ); @@ -188,21 +193,22 @@ function init() { /** * Display the 'Switched to {user}' and 'Switch back to {user}' messages in the admin area. * + * @action admin_notices * @return null */ - function admin_notice() { - global $user_identity, $user_login; + public function action_admin_notices() { + $user = wp_get_current_user(); - if ( $old_user = $this->get_old_user() ) { + if ( $old_user = self::get_old_user() ) { ?>

display_name, $user->user_login ); $url = add_query_arg( array( - 'redirect_to' => urlencode( $this->current_url() ) - ), $this->switch_back_url() ); + 'redirect_to' => urlencode( self::current_url() ) + ), self::switch_back_url() ); printf( ' %s.', $url, sprintf( __( 'Switch back to %1$s (%2$s)', 'user_switching' ), $old_user->display_name, $old_user->user_login ) ); ?>

@@ -214,9 +220,9 @@ function admin_notice() {

display_name, $user->user_login ); else - printf( __( 'Switched to %1$s (%2$s).', 'user_switching' ), $user_identity, $user_login ); + printf( __( 'Switched to %1$s (%2$s).', 'user_switching' ), $user->display_name, $user->user_login ); ?>

get_old_user() ) { + if ( $old_user = self::get_old_user() ) { $wp_admin_bar->add_menu( array( 'parent' => $parent, 'id' => 'wp-admin-bar-switch-back', 'title' => sprintf( __( 'Switch back to %1$s (%2$s)', 'user_switching' ), $old_user->display_name, $old_user->user_login ), 'href' => add_query_arg( array( - 'redirect_to' => urlencode( $this->current_url() ) - ), $this->switch_back_url() ) + 'redirect_to' => urlencode( self::current_url() ) + ), self::switch_back_url() ) ) ); } if ( current_user_can( 'switch_off' ) ) { - $url = $this->switch_off_url(); + $url = self::switch_off_url(); if ( !is_admin() ) { $url = add_query_arg( array( - 'redirect_to' => urlencode( $this->current_url() ) + 'redirect_to' => urlencode( self::current_url() ) ), $url ); } @@ -292,15 +301,16 @@ function admin_bar_menu( $wp_admin_bar ) { /** * Adds a 'Switch back to {user}' link to the WordPress footer if the admin toolbar isn't showing. * + * @action wp_footer * @return null */ - function switch_on() { + public function action_wp_footer() { - if ( !is_admin_bar_showing() and $old_user = $this->get_old_user() ) { + if ( !is_admin_bar_showing() and $old_user = self::get_old_user() ) { $link = sprintf( __( 'Switch back to %1$s (%2$s)', 'user_switching' ), $old_user->display_name, $old_user->user_login ); $url = add_query_arg( array( - 'redirect_to' => urlencode( $this->current_url() ) - ), $this->switch_back_url() ); + 'redirect_to' => urlencode( self::current_url() ) + ), self::switch_back_url() ); echo '

' . $link . '

'; } @@ -309,13 +319,15 @@ function switch_on() { /** * Adds a 'Switch back to {user}' link to the WordPress login screen. * - * @return null + * @filter login_message + * @param string $message The login screen message + * @return string The login screen message */ - function login_message( $message ) { + public function filter_login_message( $message ) { - if ( $old_user = $this->get_old_user() ) { + if ( $old_user = self::get_old_user() ) { $link = sprintf( __( 'Switch back to %1$s (%2$s)', 'user_switching' ), $old_user->display_name, $old_user->user_login ); - $url = $this->switch_back_url(); + $url = self::switch_back_url(); if ( isset( $_REQUEST['redirect_to'] ) and !empty( $_REQUEST['redirect_to'] ) ) { $url = add_query_arg( array( 'redirect_to' => $_REQUEST['redirect_to'] @@ -331,24 +343,33 @@ function login_message( $message ) { /** * Adds a 'Switch To' link to each list of user actions on the Users screen. * - * @return null + * @filter user_row_actions + * @filter ms_user_row_actions + * @param array $actions The actions to display for this user row + * @param WP_User $user The user object displayed in this row + * @return array The actions to display for this user row */ - function user_row( $actions, $user ) { - if ( current_user_can( 'switch_to_user', $user->ID ) ) - $actions['switch_to_user'] = '' . __( 'Switch To', 'user_switching' ) . ''; + public function filter_user_row_actions( array $actions, WP_User $user ) { + + if ( ! $link = self::maybe_switch_url( $user->ID ) ) + return $actions; + + $actions['switch_to_user'] = '' . __( 'Switch To', 'user_switching' ) . ''; + return $actions; } /** * Adds a 'Switch back to {user}' link to the BuddyPress admin bar. * + * @action bp_adminbar_menus * @return null */ - function bp_menu() { + public function action_bp_menus() { - if ( !is_admin() and $old_user = $this->get_old_user() ) { + if ( !is_admin() and $old_user = self::get_old_user() ) { - echo '
  • '; + echo '
  • '; printf( __( 'Switch back to %1$s (%2$s)', 'user_switching' ), $old_user->display_name, $old_user->user_login ); echo '
  • '; @@ -359,43 +380,64 @@ function bp_menu() { /** * Adds a 'Switch To' link to each member's profile page and profile listings in BuddyPress. * + * @action bp_member_header_actions + * @action bp_directory_members_actions * @return null */ - function bp_button() { + public function action_bp_button() { global $bp, $members_template; - if ( !empty( $members_template ) ) + if ( !empty( $members_template ) and empty( $bp->displayed_user->id ) ) $id = absint( $members_template->member->id ); else $id = absint( $bp->displayed_user->id ); - if ( current_user_can( 'switch_to_user', $id ) ) { + if ( ! $link = self::maybe_switch_url( $id ) ) + return; + + # Workaround for https://buddypress.trac.wordpress.org/ticket/4212 + $components = array_keys( $bp->active_components ); + if ( !empty( $components ) ) + $component = reset( $components ); + else + $component = 'core'; - # Workaround for https://buddypress.trac.wordpress.org/ticket/4212 - $components = array_keys( $bp->active_components ); - if ( !empty( $components ) ) - $component = reset( $components ); - else - $component = 'core'; + echo bp_get_button( array( + 'id' => 'user_switching', + 'component' => $component, + 'link_href' => $link, + 'link_text' => __( 'Switch To', 'user_switching' ) + ) ); - echo bp_get_button( array( - 'id' => 'user_switching', - 'component' => $component, - 'link_href' => $this->switch_to_url( $id ), - 'link_text' => __( 'Switch To', 'user_switching' ) - ) ); - } + } + + /** + * Helper function. Returns the switch to or switch back URL for a given user ID. + * + * @param int $user_id The user ID to be switched to. + * @return string|bool The required URL, or false if there's no old user or the user doesn't have the required capability. + */ + public static function maybe_switch_url( $user_id ) { + + $old_user = self::get_old_user(); + + if ( $old_user and ( $old_user->ID == $user_id ) ) + return self::switch_back_url(); + else if ( current_user_can( 'switch_to_user', $user_id ) ) + return self::switch_to_url( $user_id ); + else + return false; } /** - * Returns the nonce-secured URL needed to switch to a given user ID. + * Helper function. Returns the nonce-secured URL needed to switch to a given user ID. * * @param int $user_id The user ID to be switched to. * @return string The required URL */ - function switch_to_url( $user_id ) { + public static function switch_to_url( $user_id ) { return wp_nonce_url( add_query_arg( array( 'action' => 'switch_to_user', 'user_id' => $user_id @@ -403,33 +445,33 @@ function switch_to_url( $user_id ) { } /** - * Returns the nonce-secured URL needed to switch back to the originating user. + * Helper function. Returns the nonce-secured URL needed to switch back to the originating user. * * @return string The required URL */ - function switch_back_url() { + public static function switch_back_url() { return wp_nonce_url( add_query_arg( array( 'action' => 'switch_to_olduser' ), wp_login_url() ), 'switch_to_olduser' ); } /** - * Returns the nonce-secured URL needed to "switch off" the current user. + * Helper function. Returns the nonce-secured URL needed to switch off the current user. * * @return string The required URL */ - function switch_off_url() { + public static function switch_off_url() { return wp_nonce_url( add_query_arg( array( 'action' => 'switch_off' ), wp_login_url() ), 'switch_off' ); } /** - * Returns the current URL. + * Helper function. Returns the current URL. * * @return string The current URL */ - function current_url() { + public static function current_url() { return ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } @@ -440,21 +482,22 @@ function current_url() { * they're trying to switch to (and that user is not themselves), and to grant the 'switch_off' capability to * a user if they can edit users. * - * Important: This does not get called for Super Admins. See map_meta_cap() below. + * Important: This does not get called for Super Admins. See filter_map_meta_cap() below. * - * @param array $user_caps User's capabilities + * @filter user_has_cap + * @param array $user_caps User's capabilities * @param array $required_caps Actual required capabilities for the requested capability - * @param array $args Arguments that accompany the requested capability check: - * [0] => Requested capability from current_user_can() - * [1] => Current user ID - * [2] => Optional second parameter from current_user_can() + * @param array $args Arguments that accompany the requested capability check: + * [0] => Requested capability from current_user_can() + * [1] => Current user ID + * [2] => Optional second parameter from current_user_can() * @return array User's capabilities */ - function user_cap_filter( $user_caps, $required_caps, $args ) { + public function filter_user_has_cap( array $user_caps, array $required_caps, array $args ) { if ( 'switch_to_user' == $args[0] ) $user_caps['switch_to_user'] = ( user_can( $args[1], 'edit_user', $args[2] ) and ( $args[2] != $args[1] ) ); else if ( 'switch_off' == $args[0] ) - $user_caps['switch_off'] = ( user_can( $args[1], 'edit_users' ) and !$this->get_old_user() ); + $user_caps['switch_off'] = user_can( $args[1], 'edit_users' ); return $user_caps; } @@ -464,24 +507,23 @@ function user_cap_filter( $user_caps, $required_caps, $args ) { * This is used to add the 'do_not_allow' capability to the list of required capabilities when a super admin * is trying to switch to themselves. It affects nothing else as super admins can do everything by default. * - * @param array $required_caps Actual required capabilities for the requested action - * @param string $cap Capability or meta capability being checked - * @param string $user_id Current user ID - * @param array $args Arguments that accompany this capability check + * @filter map_meta_cap + * @param array $required_caps Actual required capabilities for the requested action + * @param string $cap Capability or meta capability being checked + * @param string $user_id Current user ID + * @param array $args Arguments that accompany this capability check * @return array Required capabilities for the requested action */ - function map_meta_cap( $required_caps, $cap, $user_id, $args ) { + public function filter_map_meta_cap( array $required_caps, $cap, $user_id, array $args ) { if ( ( 'switch_to_user' == $cap ) and ( $args[0] == $user_id ) ) $required_caps[] = 'do_not_allow'; - else if ( ( 'switch_off' == $cap ) and ( $this->get_old_user() ) ) - $required_caps[] = 'do_not_allow'; return $required_caps; } } /** - * Sets an authorisation cookie containing the user ID of the originating user. + * Sets an authorisation cookie containing the originating user, or appends it if there's more than one. * * @param int $old_user_id The ID of the originating user, usually the current logged in user. * @return null @@ -489,27 +531,51 @@ function map_meta_cap( $required_caps, $cap, $user_id, $args ) { if ( !function_exists( 'wp_set_olduser_cookie' ) ) { function wp_set_olduser_cookie( $old_user_id ) { $expiration = time() + 172800; # 48 hours - $cookie = wp_generate_auth_cookie( $old_user_id, $expiration, 'old_user' ); - setcookie( OLDUSER_COOKIE, $cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, false ); + $cookie = wp_get_olduser_cookie(); + $cookie[] = wp_generate_auth_cookie( $old_user_id, $expiration, 'old_user' ); + setcookie( OLDUSER_COOKIE, serialize( $cookie ), $expiration, COOKIEPATH, COOKIE_DOMAIN, false ); } } /** - * Clears the cookie containing the originating user ID. + * Clears the cookie containing the originating user, or pops the latest item off the end if there's more than one. * + * @param bool $clear_all Whether to clear the cookie or just pop the last user information off the end. * @return null */ if ( !function_exists( 'wp_clear_olduser_cookie' ) ) { -function wp_clear_olduser_cookie() { - setcookie( OLDUSER_COOKIE, ' ', time() - 31536000, COOKIEPATH, COOKIE_DOMAIN ); +function wp_clear_olduser_cookie( $clear_all = true ) { + $cookie = wp_get_olduser_cookie(); + array_pop( $cookie ); + if ( $clear_all or empty( $cookie ) ) { + setcookie( OLDUSER_COOKIE, ' ', time() - 31536000, COOKIEPATH, COOKIE_DOMAIN ); + } else { + $expiration = time() + 172800; # 48 hours + setcookie( OLDUSER_COOKIE, serialize( $cookie ), $expiration, COOKIEPATH, COOKIE_DOMAIN, false ); + } +} +} + +/** + * Gets the value of the cookie containing the list of originating users. + * + * @return array Array of originating user IDs. @ TODO correct doc, mention auth whatever + */ +if ( !function_exists( 'wp_get_olduser_cookie' ) ) { +function wp_get_olduser_cookie() { + if ( isset( $_COOKIE[OLDUSER_COOKIE] ) ) + $cookie = maybe_unserialize( stripslashes( $_COOKIE[OLDUSER_COOKIE] ) ); + if ( !isset( $cookie ) or !is_array( $cookie ) ) + $cookie = array(); + return $cookie; } } /** * Switches the current logged in user to the specified user. * - * @param int $user_id The ID of the user to switch to. - * @param bool $remember Whether to 'remember' the user in the form of a persistent browser cookie. Optional. + * @param int $user_id The ID of the user to switch to. + * @param bool $remember Whether to 'remember' the user in the form of a persistent browser cookie. Optional. * @param int|bool $old_user_id The ID of the originating user, or false to not set the old user cookie. Defaults to the current user. * @return bool True on success, false on failure. */ @@ -530,7 +596,7 @@ function switch_to_user( $user_id, $remember = false, $old_user_id = 0 ) { if ( $old_user_id ) wp_set_olduser_cookie( $old_user_id ); else - wp_clear_olduser_cookie(); + wp_clear_olduser_cookie( false ); wp_clear_auth_cookie(); wp_set_auth_cookie( $user_id, $remember ); @@ -574,14 +640,10 @@ function switch_off_user() { */ if ( !function_exists( 'current_user_switched' ) ) { function current_user_switched() { - - global $user_switching; - if ( !is_user_logged_in() ) return false; - return $user_switching->get_old_user(); - + return user_switching::get_old_user(); } }