diff options
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/webapp/_locales/en/messages.json | 10 | ||||
-rw-r--r-- | remoting/webapp/client_screen.js | 20 | ||||
-rw-r--r-- | remoting/webapp/event_handlers.js | 15 | ||||
-rw-r--r-- | remoting/webapp/host_controller.js | 14 | ||||
-rw-r--r-- | remoting/webapp/host_list.js | 94 | ||||
-rw-r--r-- | remoting/webapp/host_screen.js | 28 | ||||
-rw-r--r-- | remoting/webapp/host_setup_dialog.js | 18 | ||||
-rw-r--r-- | remoting/webapp/main.html | 30 | ||||
-rw-r--r-- | remoting/webapp/oauth2.js | 81 | ||||
-rw-r--r-- | remoting/webapp/remoting.js | 22 | ||||
-rw-r--r-- | remoting/webapp/ui_mode.js | 1 | ||||
-rw-r--r-- | remoting/webapp/wcs.js | 13 | ||||
-rw-r--r-- | remoting/webapp/wcs_loader.js | 41 |
13 files changed, 256 insertions, 131 deletions
diff --git a/remoting/webapp/_locales/en/messages.json b/remoting/webapp/_locales/en/messages.json index 4ca21a7..fe9a84f 100644 --- a/remoting/webapp/_locales/en/messages.json +++ b/remoting/webapp/_locales/en/messages.json @@ -127,17 +127,13 @@ "description": "Column header in the connection history table showing the length of time for which a connection was active, if available." }, "ERROR_AUTHENTICATION_FAILED": { - "message": "Authentication failed. Please sign out of Chromoting and try again.", + "message": "Authentication failed. Please sign in to Chromoting again.", "description": "Error displayed if authentication fails. This can be caused by stale credentials, in which logging out of the web-app and retrying can fix the problem." }, "ERROR_BAD_PLUGIN_VERSION": { "message": "Some components required for Chromoting are missing. Please make sure you have installed the latest version and try again.", "description": "Error displayed if the host or client plugin are missing or if they could not be loaded." }, - "ERROR_GENERIC": { - "message": "An unknown error occurred. Please sign out of Chromoting and try again.", - "description": "Generic error message, displayed if something went wrong, but we aren't able to determine what, or the cause is too technical to be of use to a typical user." - }, "ERROR_HOST_OVERLOAD": { "message": "Connections to the remote computer are temporarily blocked because somebody was trying to connect to it with invalid PIN. Please try again later.", "description": "Error that is shown on the client side when the host is blocking all connections due to failed authentication attempts." @@ -428,6 +424,10 @@ "message": "Shrink to fit", "description": "Menu option for enabling shrink-to-fit. When clicked, the remote desktop will be scaled down so that it fits in the browser window." }, + "SIGN_IN_BUTTON": { + "message": "Sign in", + "description": "Sign in button, visible if the user's authentication token is corrupt, in which case the user has to sign in (authenticate) to the app again." + }, "SIGN_OUT_BUTTON": { "message": "Sign out", "description": "Sign out button, visible if the user has authenticated. Clicking this clears authentication credentials and returns the web-app to the initial 'unauthenticated' state." diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js index f57c937..808c825 100644 --- a/remoting/webapp/client_screen.js +++ b/remoting/webapp/client_screen.js @@ -70,7 +70,8 @@ remoting.currentConnectionType = null; */ remoting.connectIt2Me = function() { remoting.currentConnectionType = remoting.ConnectionType.It2Me; - remoting.WcsLoader.load(connectIt2MeWithAccessToken_); + remoting.WcsLoader.load(connectIt2MeWithAccessToken_, + remoting.defaultOAuthErrorHandler); }; /** @@ -176,7 +177,7 @@ function connectIt2MeWithAccessToken_(token) { } else { var supportId = remoting.accessCode.substring(0, kSupportIdLen); remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); - resolveSupportId(supportId); + resolveSupportId(supportId, token); } } else { showConnectError_(remoting.Error.AUTHENTICATION_FAILED); @@ -264,7 +265,7 @@ function onClientStateChange_(oldState, newState) { remoting.ClientSession.ConnectionError.HOST_OVERLOAD) { showConnectError_(remoting.Error.HOST_OVERLOAD); } else { - showConnectError_(remoting.Error.GENERIC); + showConnectError_(remoting.Error.UNEXPECTED); } if (clearPin) { @@ -334,7 +335,8 @@ function startSession_() { showConnectError_(remoting.Error.AUTHENTICATION_FAILED); } }; - remoting.oauth2.callWithToken(createPluginAndConnect); + remoting.oauth2.callWithToken(createPluginAndConnect, + remoting.defaultOAuthErrorHandler); } /** @@ -397,7 +399,7 @@ function parseServerResponse_(xhr) { console.error('Invalid "support-hosts" response from server.'); } } - var errorMsg = remoting.Error.GENERIC; + var errorMsg = remoting.Error.UNEXPECTED; if (xhr.status == 404) { errorMsg = remoting.Error.INVALID_ACCESS_CODE; } else if (xhr.status == 0) { @@ -426,10 +428,11 @@ function normalizeAccessCode_(accessCode) { * Initiate a request to the server to resolve a support ID. * * @param {string} supportId The canonicalized support ID. + * @param {string} token The OAuth access token. */ -function resolveSupportId(supportId) { +function resolveSupportId(supportId, token) { var headers = { - 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() + 'Authorization': 'OAuth ' + token }; remoting.supportHostsXhr_ = remoting.xhr.get( @@ -506,7 +509,8 @@ remoting.connectMe2MeWithPin = function() { document.title = chrome.i18n.getMessage('PRODUCT_NAME') + ': ' + host.hostName; - remoting.WcsLoader.load(connectMe2MeWithAccessToken_); + remoting.WcsLoader.load(connectMe2MeWithAccessToken_, + remoting.defaultOAuthErrorHandler); }; /** diff --git a/remoting/webapp/event_handlers.js b/remoting/webapp/event_handlers.js index 778c160..338bd1e 100644 --- a/remoting/webapp/event_handlers.js +++ b/remoting/webapp/event_handlers.js @@ -12,7 +12,15 @@ function onLoad() { window.location.replace(chrome.extension.getURL('main.html')); }; var goEnterAccessCode = function() { - remoting.setMode(remoting.AppMode.CLIENT_UNCONNECTED); + // We don't need a token until we authenticate, but asking for one here + // handles the token-expired case earlier, avoiding asking the user for + // the access code both before and after re-authentication. + remoting.oauth2.callWithToken( + /** @param {string} token */ + function(token) { + remoting.setMode(remoting.AppMode.CLIENT_UNCONNECTED); + }, + remoting.defaultOAuthErrorHandler); }; var goFinishedIt2Me = function() { if (remoting.currentMode == remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME) { @@ -87,7 +95,10 @@ function onLoad() { { event: 'click', id: 'host-config-install-dismiss', fn: function() { remoting.hostSetupDialog.hide(); } }, { event: 'click', id: 'host-config-install-retry', fn: function() { - remoting.hostSetupDialog.onInstallDialogRetry(); } } + remoting.hostSetupDialog.onInstallDialogRetry(); } }, + { event: 'click', id: 'token-refresh-error-ok', + fn: function() { remoting.setMode(remoting.AppMode.HOME); } }, + { event: 'click', id: 'token-refresh-error-sign-in', fn: doAuthRedirect } ]; for (var i = 0; i < actions.length; ++i) { diff --git a/remoting/webapp/host_controller.js b/remoting/webapp/host_controller.js index 63ef1f1..845e65d 100644 --- a/remoting/webapp/host_controller.js +++ b/remoting/webapp/host_controller.js @@ -225,14 +225,14 @@ remoting.HostController.prototype.start = function(hostPin, consent, callback) { * @param {string} publicKey */ function onKeyGenerated(privateKey, publicKey) { remoting.oauth2.callWithToken( - /** @param {string?} oauthToken */ + /** @param {string} oauthToken */ function(oauthToken) { - if (oauthToken) { - doRegisterHost(privateKey, publicKey, oauthToken); - } else { - // TODO(jamiewalch): Have a more specific error code here? - callback(remoting.HostController.AsyncResult.FAILED); - } + doRegisterHost(privateKey, publicKey, oauthToken); + }, + /** @param {remoting.Error} error */ + function(error) { + // TODO(jamiewalch): Have a more specific error code here? + callback(remoting.HostController.AsyncResult.FAILED); }); }; diff --git a/remoting/webapp/host_list.js b/remoting/webapp/host_list.js index dd5a546..540067e 100644 --- a/remoting/webapp/host_list.js +++ b/remoting/webapp/host_list.js @@ -19,9 +19,11 @@ var remoting = remoting || {}; * * @constructor * @param {Element} table The HTML <table> to contain host-list. - * @param {Element} errorDiv The HTML <div> to display error messages. + * @param {Element} errorMsg The HTML <div> to display error messages. + * @param {Element} errorButton The HTML <button> to display the error + * resolution action. */ -remoting.HostList = function(table, errorDiv) { +remoting.HostList = function(table, errorMsg, errorButton) { /** * @type {Element} * @private @@ -31,7 +33,12 @@ remoting.HostList = function(table, errorDiv) { * @type {Element} * @private */ - this.errorDiv_ = errorDiv; + this.errorMsg_ = errorMsg; + /** + * @type {Element} + * @private + */ + this.errorButton_ = errorButton; /** * @type {Array.<remoting.HostTableEntry>} * @private @@ -48,6 +55,10 @@ remoting.HostList = function(table, errorDiv) { */ this.lastError_ = ''; + this.errorButton_.addEventListener('click', + this.onErrorClick_.bind(this), + false); + // Load the cache of the last host-list, if present. var cachedStr = /** @type {string} */ (window.localStorage.getItem(remoting.HostList.HOSTS_KEY)); @@ -88,21 +99,21 @@ remoting.HostList.prototype.refresh = function(onDone) { var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone); /** @type {remoting.HostList} */ var that = this; - /** @param {string?} token The OAuth2 token. */ + /** @param {string} token The OAuth2 token. */ var getHosts = function(token) { - if (token) { - var headers = { 'Authorization': 'OAuth ' + token }; - remoting.xhr.get( - 'https://www.googleapis.com/chromoting/v1/@me/hosts', - parseHostListResponse, '', headers); - } else { - that.lastError_ = remoting.Error.AUTHENTICATION_FAILED; - onDone(false); - } + var headers = { 'Authorization': 'OAuth ' + token }; + remoting.xhr.get( + 'https://www.googleapis.com/chromoting/v1/@me/hosts', + parseHostListResponse, '', headers); + }; + /** @param {remoting.Error} error */ + var onError = function(error) { + that.lastError_ = error; + onDone(false); }; this.hosts_ = []; this.lastError_ = ''; - remoting.oauth2.callWithToken(getHosts); + remoting.oauth2.callWithToken(getHosts, onError); }; /** @@ -141,11 +152,8 @@ remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) { } else { // Some other error. console.error('Bad status on host list query: ', xhr); - if (xhr.status == 403) { - // The user's account is not enabled for Me2Me, so fail silently. - } else if (xhr.status >= 400 && xhr.status < 500) { - // For other errors, tell the user to re-authorize us. - this.lastError_ = remoting.Error.GENERIC; + if (xhr.status == 401) { + this.lastError_ = remoting.Error.AUTHENTICATION_FAILED; } else if (xhr.status == 503) { this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE; } else { @@ -170,7 +178,7 @@ remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) { */ remoting.HostList.prototype.display = function(thisHostId) { this.table_.innerText = ''; - this.errorDiv_.innerText = ''; + this.errorMsg_.innerText = ''; this.hostTableEntries_ = []; this.table_.hidden = (this.hosts_.length == 0); @@ -193,9 +201,16 @@ remoting.HostList.prototype.display = function(thisHostId) { } if (this.lastError_ != '') { - l10n.localizeElementFromTag(this.errorDiv_, this.lastError_); + l10n.localizeElementFromTag(this.errorMsg_, this.lastError_); + if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) { + l10n.localizeElementFromTag(this.errorButton_, + /*i18n-content*/'SIGN_IN_BUTTON'); + } else { + l10n.localizeElementFromTag(this.errorButton_, + /*i18n-content*/'RETRY'); + } } - this.errorDiv_.hidden = (this.lastError_ == ''); + this.errorMsg_.parentNode.hidden = (this.lastError_ == ''); }; /** @@ -219,19 +234,14 @@ remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) { * @return {void} Nothing. */ remoting.HostList.unregisterHostById = function(hostId) { - /** @param {string?} token The OAuth2 token. */ + /** @param {string} token The OAuth2 token. */ var deleteHost = function(token) { - if (token) { - var headers = { 'Authorization': 'OAuth ' + token }; - remoting.xhr.remove( - 'https://www.googleapis.com/chromoting/v1/@me/hosts/' + hostId, - function() {}, '', headers); - } else { - console.error('Could not unregister host. Authentication failure.'); - // TODO(jamiewalch): Add a callback to signify success/failure? - } + var headers = { 'Authorization': 'OAuth ' + token }; + remoting.xhr.remove( + 'https://www.googleapis.com/chromoting/v1/@me/hosts/' + hostId, + function() {}, '', headers); } - remoting.oauth2.callWithToken(deleteHost); + remoting.oauth2.callWithToken(deleteHost, remoting.defaultOAuthErrorHandler); }; /** @@ -271,7 +281,7 @@ remoting.HostList.prototype.renameHost = function(hostTableEntry) { console.error('Could not rename host. Authentication failure.'); } } - remoting.oauth2.callWithToken(renameHost); + remoting.oauth2.callWithToken(renameHost, remoting.defaultOAuthErrorHandler); }; /** @@ -285,6 +295,22 @@ remoting.HostList.prototype.addHost = function(localHost) { this.hosts_.push(localHost); window.localStorage.setItem(remoting.HostList.HOSTS_KEY, JSON.stringify(this.hosts_)); +}; + +/** + * Called when the user clicks the button next to the error message. The action + * depends on the error. + * + * @private + */ +remoting.HostList.prototype.onErrorClick_ = function() { + if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) { + remoting.oauth2.doAuthRedirect(); + } else { + this.lastError_ = ''; + this.display(null); + this.refresh(remoting.extractThisHostAndDisplay); + } } /** diff --git a/remoting/webapp/host_screen.js b/remoting/webapp/host_screen.js index f904c96..7edb2f0 100644 --- a/remoting/webapp/host_screen.js +++ b/remoting/webapp/host_screen.js @@ -26,20 +26,16 @@ var lastShareWasCancelled_ = false; */ remoting.tryShare = function() { console.log('Attempting to share...'); - lastShareWasCancelled_ = false; - if (remoting.oauth2.needsNewAccessToken()) { - console.log('Refreshing token...'); - remoting.oauth2.refreshAccessToken(function() { - if (remoting.oauth2.needsNewAccessToken()) { - // If we still need it, we're going to infinite loop. - showShareError_(remoting.Error.AUTHENTICATION_FAILED); - throw 'Unable to get access token'; - } - remoting.tryShare(); - }); - return; - } + remoting.oauth2.callWithToken(remoting.tryShareWithToken_, + remoting.defaultOAuthErrorHandler); +}; +/** + * @param {string} token The OAuth access token. + * @private + */ +remoting.tryShareWithToken_ = function(token) { + lastShareWasCancelled_ = false; onNatTraversalPolicyChanged_(true); // Hide warning by default. remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE); document.getElementById('cancel-share-button').disabled = false; @@ -50,7 +46,7 @@ remoting.tryShare = function() { remoting.hostSession.createPluginAndConnect( document.getElementById('host-plugin-container'), /** @type {string} */(remoting.oauth2.getCachedEmail()), - remoting.oauth2.getAccessToken(), + token, onNatTraversalPolicyChanged_, onHostStateChanged_, logDebugInfo_); @@ -124,7 +120,7 @@ function onHostStateChanged_(state) { } else if (state == remoting.HostSession.State.ERROR) { console.error('Host plugin state: ERROR'); - showShareError_(remoting.Error.GENERIC); + showShareError_(remoting.Error.UNEXPECTED); } else { console.error('Unknown state -> ' + state); } @@ -172,7 +168,7 @@ remoting.cancelShare = function() { // the host plugin, like we do for the client, which should handle crash // reporting and it should use a more detailed error message than the // default 'generic' one. See crbug.com/94624 - showShareError_(remoting.Error.GENERIC); + showShareError_(remoting.Error.UNEXPECTED); } disableTimeoutCountdown_(); }; diff --git a/remoting/webapp/host_setup_dialog.js b/remoting/webapp/host_setup_dialog.js index fe60ecb..c0e5ae2 100644 --- a/remoting/webapp/host_setup_dialog.js +++ b/remoting/webapp/host_setup_dialog.js @@ -150,6 +150,18 @@ remoting.HostSetupDialog = function(hostController) { * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.showForStart = function() { + // Although we don't need an access token in order to start the host, + // using callWithToken here ensures consistent error handling in the + // case where the refresh token is invalid. + remoting.oauth2.callWithToken(this.showForStartWithToken_.bind(this), + remoting.defaultOAuthErrorHandler); +}; + +/** + * @param {string} token The OAuth2 token. + * @private + */ +remoting.HostSetupDialog.prototype.showForStartWithToken_ = function(token) { /** @type {remoting.HostSetupDialog} */ var that = this; @@ -423,7 +435,7 @@ remoting.HostSetupDialog.validPin_ = function(pin) { } } return true; -} +}; /** * @return {void} Nothing. @@ -436,14 +448,14 @@ remoting.HostSetupDialog.prototype.onInstallDialogOk = function() { } else { remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL_PENDING); } -} +}; /** * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.onInstallDialogRetry = function() { remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL); -} +}; /** @type {remoting.HostSetupDialog} */ remoting.hostSetupDialog = null; diff --git a/remoting/webapp/main.html b/remoting/webapp/main.html index 17a338c..a47b80e 100644 --- a/remoting/webapp/main.html +++ b/remoting/webapp/main.html @@ -140,7 +140,12 @@ found in the LICENSE file. </div> <div id="me2me-content"> <div id="host-list" hidden></div> - <div id="host-list-error" class="error-state" hidden></div> + <div id="host-list-error" class="box" hidden> + <div id="host-list-error-message" class="error-state"></div> + <div class="box-spacer"></div> + <button type="button" + id="host-list-refresh-failed-button"></button> + </div> <div id="daemon-control" data-daemon-state="enabled disabled" hidden> <div class="section-row no-non-local-hosts" data-daemon-state="disabled"> @@ -182,15 +187,34 @@ found in the LICENSE file. </div> <!-- home --> <div id="dialog-screen" - data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup" + data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup home.token-refresh-failed" hidden></div> <div id="dialog-container" - data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup" + data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup home.token-refresh-failed" hidden> <div class="box-spacer"></div> + <!-- TODO(jamiewalch): Refactor the various error-state divs --> + <div class="kd-modaldialog" data-ui-mode="home.token-refresh-failed"> + <div class="message"> + <span id="token-refresh-error-message" class="error-state"></span> + </div> + <div id="token-refresh-auth-failed" class="box"> + <div class="box-spacer"></div> + <button id="token-refresh-error-sign-in" + type="button" + i18n-content="SIGN_IN_BUTTON"></button> + </div> + <div id="token-refresh-other-error" class="box"> + <div class="box-spacer"></div> + <button id="token-refresh-error-ok" + type="button" + i18n-content="OK"></button> + </div> + </div> <!-- home.token-refresh-failed --> + <div id="host-setup-dialog" class="kd-modaldialog" data-ui-mode="home.host-setup"> diff --git a/remoting/webapp/oauth2.js b/remoting/webapp/oauth2.js index 6024afc..e030822 100644 --- a/remoting/webapp/oauth2.js +++ b/remoting/webapp/oauth2.js @@ -184,8 +184,9 @@ remoting.OAuth2.prototype.needsNewAccessToken = function() { * Will throw if !isAuthenticated() or needsNewAccessToken(). * * @return {string} The access token. + * @private */ -remoting.OAuth2.prototype.getAccessToken = function() { +remoting.OAuth2.prototype.getAccessToken_ = function() { if (this.needsNewAccessToken()) { throw 'Access Token expired.'; } @@ -245,8 +246,9 @@ remoting.OAuth2.prototype.processTokenResponse_ = function(onDone, xhr) { * @param {function(XMLHttpRequest): void} onDone Callback to invoke on * completion. * @return {void} Nothing. + * @private */ -remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { +remoting.OAuth2.prototype.refreshAccessToken_ = function(onDone) { if (!this.isAuthenticated()) { throw 'Not Authenticated.'; } @@ -335,24 +337,51 @@ remoting.OAuth2.prototype.revokeToken_ = function(token) { * * The access token will remain valid for at least 2 minutes. * - * @param {function(string?):void} myfunc Function to invoke with access token. + * @param {function(string):void} onOk Function to invoke with access token if + * an access token was successfully retrieved. + * @param {function(remoting.Error):void} onError Function to invoke with an + * error code on failure. * @return {void} Nothing. */ -remoting.OAuth2.prototype.callWithToken = function(myfunc) { - /** @type {remoting.OAuth2} */ - var that = this; +remoting.OAuth2.prototype.callWithToken = function(onOk, onError) { if (this.needsNewAccessToken()) { - var onRefresh = function() { - if (that.needsNewAccessToken()) { - myfunc(null); - } else { - myfunc(that.getAccessToken()); - } - }; - this.refreshAccessToken(onRefresh); + this.refreshAccessToken_(this.onRefreshToken_.bind(this, onOk, onError)); } else { - myfunc(this.getAccessToken()); + onOk(this.getAccessToken_()); + } +}; + +/** + * Process token refresh results and notify caller. + * + * @param {function(string):void} onOk Function to invoke with access token if + * an access token was successfully retrieved. + * @param {function(remoting.Error):void} onError Function to invoke with an + * error code on failure. + * @param {XMLHttpRequest} xhr The result of the refresh operation. + * @private + */ +remoting.OAuth2.prototype.onRefreshToken_ = function(onOk, onError, xhr) { + var error = remoting.Error.UNEXPECTED; + if (xhr.status == 200) { + onOk(this.getAccessToken_()); + return; + } else if (xhr.status == 400) { + var result = + /** @type {{error: string}} */ (jsonParseSafe(xhr.responseText)); + if (result && result.error == 'invalid_grant') { + error = remoting.Error.AUTHENTICATION_FAILED; + } + } else if (xhr.status == 401) { + // According to the OAuth2 draft RFC, the server shouldn't return 401, + // but AUTHENTICATION_FAILED is the obvious interpretation if it does. + console.warn('Unexpected 401 in response to refresh.'); + error = remoting.Error.AUTHENTICATION_FAILED; + } else if (xhr.status == 503) { + error = remoting.Error.SERVICE_UNAVAILABLE; } + // TODO(jamiewalch): Add timeout support. + onError(error); }; /** @@ -378,20 +407,20 @@ remoting.OAuth2.prototype.getEmail = function(setEmail) { setEmail(that.email); }; - /** @param {string?} token The access token. */ + /** @param {string} token The access token. */ var getEmailFromToken = function(token) { - if (token) { - var headers = { 'Authorization': 'OAuth ' + token }; - // TODO(ajwong): Update to new v2 API. - remoting.xhr.get('https://www.googleapis.com/userinfo/email', - onResponse, '', headers); - } else { - console.error('Unable to get email address: no access token'); - setEmail(null); - } + var headers = { 'Authorization': 'OAuth ' + token }; + // TODO(ajwong): Update to new v2 API. + remoting.xhr.get('https://www.googleapis.com/userinfo/email', + onResponse, '', headers); + }; + /** @param {remoting.Error} error */ + var onError = function(error) { + console.error('Unable to get email address: ' + error); + setEmail(null); }; - this.callWithToken(getEmailFromToken); + this.callWithToken(getEmailFromToken, onError); }; /** diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index 1737436..5fa18f4 100644 --- a/remoting/webapp/remoting.js +++ b/remoting/webapp/remoting.js @@ -22,7 +22,6 @@ remoting.Error = { BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION', NETWORK_FAILURE: /*i18n-content*/'ERROR_NETWORK_FAILURE', HOST_OVERLOAD: /*i18n-content*/'ERROR_HOST_OVERLOAD', - GENERIC: /*i18n-content*/'ERROR_GENERIC', UNEXPECTED: /*i18n-content*/'ERROR_UNEXPECTED', SERVICE_UNAVAILABLE: /*i18n-content*/'ERROR_SERVICE_UNAVAILABLE' }; @@ -40,7 +39,8 @@ remoting.init = function() { remoting.formatIq = new remoting.FormatIq(); remoting.hostList = new remoting.HostList( document.getElementById('host-list'), - document.getElementById('host-list-error')); + document.getElementById('host-list-error-message'), + document.getElementById('host-list-refresh-failed-button')); remoting.toolbar = new remoting.Toolbar( document.getElementById('session-toolbar')); remoting.clipboard = new remoting.Clipboard(); @@ -322,3 +322,21 @@ remoting.timestamp = function() { pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3); return '[' + timestamp + ']'; }; + +/** + * Default handler for OAuth token refresh failures. This switches the app mode + * to display an error message, optionally including a short-cut for signing in + * to Chromoting again. + * + * @param {remoting.Error} error + * @return {void} Nothing. + */ +remoting.defaultOAuthErrorHandler = function(error) { + l10n.localizeElementFromTag( + document.getElementById('token-refresh-error-message'), + error); + var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED); + document.getElementById('token-refresh-auth-failed').hidden = !auth_failed; + document.getElementById('token-refresh-other-error').hidden = auth_failed; + remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED); +}; diff --git a/remoting/webapp/ui_mode.js b/remoting/webapp/ui_mode.js index ea1862d..e800e4a 100644 --- a/remoting/webapp/ui_mode.js +++ b/remoting/webapp/ui_mode.js @@ -22,6 +22,7 @@ var remoting = remoting || {}; remoting.AppMode = { HOME: 'home', UNAUTHENTICATED: 'home.auth', + TOKEN_REFRESH_FAILED: 'home.token-refresh-failed', HOST: 'home.host', HOST_WAITING_FOR_CODE: 'home.host.waiting-for-code', HOST_WAITING_FOR_CONNECTION: 'home.host.waiting-for-connection', diff --git a/remoting/webapp/wcs.js b/remoting/webapp/wcs.js index 4906c82..d5c4257 100644 --- a/remoting/webapp/wcs.js +++ b/remoting/webapp/wcs.js @@ -53,6 +53,10 @@ remoting.Wcs = function(wcsIqClient, token, onReady) { this.clientFullJid_ = ''; var updateAccessToken = this.updateAccessToken_.bind(this); + /** @param {remoting.Error} error */ + var onError = function(error) { + console.error('updateAccessToken: Authentication failed: ' + error); + }; /** * A timer that polls for an updated access token. @@ -60,7 +64,9 @@ remoting.Wcs = function(wcsIqClient, token, onReady) { * @private */ this.pollForUpdatedToken_ = setInterval( - function() { remoting.oauth2.callWithToken(updateAccessToken); }, + function() { + remoting.oauth2.callWithToken(updateAccessToken, onError); + }, 60 * 1000); /** @@ -85,10 +91,7 @@ remoting.Wcs = function(wcsIqClient, token, onReady) { * @private */ remoting.Wcs.prototype.updateAccessToken_ = function(tokenNew) { - if (!tokenNew) { - console.error('updateAccessToken_: Authentication failed.'); - // No need to update the token. The hanging GET will fail anyway. - } else if (tokenNew != this.token_) { + if (tokenNew != this.token_) { this.token_ = tokenNew; this.wcsIqClient_.updateAccessToken(this.token_); } diff --git a/remoting/webapp/wcs_loader.js b/remoting/webapp/wcs_loader.js index fdd3736..000369d 100644 --- a/remoting/webapp/wcs_loader.js +++ b/remoting/webapp/wcs_loader.js @@ -33,24 +33,21 @@ remoting.WcsLoader = function() { /** * Load WCS if necessary, then invoke the callback with an access token. * - * @param {function(string?): void} onReady The callback function, called with - * an OAuth2 access token when WCS has been loaded, or with null on error. + * @param {function(string): void} onReady The callback function, called with + * an OAuth2 access token when WCS has been loaded. + * @param {function(remoting.Error):void} onError Function to invoke with an + * error code on failure. * @return {void} Nothing. */ -remoting.WcsLoader.load = function(onReady) { +remoting.WcsLoader.load = function(onReady, onError) { if (!remoting.wcsLoader) { remoting.wcsLoader = new remoting.WcsLoader(); } - /** @param {string?} token The OAuth2 access token. */ + /** @param {string} token The OAuth2 access token. */ var start = function(token) { - if (token) { - remoting.wcsLoader.start_(token, onReady); - } else { - console.error('WcsLoader: Authentication failed.'); - onReady(null); - } + remoting.wcsLoader.start_(token, onReady, onError); }; - remoting.oauth2.callWithToken(start); + remoting.oauth2.callWithToken(start, onError); }; /** @@ -79,15 +76,18 @@ remoting.WcsLoader.prototype.SCRIPT_NODE_LOADED_FLAG_ = 'wcs-script-loaded'; * Starts loading the WCS IQ client. * * When it's loaded, construct remoting.wcs as a wrapper for it. - * When the WCS connection is ready, or on error, call |onReady|. + * When the WCS connection is ready, or on error, call |onReady| or |onError|, + * respectively. * * @param {string} token An OAuth2 access token. - * @param {function(string?): void} onReady The callback function, called with - * an OAuth2 access token when WCS has been loaded, or with null on error. + * @param {function(string): void} onReady The callback function, called with + * an OAuth2 access token when WCS has been loaded. + * @param {function(remoting.Error):void} onError Function to invoke with an + * error code on failure. * @return {void} Nothing. * @private */ -remoting.WcsLoader.prototype.start_ = function(token, onReady) { +remoting.WcsLoader.prototype.start_ = function(token, onReady, onError) { var node = document.getElementById(this.SCRIPT_NODE_ID_); if (!node) { // The first time, there will be no script node, so create one. @@ -111,21 +111,22 @@ remoting.WcsLoader.prototype.start_ = function(token, onReady) { typedNode.setAttribute(that.SCRIPT_NODE_LOADED_FLAG_, true); that.constructWcs_(token, onReady); }; - var onError = function() { + var removeNodeAndNotify = function() { var typedNode = /** @type {Element} */ (node); typedNode.parentNode.removeChild(node); - onReady(null); + // TODO(jamiewalch): See if we can do better by looking at the event. + onError(remoting.Error.NETWORK_FAILURE); }; node.addEventListener('load', onLoad, false); - node.addEventListener('error', onError, false); + node.addEventListener('error', removeNodeAndNotify, false); }; /** * Constructs the remoting.wcs object. * * @param {string} token An OAuth2 access token. - * @param {function(string?): void} onReady The callback function, called with - * an OAuth2 access token when WCS has been loaded, or with null on error. + * @param {function(string): void} onReady The callback function, called with + * an OAuth2 access token when WCS has been loaded. * @return {void} Nothing. * @private */ |