diff --git a/client/components/support-user/README.md b/client/components/support-user/README.md
new file mode 100644
index 00000000000000..c828150d3d33ac
--- /dev/null
+++ b/client/components/support-user/README.md
@@ -0,0 +1,3 @@
+Support User
+=============
+This component is used to provide user support.
diff --git a/client/components/support-user/index.jsx b/client/components/support-user/index.jsx
new file mode 100644
index 00000000000000..44ee44e4e55782
--- /dev/null
+++ b/client/components/support-user/index.jsx
@@ -0,0 +1,126 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import observe from 'lib/mixins/data-observe';
+import User from 'lib/user';
+import userSettings from 'lib/user-settings';
+import Dialog from 'components/dialog';
+import KeyboardShortcuts from 'lib/keyboard-shortcuts';
+
+module.exports = React.createClass( {
+ displayName: 'SupportUser',
+
+ mixins: [ observe( 'userSettings' ), React.addons.LinkedStateMixin ],
+
+ componentDidMount: function() {
+ KeyboardShortcuts.on( 'open-support-user', this.toggleShowDialog );
+ },
+
+ componentWillUnmount: function() {
+ KeyboardShortcuts.off( 'open-support-user', this.toggleShowDialog );
+ },
+
+ getInitialState: function() {
+ return {
+ supportUser: null,
+ isSupportUser: false,
+ showDialog: false
+ };
+ },
+
+ isEnabled: function() {
+ if ( ! userSettings.hasSettings() ) {
+ userSettings.fetchSettings();
+ return false;
+ }
+ return ! userSettings.getSetting( 'user_login_can_be_changed' );
+ },
+
+ toggleShowDialog: function() {
+ if ( this.isEnabled() ) {
+ this.setState( { showDialog: ! this.state.showDialog } );
+ }
+ },
+
+ closeDialog: function() {
+ },
+
+ onChangeUser: function( e ) {
+ e.preventDefault();
+
+ if ( this.state.supportUser && this.state.supportPassword ) {
+ let user = new User();
+ user.clear();
+ user.changeUser( this.state.supportUser, this.state.supportPassword );
+ this.setState( { isSupportUser: true } );
+ this.setState( { supportPassword: null } );
+ }
+
+ this.setState( { showDialog: false } );
+ },
+
+ onRestoreUser: function( e ) {
+ if ( this.state.isSupportUser && this.state.restoreUser ) {
+ let user = new User();
+ user.clear().fetch();
+ this.setState( {
+ supportUser: null,
+ supportPassword: null,
+ isSupportUser: false,
+ showDialog: false
+ } );
+ window.location.reload.bind( window.location );
+ }
+ },
+
+ render: function() {
+ if ( this.state.isSupportUser ) {
+ return (
+
+ );
+ } else {
+ return (
+
+ ); }
+ }
+} );
diff --git a/client/components/support-user/style.scss b/client/components/support-user/style.scss
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/client/layout/index.jsx b/client/layout/index.jsx
index 81b696f292f257..d78516baaec6fc 100644
--- a/client/layout/index.jsx
+++ b/client/layout/index.jsx
@@ -25,6 +25,7 @@ var Masterbar = require( './masterbar' ),
PulsingDot = require( 'components/pulsing-dot' ),
SitesListNotices = require( 'lib/sites-list/notices' ),
PollerPool = require( 'lib/data-poller' ),
+ SupportUser = require( 'components/support-user' ),
KeyboardShortcutsMenu;
if ( config.isEnabled( 'keyboard-shortcuts' ) ) {
@@ -102,6 +103,7 @@ module.exports = React.createClass( {
return (
{ config.isEnabled( 'keyboard-shortcuts' ) ?
: null }
+
diff --git a/client/lib/keyboard-shortcuts/key-bindings.js b/client/lib/keyboard-shortcuts/key-bindings.js
index 8c7cf73d63460e..412d4ac4492a5f 100644
--- a/client/lib/keyboard-shortcuts/key-bindings.js
+++ b/client/lib/keyboard-shortcuts/key-bindings.js
@@ -126,6 +126,14 @@ KeyBindings.prototype.get = function() {
keys: [ 'n' ],
text: i18n.translate( 'Open Notifications' )
}
+ },
+ {
+ eventName: 'open-support-user',
+ keys: [ 's', 'u' ],
+ description: {
+ keys: [],
+ text: '',
+ }
}
],
diff --git a/client/lib/user/user.js b/client/lib/user/user.js
index adde48fa7bf672..f2d6b9a0426513 100644
--- a/client/lib/user/user.js
+++ b/client/lib/user/user.js
@@ -238,6 +238,14 @@ User.prototype.set = function( attributes ) {
return changed;
};
+User.prototype.changeUser = function( username, password ) {
+ wpcom.changeUser( username, password, function( error ) {
+ if ( ! error ) {
+ this.fetch();
+ }
+ }.bind( this ) );
+};
+
/**
* Expose `User`
*/
diff --git a/shared/lib/wp/browser.js b/shared/lib/wp/browser.js
index b91ac9b0bf618d..e88f9ff55dfa72 100644
--- a/shared/lib/wp/browser.js
+++ b/shared/lib/wp/browser.js
@@ -9,6 +9,7 @@ const debug = debugFactory( 'calypso:wp' );
*/
import wpcomUndocumented from 'lib/wpcom-undocumented';
import config from 'config';
+import wpcomSupport from 'lib/wp/support';
let wpcom;
@@ -36,4 +37,4 @@ if ( config.isEnabled( 'oauth' ) ) {
/**
* Expose `wpcom`
*/
-module.exports = wpcom;
+module.exports = wpcomSupport( wpcom );
diff --git a/shared/lib/wp/support.js b/shared/lib/wp/support.js
new file mode 100644
index 00000000000000..2f375d048229c6
--- /dev/null
+++ b/shared/lib/wp/support.js
@@ -0,0 +1,102 @@
+export default function wpcomSupport( wpcom ) {
+ let supportUser = '';
+ let supportToken = '';
+
+ /**
+ * Request parameters are as follows:
+ * - param (required) A string or object that contains the path
+ * - query (optional) An object that expands the response object
+ * - body (required*) Required by POST and PUT, but not by GET and DEL
+ * - fn (required) A callback function to handle the returned result
+ * - ...rest (optional) Some queries have additional parameters after
+ * callback function
+ *
+ * Return the index of the query parameter, or if one does not exist,
+ * return false.
+ */
+ const getQueryIndex = function( req, args ) {
+ let fnIndex, queryIndex;
+
+ // Find the index of the callback in the arguments
+ fnIndex = args.findIndex( function( e ) {
+ return 'function' === typeof e;
+ } );
+
+ // Set queryIndex based on the request type and fnIndex
+ if ( req === 'post' || req === 'put' ) {
+ queryIndex = ( 3 === fnIndex ) ? 1 : false;
+ } else {
+ queryIndex = ( 2 === fnIndex ) ? 1 : false;
+ }
+ return queryIndex;
+ }
+
+ /**
+ * Add the supportUser and supportToken to the query.
+ */
+ const addSupportData = function( query ) {
+ return Object.assign( {}, query, {
+ support_user: supportUser,
+ _support_token: supportToken
+ } );
+ }
+
+ /**
+ * Mutate the query parameter of the request by adding values for
+ * support_user and _support_token to the query parameter.
+ */
+ const extendRequest = function( req, args ) {
+ if ( ! supportUser || ! supportToken ) {
+ return args;
+ }
+
+ let queryIndex = getQueryIndex( req, args );
+ if ( queryIndex ) {
+ args[ queryIndex ] = addSupportData( args[ queryIndex ] );
+ } else {
+ args.splice( 1, 0, addSupportData( {} ) );
+ }
+ return args;
+ }
+
+ const del = wpcom.req.del.bind( wpcom.req );
+ const get = wpcom.req.get.bind( wpcom.req );
+ const post = wpcom.req.post.bind( wpcom.req );
+ const put = wpcom.req.put.bind( wpcom.req );
+
+ return Object.assign( wpcom, {
+ changeUser: function( username, password, fn ) {
+ var args = {
+ apiVersion: '1.1',
+ path: '/internal/support/' + username + '/grant'
+ };
+
+ return wpcom.req.post( args, { password: password }, function( error, response ) {
+ if ( ! error ) {
+ supportUser = response.username;
+ supportToken = response.token;
+ }
+
+ fn( error, response );
+ } );
+ },
+ restoreUser: function() {
+ supportUser = '';
+ supportToken = '';
+ },
+ req: {
+ del: function( ...args ) {
+ return del.apply( wpcom, extendRequest( 'del', args ) );
+ },
+ get: function( ...args ) {
+ return get.apply( wpcom, extendRequest( 'get', args ) );
+ },
+ post: function( ...args ) {
+ return post.apply( wpcom, extendRequest( 'post', args ) );
+ },
+ put: function( ...args ) {
+ return put.apply( wpcom, extendRequest( 'put', args ) );
+ }
+ }
+ } );
+};