├── .gitignore ├── .travis.yml ├── bin └── install-wp-tests.sh ├── log-in-as.css ├── log-in-as.js ├── log-in-as.php ├── phpcs.ruleset.xml ├── phpunit.xml.dist ├── readme.md ├── screenshot.png └── tests ├── bootstrap.php └── test-authentication.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | notifications: 6 | email: 7 | on_success: never 8 | on_failure: change 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | cache: 15 | - composer 16 | - $HOME/.composer/cache 17 | 18 | php: 19 | - 5.3 20 | - 5.6 21 | 22 | env: 23 | - WP_VERSION=latest WP_MULTISITE=0 24 | 25 | matrix: 26 | include: 27 | - php: 5.3 28 | env: WP_VERSION=latest WP_MULTISITE=1 29 | 30 | before_script: 31 | - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 32 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 33 | - | 34 | if [[ ${TRAVIS_PHP_VERSION:0:2} == "7." ]]; then 35 | composer global require "phpunit/phpunit=5.7.*" 36 | else 37 | composer global require "phpunit/phpunit=4.8.*" 38 | fi 39 | - | 40 | composer global require wp-coding-standards/wpcs 41 | phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs 42 | 43 | script: 44 | - phpcs --standard=phpcs.ruleset.xml $(find . -name '*.php') 45 | - phpunit 46 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 16 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 17 | 18 | download() { 19 | if [ `which curl` ]; then 20 | curl -s "$1" > "$2"; 21 | elif [ `which wget` ]; then 22 | wget -nv -O "$2" "$1" 23 | fi 24 | } 25 | 26 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 27 | WP_TESTS_TAG="tags/$WP_VERSION" 28 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 29 | WP_TESTS_TAG="trunk" 30 | else 31 | # http serves a single offer, whereas https serves multiple. we only want one 32 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 33 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 34 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 35 | if [[ -z "$LATEST_VERSION" ]]; then 36 | echo "Latest WordPress version could not be found" 37 | exit 1 38 | fi 39 | WP_TESTS_TAG="tags/$LATEST_VERSION" 40 | fi 41 | 42 | set -ex 43 | 44 | install_wp() { 45 | 46 | if [ -d $WP_CORE_DIR ]; then 47 | return; 48 | fi 49 | 50 | mkdir -p $WP_CORE_DIR 51 | 52 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 53 | mkdir -p /tmp/wordpress-nightly 54 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 55 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 56 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 57 | else 58 | if [ $WP_VERSION == 'latest' ]; then 59 | local ARCHIVE_NAME='latest' 60 | else 61 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 62 | fi 63 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 64 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 65 | fi 66 | 67 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 68 | } 69 | 70 | install_test_suite() { 71 | # portable in-place argument for both GNU sed and Mac OSX sed 72 | if [[ $(uname -s) == 'Darwin' ]]; then 73 | local ioption='-i .bak' 74 | else 75 | local ioption='-i' 76 | fi 77 | 78 | # set up testing suite if it doesn't yet exist 79 | if [ ! -d $WP_TESTS_DIR ]; then 80 | # set up testing suite 81 | mkdir -p $WP_TESTS_DIR 82 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 83 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 84 | fi 85 | 86 | if [ ! -f wp-tests-config.php ]; then 87 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 88 | # remove all forward slashes in the end 89 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 90 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 91 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 92 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 93 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 94 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 95 | fi 96 | 97 | } 98 | 99 | install_db() { 100 | 101 | if [ ${SKIP_DB_CREATE} = "true" ]; then 102 | return 0 103 | fi 104 | 105 | # parse DB_HOST for port or socket references 106 | local PARTS=(${DB_HOST//\:/ }) 107 | local DB_HOSTNAME=${PARTS[0]}; 108 | local DB_SOCK_OR_PORT=${PARTS[1]}; 109 | local EXTRA="" 110 | 111 | if ! [ -z $DB_HOSTNAME ] ; then 112 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 113 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 114 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 115 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 116 | elif ! [ -z $DB_HOSTNAME ] ; then 117 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 118 | fi 119 | fi 120 | 121 | # create database 122 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 123 | } 124 | 125 | install_wp 126 | install_test_suite 127 | install_db 128 | -------------------------------------------------------------------------------- /log-in-as.css: -------------------------------------------------------------------------------- 1 | #loginform > p, 2 | #loginform > div.user-pass-wrap { 3 | display: none; 4 | } 5 | 6 | #log-in-as { 7 | clear: both; 8 | } 9 | 10 | #log-in-as.pad { 11 | padding: 10px 0 0; 12 | } 13 | 14 | #log-in-as button.button { 15 | clear: both; 16 | float: right; 17 | margin: -5px -5px 0 0 !important; 18 | } 19 | 20 | #log-in-as h2 { 21 | margin: 0 0 20px; 22 | } 23 | 24 | #log-in-as h4 { 25 | padding: 5px; 26 | border-bottom: 2px solid grey; 27 | } 28 | #log-in-as h4 .dashicons { 29 | cursor: pointer; 30 | float: right; 31 | } 32 | 33 | #log-in-as .log-in-as-user { 34 | display: block; 35 | padding: 5px 10px; 36 | background: whitesmoke; 37 | margin: 0 0 3px; 38 | } 39 | #log-in-as .log-in-as-user:hover { 40 | background: gainsboro; 41 | } 42 | 43 | #log-in-as .log-in-as-group.hidden { 44 | display: none; 45 | } 46 | -------------------------------------------------------------------------------- /log-in-as.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | var wp = window.wp, 4 | $loginform = $('#loginform'); 5 | 6 | buttonClick = function( event ) { 7 | event.preventDefault(); 8 | 9 | var $logInAs = $('#log-in-as'); 10 | 11 | // move the 'remember me' checkbox and submit button elements 12 | // above our user selection div so they can toggle together 13 | $forgetmenot = $('p.forgetmenot').detach() 14 | $logInAs.before( $forgetmenot ); 15 | 16 | $submit = $('p.submit').detach() 17 | $logInAs.before( $submit ); 18 | 19 | // add class padding class for compat with standard login form 20 | $logInAs.toggleClass('pad'); 21 | 22 | // toggle the standard form 23 | $loginform.find('p,div.user-pass-wrap').slideToggle( function() { 24 | $loginform.find('div.user-pass-wrap input').removeAttr( 'disabled' ); 25 | }); 26 | } 27 | 28 | userClick = function( event ) { 29 | event.preventDefault(); 30 | 31 | var $a = $(this); 32 | 33 | // dim form so user know *something* is happening 34 | $('body').css('opacity', '0.3'); 35 | 36 | wp.ajax.send( 'log_in_as', { 37 | data: { 38 | user_id : $a.attr('data-user-id'), 39 | interim : $('input[name="interim-login"]').length 40 | }, 41 | success: userClickSuccess, 42 | error: userClickError 43 | } ); 44 | } 45 | 46 | userClick2 = function( event ) { 47 | event.preventDefault(); 48 | 49 | var $a = $(this); 50 | 51 | $('body').css('opacity', '0.3'); 52 | 53 | wp.ajax.send( 'log_out_and_in_as', { 54 | data: { 55 | user_id : $a.attr('data-user-id') 56 | }, 57 | success: userClickSuccess, 58 | error: userClickError 59 | } ); 60 | } 61 | 62 | userClick3 = function( event ) { 63 | event.preventDefault(); 64 | 65 | var $a = $(this); 66 | 67 | $('body').css('opacity', '0.3'); 68 | 69 | wp.ajax.send( 'switch_back', { 70 | data: { 71 | user_id : $a.attr('data-user-id') 72 | }, 73 | success: userClickSuccess, 74 | error: userClickError 75 | } ); 76 | } 77 | 78 | userClickSuccess = function( data ) { 79 | // all good, go to Dashboard 80 | window.location = data; 81 | } 82 | 83 | userClickError = function ( data ) { 84 | // restore opacity to indicate we tried 85 | $('body').css('opacity', '1'); 86 | 87 | // output error, creating container if needed 88 | $error = $('#login_error') 89 | if ( $error.length < 1 ) { 90 | $error_wrap = $('
'); 91 | $loginform.before( $error_wrap ); 92 | $error = $('#login_error'); 93 | } 94 | $error.html( data ); 95 | } 96 | 97 | roleHeadingClick = function() { 98 | $(this).next('.log-in-as-group').slideToggle('hidden'); 99 | } 100 | 101 | $('#log-in-as button.button').on('click', buttonClick ); 102 | $('#log-in-as h4').on('click', roleHeadingClick ); 103 | $('.log-in-as-user').on( 'click', userClick ); 104 | $('.log-out-and-in-as-user').on( 'click', userClick2 ); 105 | $('.switch-back').on( 'click', userClick3 ); 106 | 107 | })( jQuery ); 108 | -------------------------------------------------------------------------------- /log-in-as.php: -------------------------------------------------------------------------------- 1 | '; 82 | echo ''; 83 | 84 | echo '

' . esc_html__( 'Log in as…', 'log-in-as' ) . '

'; 85 | 86 | $hide = false; 87 | 88 | if ( is_multisite() ) { 89 | $links = array(); 90 | foreach ( get_super_admins() as $username ) { 91 | $user = get_user_by( 'login', $username ); 92 | $links[] = ''; 93 | } 94 | if ( ! empty( $links ) ) { 95 | echo '

'; 96 | printf( esc_html__( 'Super Admin %s', 'log-in-as' ), "" ); 97 | echo '

'; 98 | 99 | $class = $hide ? ' hidden' : ''; 100 | $hide = true; 101 | echo ''; 102 | } 103 | } 104 | 105 | if ( ! function_exists( 'get_editable_roles' ) ) { 106 | require_once( ABSPATH . 'wp-admin/includes/user.php' ); 107 | } 108 | 109 | // default args for users query. 110 | $get_user_args = apply_filters( 'log_in_as_user_args', array( 'number' => 2 ) ); 111 | 112 | foreach ( get_editable_roles() as $role => $details ) { 113 | $links = array(); 114 | 115 | // filter for per-role query args. 116 | $args = apply_filters( 'log_in_as_user_args_for_' . $role, $get_user_args ); 117 | // make sure we don't alter the role. 118 | $args = array_merge( $args, array( 'role' => $role ) ); 119 | 120 | foreach ( get_users( $args ) as $user ) { 121 | $links[] = ''; 122 | } 123 | if ( ! empty( $links ) ) { 124 | echo '

' . esc_html( $details['name'] ) . '

'; 125 | $class = $hide ? ' hidden' : ''; 126 | $hide = true; 127 | echo ''; 128 | } 129 | } 130 | 131 | echo '
'; 132 | 133 | } 134 | 135 | /** 136 | * Authenticate by looking up user by ID 137 | * 138 | * @param null|WP_User|WP_Error $user WP_User if the user is authenticated. 139 | * WP_Error or null otherwise. 140 | * @return WP_User|false User or Error 141 | */ 142 | function authenticate( $user ) { 143 | $uid = isset( $_REQUEST['user_id'] ) ? absint( $_REQUEST['user_id'] ) : 0; 144 | return get_user_by( 'id', $uid ); 145 | } 146 | 147 | /** 148 | * Ajax Callback for logged out users 149 | * Authenticate given user ID 150 | * 151 | * @return void 152 | */ 153 | function log_in_as() { 154 | sleep( 1 ); 155 | 156 | // attempt to sign in. 157 | $user = wp_signon(); 158 | 159 | // flush the buffer (it's fun to say). 160 | ob_end_clean(); 161 | 162 | // if successful, send Dashboard url to JS for redirecting. 163 | if ( ! is_wp_error( $user ) ) { 164 | $redirect = admin_url(); 165 | 166 | $interim = isset( $_POST['interim'] ) ? intval( $_POST['interim'] ) : 0; 167 | $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; 168 | 169 | if ( 1 === $interim ) { 170 | $redirect = add_query_arg( array( 171 | 'action' => 'login', 172 | 'user_id' => $user_id, 173 | 'customize-login' => '1', 174 | 'interim-login' => '1', 175 | ), wp_login_url() ); 176 | } 177 | wp_send_json_success( $redirect ); 178 | 179 | } else { // otherwise, send error message. 180 | wp_send_json_error( $user->get_error_message() ); 181 | } 182 | } 183 | 184 | /** 185 | * Ajax Callback for logged in users that are switching 186 | * Authenticate given user ID 187 | */ 188 | function log_out_and_in_as() { 189 | 190 | // save original user for switching back, yes, this can be faked. 191 | setcookie( 'switch', get_current_user_id(), time() + WEEK_IN_SECONDS ); 192 | 193 | wp_logout(); 194 | $this->log_in_as(); 195 | } 196 | 197 | /** 198 | * Ajax Callback for logged in users that are switching back 199 | * Authenticate given user ID 200 | */ 201 | function switch_back() { 202 | 203 | // remove cookie. 204 | setcookie( 'switch', '', time() - ( 86400 * 7 ) ); 205 | 206 | wp_logout(); 207 | $this->log_in_as(); 208 | } 209 | 210 | /** 211 | * Ajax Callback for logged in users 212 | * If logged in, tell them and give options 213 | */ 214 | function already_logged_in() { 215 | 216 | $actions = array( 217 | '' . esc_html__( 'Dashboard', 'log-in-as' ) . '', 218 | '' . esc_html__( 'Log out', 'log-in-as' ) . '', 219 | ); 220 | $action_links = implode( ' | ', $actions ); 221 | 222 | // clean it like you mean it. 223 | ob_end_clean(); 224 | 225 | wp_send_json_error( 226 | sprintf( __( 'You are already logged in as %s.', 'log-in-as' ), wp_get_current_user()->user_login ) . 227 | "

$action_links

" 228 | ); 229 | } 230 | 231 | /** 232 | * Display 'switch back' for switched users 233 | */ 234 | function admin_notices() { 235 | if ( ! isset( $_COOKIE['switch'] ) ) { 236 | return; 237 | } 238 | $user = absint( $_COOKIE['switch'] ); 239 | if ( ! ( $original_user = get_user_by( 'id', $user ) ) ) { 240 | return; 241 | } 242 | 243 | $text = sprintf( __( 'Switch back to %s', 'log-in-as' ), $original_user->user_login ); 244 | echo ''; 245 | } 246 | 247 | /** 248 | * Add switch links to users table 249 | * 250 | * @param array $actions Actions. 251 | * @param object $user User object. 252 | * @return array Actions 253 | */ 254 | function user_row_actions( $actions, $user ) { 255 | if ( get_current_user_id() === $user->ID ) { 256 | return $actions; 257 | } 258 | $text = esc_html__( 'Switch', 'log-in-as' ); 259 | $actions['switch'] = '' . esc_html( $text ) . ''; 260 | return $actions; 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /phpcs.ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | */node_modules/* 9 | */vendor/* 10 | 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Log In As 2 | 3 | ## NOT FOR USE IN PRODUCTION ENVIRONMENTS 4 | 5 | Log in as any user. Handy for local dev where databases come and go and you can never remember the dang credentials. 6 | 7 | ![screenshot](screenshot.png) 8 | 9 | Includes user-switching for logged-in users too. 10 | 11 | --- 12 | 13 | _Tested up to WordPress 5.5-alpha_ -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trepmal/log-in-as/b439930d1314569465b389dbb8abfaa29215a9b5/screenshot.png -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | authenticate( null ); 20 | $this->assertTrue( is_a( $user, 'WP_User' ) ); 21 | } 22 | 23 | /** 24 | * Test authentication failure 25 | */ 26 | function test_authentication_failure() { 27 | $Log_In_As = new Log_In_As; 28 | $_REQUEST['user_id'] = 999; 29 | $user = $Log_In_As->authenticate( null ); 30 | $this->assertFalse( $user ); 31 | } 32 | } 33 | --------------------------------------------------------------------------------