diff options
author | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-07-07 02:23:03 +0000 |
---|---|---|
committer | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-07-07 02:23:03 +0000 |
commit | 134acbf8b30f5797eed8bdb344f7425b706a8144 (patch) | |
tree | b0710cbfefe282f5b8dac1aaf7e8f6555b31251e /remoting | |
parent | 5272ff3b41be1ff32138375791e8296095ab964d (diff) | |
download | chromium_src-134acbf8b30f5797eed8bdb344f7425b706a8144.zip chromium_src-134acbf8b30f5797eed8bdb344f7425b706a8144.tar.gz chromium_src-134acbf8b30f5797eed8bdb344f7425b706a8144.tar.bz2 |
Remove the background page, and majorly refactor Javascript + Webapp.
This removes remoting_session.html, puts the "in-session" UI into the same page, uses classes to hide/display elements, and redefines what we mean by the <section> tag.
Also takes a bunch of the javascript logic and makes classes out of it as well as detangling a bit of the UI manipulation from the behavior functions.
BUG=none
TEST=manual
Review URL: http://codereview.chromium.org/7277020
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@91658 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/remoting.gyp | 8 | ||||
-rw-r--r-- | remoting/webapp/me2mom/background.html | 9 | ||||
-rw-r--r-- | remoting/webapp/me2mom/background.js | 22 | ||||
-rw-r--r-- | remoting/webapp/me2mom/choice.css | 70 | ||||
-rw-r--r-- | remoting/webapp/me2mom/choice.html | 94 | ||||
-rw-r--r-- | remoting/webapp/me2mom/client_session.js | 320 | ||||
-rw-r--r-- | remoting/webapp/me2mom/debug_log.js | 42 | ||||
-rw-r--r-- | remoting/webapp/me2mom/manifest.json | 1 | ||||
-rw-r--r-- | remoting/webapp/me2mom/oauth2.js | 307 | ||||
-rw-r--r-- | remoting/webapp/me2mom/oauth2_callback.html | 3 | ||||
-rw-r--r-- | remoting/webapp/me2mom/plugin_settings.js | 3 | ||||
-rw-r--r-- | remoting/webapp/me2mom/remoting.js | 315 | ||||
-rw-r--r-- | remoting/webapp/me2mom/remoting_session.css | 54 | ||||
-rw-r--r-- | remoting/webapp/me2mom/remoting_session.html | 38 | ||||
-rw-r--r-- | remoting/webapp/me2mom/remoting_session.js | 317 | ||||
-rw-r--r-- | remoting/webapp/me2mom/storage.js | 96 | ||||
-rw-r--r-- | remoting/webapp/me2mom/xhr.js | 154 |
17 files changed, 1041 insertions, 812 deletions
diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index 1442876..66d23fc 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -34,8 +34,6 @@ ], 'remoting_it2me_files': [ 'resources/chromoting128.png', - 'webapp/me2mom/background.html', - 'webapp/me2mom/background.js', 'webapp/me2mom/choice.css', 'webapp/me2mom/choice.html', 'webapp/me2mom/cs_oauth2_trampoline.js', @@ -49,11 +47,9 @@ 'webapp/me2mom/oauth2_callback.html', 'webapp/me2mom/plugin_settings.js', 'webapp/me2mom/remoting.js', - 'webapp/me2mom/remoting_session.css', - 'webapp/me2mom/remoting_session.html', - 'webapp/me2mom/remoting_session.js', + 'webapp/me2mom/client_session.js', 'webapp/me2mom/spinner.gif', - 'webapp/me2mom/storage.js', + 'webapp/me2mom/xhr.js', ], }, diff --git a/remoting/webapp/me2mom/background.html b/remoting/webapp/me2mom/background.html deleted file mode 100644 index b3570d3..0000000 --- a/remoting/webapp/me2mom/background.html +++ /dev/null @@ -1,9 +0,0 @@ -<!doctype html> -<!-- -Copyright (c) 2011 The Chromium Authors. All rights reserved. -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. ---> -<html> - <script src="background.js"></script> -</html> diff --git a/remoting/webapp/me2mom/background.js b/remoting/webapp/me2mom/background.js deleted file mode 100644 index 0da714b..0000000 --- a/remoting/webapp/me2mom/background.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -var remoting = {}; - -function setItem(key, value) { - window.localStorage.setItem(key, value); -} - -function getItem(key, defaultValue) { - var result = window.localStorage.getItem(key); - return (result != null) ? result : defaultValue; -} - -function removeItem(key) { - window.localStorage.removeItem(key); -} - -function clearAll() { - window.localStorage.clear(); -} diff --git a/remoting/webapp/me2mom/choice.css b/remoting/webapp/me2mom/choice.css index ab592f1..7efe829 100644 --- a/remoting/webapp/me2mom/choice.css +++ b/remoting/webapp/me2mom/choice.css @@ -9,7 +9,6 @@ a { text-decoration: none; } - body { background: -webkit-gradient(radial, center center, 0, center center, 400, from(rgb(254, 254, 254)), @@ -50,11 +49,7 @@ button[disabled], .button[disabled]:hover { -webkit-box-shadow: none; } -footer { - font-size: 14px; -} - -header h1 { +h1 { font-size: 24px; font-weight: normal; margin-left: 10px; @@ -65,11 +60,6 @@ label { font-weight: bold; } -section { - margin-top: 17px; - padding-bottom: 20px; -} - /* Classes */ .access-code-digit-group { @@ -101,6 +91,21 @@ section { text-align: center; } +.choice-header { + font-size: 24px; + font-weight: normal; + margin-left: 10px; +} + +.choice-footer { + font-size: 14px; +} + +.choice-panel { + margin-top: 17px; + padding-bottom: 20px; +} + .client-element { /* Class used to denote client UI elements. */ } @@ -116,6 +121,11 @@ section { */ } +.hidden { + display: none; +} + + .host-element { /* Class used to denote host UI elements. */ } @@ -136,16 +146,12 @@ section { */ } -.hidden { - display: none; -} - .waiting { color: rgb(180, 180, 180); } /* Ids */ -#auth-panel { +#control-panel { border-bottom: 2px solid gray; padding: 5px 10px; } @@ -172,7 +178,7 @@ section { text-align: center; } -#main-panel { +#choice-mode { color: rgb(115, 115, 115); font-size: 16px; margin: 100px auto 0 auto; @@ -184,8 +190,8 @@ section { float: right; } -#email-status { - margin-right: 0.5ex; +#client-footer-text, #host-footer-text { + text-align: center; } #current-email { @@ -200,18 +206,36 @@ section { margin: 25px 0 10px 0; } +#email-status { + margin-right: 0.5ex; +} + #icon { height: 64px; width: 64px; } + #oauth2-entry { width: 400px; } -#server-response { - font-weight: bolder; +#session-mode { + overflow: auto; + width: 100%; + -webkit-user-select: none; } -#client-footer, #host-footer { - text-align: center; +#session-status-msg { + /* + clear: both; forces the session-controls div to size appropriately for + session-buttons. + */ + clear: both; + font-family: monospace; + font-size: small; + margin: 5px; +} + +#server-response { + font-weight: bolder; } diff --git a/remoting/webapp/me2mom/choice.html b/remoting/webapp/me2mom/choice.html index 1ca4c08..1b95111 100644 --- a/remoting/webapp/me2mom/choice.html +++ b/remoting/webapp/me2mom/choice.html @@ -14,53 +14,64 @@ found in the LICENSE file. <link rel="stylesheet" href="debug_log.css" /> <link rel="stylesheet" href="main.css" /> <link rel="stylesheet" href="choice.css" /> + <script src="client_session.js"></script> <script src="debug_log.js"></script> <script src="oauth2.js"></script> <script src="plugin_settings.js"></script> <script src="remoting.js"></script> + <script src="xhr.js"></script> <title>Chromoting</title> </head> - <body> - <div id="auth-panel"> - <span id="email-status" class="display-inline"> + <body onLoad="remoting.init();"> + <nav id="control-panel"> + <span id="email-status"> <span class="auth-status-label">Email:</span> - <span id="current-email" class="display-inline"></span> + <span id="current-email"></span> </span> <!-- email-status --> - <span id="oauth2-entry"> + <span id="oauth2-entry" class="client-element host-element"> <form> <span class="auth-status-label">OAuth2 Token:</span> - <input id="oauth2-token-button" class="display-inline" type="button" - onclick="remoting.oauth2.openOAuth2Window(); return false;" + <input id="oauth2-token-button" type="button" + onclick="remoting.oauth2.doAuthRedirect();" value="Get OAuth2 Token…" /> - <input id="oauth2-clear-button" class="display-inline" type="button" - onclick="clearOAuth2(); return false;" value="Revoke" /> + <input id="oauth2-clear-button" type="button" + onclick="remoting.clearOAuth2();" + value="Revoke" /> </form> </span> <!-- oauth2-entry --> + <span id="session-controls" class="in-session-element"> + <form id="session-buttons"> + <input id="scale-to-fit-toggle" type="button" + value="Scale to fit" onclick="remoting.toggleScaleToFit();"/> + </form> + <span id="session-status-message">Initializing...</span> + </span> <!-- session-controls --> + <span id="debug-enable"> <form> - <input id="debug-log-toggle" class="display-inline" type="button" - value="Debug Log" onclick="toggleDebugLog(); return false;"/> + <input id="debug-log-toggle" type="button" value="Debug Log" + onclick="remoting.toggleDebugLog();"/> </form> </span> <!-- debug-enable --> - </div> <!-- auth-panel --> + </nav> <!-- control-panel --> - <div id="main-panel" class="hidden"> - <header> + <section id="choice-mode" class="mode hidden host-element client-element"> + <header class="choice-header"> <img id="icon" src="chromoting128.png"> - <h1 class="icon-label host-element display-inline"> + <h1 class="icon-label host-element"> Chromoting › Share </h1> - <h1 class="icon-label client-element display-inline"> + <h1 class="icon-label client-element"> Chromoting › Connect </h1> <img id="divider-top" src="dividertop.png"> </header> - <section id="host-section" class="host-element"> + <div id="host-panel" class="host-element"> <div id="unshared" class="mode"> <div class="description"> With Chromoting you can easily let another Chrome user see and @@ -68,7 +79,7 @@ found in the LICENSE file. </div> <div class="centered-button"> <button id="share-button" class="auth-status-control" - type="button" onclick="tryShare(); return false;"> + type="button" onclick="remoting.tryShare();"> Share this computer </button> </div> @@ -97,7 +108,7 @@ found in the LICENSE file. Your desktop is currently being shared. </div> <div class="centered-button"> - <button type="button" onclick="cancelShare(); return false;"> + <button type="button" onclick="remoting.cancelShare();"> Stop sharing </button> </div> @@ -109,14 +120,14 @@ found in the LICENSE file. </div> <div class="centered-button"> <button type="button" class="auth-status-control" - onclick="setHostMode('unshared'); return false;"> + onclick="remoting.setHostMode('unshared');"> OK </button> </div> </div> <!-- share-failed --> - </section> <!-- host-section --> + </div> <!-- host-panel --> - <section id="client-section" class="client-element"> + <div id="client-panel" class="client-element"> <div id="unconnected" class="mode"> <div class="description"> Have the user whose computer you wish to access click @@ -124,7 +135,7 @@ found in the LICENSE file. their access code to you. </div> <div id="access-code-entry-row"> - <form action="" onsubmit="tryConnect(); return false;"> + <form action="" onsubmit="remoting.tryConnect(); return false;"> <label class="auth-status-control" for="access-code-entry"> Access code </label> @@ -159,50 +170,53 @@ found in the LICENSE file. </div> <div class="centered-button"> <button type="button" class="auth-status-control" - onclick="setClientMode('unconnected'); return false;"> + onclick="remoting.setClientMode('unconnected');"> OK </button> </div> </div> <!-- connect-failed --> - </section> <!-- client-section --> + </div> <!-- client-panel --> - <footer> + <footer class="choice-footer"> <img id="divider-bottom" src="dividerbottom.png"> - <div id="client-footer" class="client-element"> + <div id="client-footer-text" class="client-element"> Click here to <a class="switch-mode" href="#" - onclick="setGlobalModePersistent(remoting.HOST_MODE); - return false;">share this computer</a> + onclick="remoting.setAppMode(remoting.AppMode.HOST);"> + share this computer</a> with another user. - </div> <!-- client-footer --> + </div> <!-- client-footer-text --> - <div id="host-footer" class="host-element"> + <div id="host-footer-text" class="host-element"> Click here to <a class="switch-mode" href="#" - onclick="setGlobalModePersistent(remoting.CLIENT_MODE); - return false;"> + onclick="remoting.setAppMode(remoting.AppMode.CLIENT);"> access a shared computer</a>. - </div> <!-- host-footer --> + </div> <!-- host-footer-text --> <div id="waiting-footer"> <img src="spinner.gif"> <span class="waiting icon-label">waiting for connection…</span> <button id="cancel-button" - onclick="cancelPendingOperation();"> + onclick="remoting.cancelPendingOperation();"> Cancel </button> </div> <!-- waiting-footer --> </footer> - </div> <!-- main-panel --> + </section> <!-- choice-mode --> + + <section id="session-mode" class="mode in-session-element hidden"> + </section> <!-- session-mode --> - <div id="loading-panel"> + + <section id="loading-mode"> <em>Loading…</em> - </div> + </section> - <div id="plugin-wrapper"> + <div id="host-plugin-container" class="hidden"> </div> - <div id="debug-log"> + <div id="debug-log" class="hidden"> </div> </body> </html> diff --git a/remoting/webapp/me2mom/client_session.js b/remoting/webapp/me2mom/client_session.js new file mode 100644 index 0000000..87102682 --- /dev/null +++ b/remoting/webapp/me2mom/client_session.js @@ -0,0 +1,320 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview + * Session class that handles creation and teardown of a remoting session. + * + * This abstracts a <embed> element and controls the plugin which does the + * actual remoting work. There should be no UI code inside this class. It + * should be purely thought of as a controller of sorts. + */ + +"use strict"; + +var remoting = remoting || {}; + +(function() { +/** + * @param {string} hostJid The jid of the host to connect to. + * @param {string} hostPublicKey The base64 encoded version of the host's + * public key. + * @param {string} accessCode The access code for the IT2Me connection. + * @param {string} email The username for the talk network. + * @param {function(remoting.ClientSession.State):void} onStateChange + * The callback to invoke when the session changes state. + * @constructor + */ +remoting.ClientSession = function(hostJid, hostPublicKey, accessCode, email, + onStateChange) { + this.state = remoting.ClientSession.CREATED; + + this.hostJid = hostJid; + this.hostPublicKey = hostPublicKey; + this.accessCode = accessCode; + this.email = email; + this.clientJid = ''; + this.onStateChange = onStateChange; +}; + +/** @enum {number} */ +remoting.ClientSession.State = { + UNKNOWN: 0, + CREATED: 1, + BAD_PLUGIN_VERSION: 2, + UNKNOWN_PLUGIN_ERROR: 3, + CONNECTING: 4, + INITIALIZING: 5, + CONNECTED: 6, + CLOSED: 7, + CONNECTION_FAILED: 8 +}; + +/** + * The current state of the session. + * @type {remoting.ClientSession.State} + */ +remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN; + +/** + * Chromoting session API version (for this javascript). + * This is compared with the plugin API version to verify that they are + * compatible. + * + * @const + */ +remoting.ClientSession.prototype.API_VERSION_ = 2; + +/** + * Server used to bridge into the Jabber network for establishing Jingle + * connections. + * + * @const + */ +remoting.ClientSession.prototype.HTTP_XMPP_PROXY_ = + 'https://chromoting-httpxmpp-oauth2-dev.corp.google.com'; + +/** + * The oldest API version that we support. + * This will differ from the |API_VERSION_| if we maintain backward + * compatibility with older API versions. + * + * @const + */ +remoting.ClientSession.prototype.API_MIN_VERSION_ = 1; + +/** + * Callback to invoke when the state is changed. + * + * @type {function(remoting.ClientSession.State):void} + */ +remoting.ClientSession.prototype.onStateChange = null; + +/** + * Adds <embed> tag to |container| and readies the sesion object. + * + * @param {Element} container The element to add the plugin to. + * @param {string} oauth2AccessToken A valid OAuth2 access token. + * @return {void} + */ +remoting.ClientSession.prototype.createPluginAndConnect = + function(container, oauth2AccessToken) { + this.plugin = document.createElement('embed'); + this.plugin.id = 'session-client-plugin'; + this.plugin.src = 'about://none'; + this.plugin.type = 'pepper-application/x-chromoting'; + this.plugin.className = 'client-plugin'; + container.appendChild(this.plugin); + + if (!this.isPluginVersionSupported_(this.plugin)) { + // TODO(ajwong): Remove from parent. + delete this.plugin; + this.setState_(remoting.ClientSession.BAD_PLUGIN_VERSION); + return; + } + + var that = this; + this.plugin.sendIq = function(msg) { that.sendIq_(msg); }; + this.plugin.debugInfo = function(msg) { + remoting.debug.log('plugin: ' + msg); + }; + + // TODO(ajwong): Is it even worth having this class handle these events? + // Or would it be better to just allow users to pass in their own handlers + // and leave these blank by default? + this.plugin.connectionInfoUpdate = function() { + that.connectionInfoUpdateCallback(); + }; + this.plugin.desktopSizeUpdate = function() { that.onDesktopSizeChanged_(); }; + + // For IT2Me, we are pre-authorized so there is no login challenge. + this.plugin.loginChallenge = function () {}; + + // TODO(garykac): Clean exit if |connect| isn't a function. + if (typeof this.plugin.connect === 'function') { + this.registerConnection_(oauth2AccessToken); + } else { + remoting.debug.log('ERROR: remoting plugin not loaded'); + this.setState_(remoting.ClientSession.UNKNOWN_PLUGIN_ERROR); + } +}; + +/** + * Sends an IQ stanza via the http xmpp proxy. + * + * @param {string} msg XML string of IQ stanza to send to server. + * @return {void} + */ +remoting.ClientSession.prototype.sendIq_ = function(msg) { + remoting.debug.log('Sending Iq: ' + msg); + + // Extract the top level fields of the Iq packet. + // TODO(ajwong): Can the plugin just return these fields broken out. + var parser = new DOMParser(); + var iqNode = parser.parseFromString(msg, 'text/xml').firstChild; + var serializer = new XMLSerializer(); + var parameters = { + 'to': iqNode.getAttribute('to'), + 'payload_xml': serializer.serializeToString(iqNode.firstChild), + 'id': iqNode.getAttribute('id') || '1', + 'type': iqNode.getAttribute('type'), + 'host_jid': this.hostJid + }; + + remoting.xhr.post(this.HTTP_XMPP_PROXY_ + '/sendIq', function(xhr) {}, + parameters, {}, true); +}; + +/** + * Executes a poll loop on the server for more IQ packet to feed to the plugin. + * + * @return {void} + */ +remoting.ClientSession.prototype.feedIq_ = function() { + var that = this; + + var onIq = function(xhr) { + if (xhr.status == 200) { + remoting.debug.log('Receiving Iq: --' + xhr.responseText + '--'); + that.plugin.onIq(xhr.responseText); + } + if (xhr.status == 200 || xhr.status == 204) { + // Poll again. + that.feedIq_(); + } else { + remoting.debug.log('HttpXmpp gateway returned code: ' + xhr.status); + that.plugin.disconnect(); + that.setState_(remoting.ClientSession.CONNECTION_FAILED); + } + } + + remoting.xhr.get(this.HTTP_XMPP_PROXY_ + '/readIq', onIq, + {'host_jid': this.hostJid}, {}, true); +}; + +/** + * @param {Element} plugin The embed element for the plugin. + * @return {boolean} + */ +remoting.ClientSession.prototype.isPluginVersionSupported_ = function(plugin) { + return this.API_VERSION_ >= plugin.apiMinVersion && + plugin.apiVersion >= this.API_MIN_VERSION_; +}; + +/** + * Registers a new connection with the HttpXmpp proxy. + * + * @param {string} oauth2AccessToken A valid OAuth2 access token. + * @return {void} + */ +remoting.ClientSession.prototype.registerConnection_ = + function(oauth2AccessToken) { + var parameters = { + 'host_jid': this.hostJid, + 'username': this.email, + 'password': oauth2AccessToken + }; + + var that = this; + var onRegistered = function(xhr) { + if (xhr.status != 200) { + remoting.debug.log('FailedToConnect: --' + xhr.responseText + + '-- (status=' + xhr.status + ')'); + that.setState_(remoting.ClientSession.CONNECTION_FAILED); + return; + } + + remoting.debug.log('Receiving Iq: --' + xhr.responseText + '--'); + that.clientJid = xhr.responseText; + + // TODO(ajwong): Remove old version support. + if (that.plugin.API_VERSION_ >= 2) { + that.plugin.connect(that.hostJid, that.hostPublicKey, that.clientJid, + that.accessCode); + } else { + that.plugin.connect(that.hostJid, that.clientJid, that.accessCode); + } + that.feedIq_(); + }; + + remoting.xhr.post(this.HTTP_XMPP_PROXY_ + '/newConnection', + onRegistered, parameters, {}, true); +}; + +/** + * Callback that the plugin invokes to indicate that the connection + * status has changed. + */ +remoting.ClientSession.prototype.connectionInfoUpdateCallback = function () { + var state = this.plugin.status; + + // TODO(ajwong): We're doing silly type translation here. Any way to avoid? + if (state == this.plugin.STATUS_UNKNOWN) { + this.setState_(remoting.ClientSession.State.UNKNOWN); + } else if (state == this.plugin.STATUS_CONNECTING) { + this.setState_(remoting.ClientSession.State.CONNECTING); + } else if (state == this.plugin.STATUS_INITIALIZING) { + this.setState_(remoting.ClientSession.State.INITIALIZING); + } else if (state == this.plugin.STATUS_CONNECTED) { + this.onDesktopSizeChanged_(); + this.setState_(remoting.ClientSession.State.CONNECTED); + } else if (state == this.plugin.STATUS_CLOSED) { + this.setState_(remoting.ClientSession.State.CLOSED); + } else if (state == this.plugin.STATUS_FAILED) { + this.setState_(remoting.ClientSession.State.CONNECTION_FAILED); + } +}; + +/** + * @param {remoting.ClientSession.State} state The new state for the session. + * @return {void} + */ +remoting.ClientSession.prototype.setState_ = function(state) { + this.state = state; + if (this.onStateChange) { + this.onStateChange(this.state); + } +}; + +/** + * This is a callback that gets called when the desktop size contained in the + * the plugin has changed. + * + * @return {void} + */ +remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() { + var width = this.plugin.desktopWidth; + var height = this.plugin.desktopHeight; + remoting.debug.log('desktop size changed: ' + width + 'x' + height); + this.plugin.width = width; + this.plugin.height = height; +}; + +/** + * Informs the plugin that it should scale itself. + * + * @param {boolean} shouldScale If the plugin should scale itself. + * @return {void} + */ +remoting.ClientSession.prototype.toggleScaleToFit = function (shouldScale) { + this.plugin.setScaleToFit(shouldScale); +}; + +/** + * Returns an associative array with a set of stats for this connection. + * + * @return {Object} + */ +remoting.ClientSession.prototype.stats = function () { + return { + 'video_bandwidth': this.plugin.videoBandwidth, + 'capture_latency': this.plugin.videoCaptureLatency, + 'encode_latency': this.plugin.videoEncodeLatency, + 'decode_latency': this.plugin.videoDecodeLatency, + 'render_latency': this.plugin.videoRenderLatency, + 'roundtrip_latency': this.plugin.roundTripLatency + }; +}; + +}()); diff --git a/remoting/webapp/me2mom/debug_log.js b/remoting/webapp/me2mom/debug_log.js index a848b2d..2a1ad1c 100644 --- a/remoting/webapp/me2mom/debug_log.js +++ b/remoting/webapp/me2mom/debug_log.js @@ -2,41 +2,43 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Maximum numer of lines to record in the debug log. -// Only the most recent <n> lines are displayed. -var MAX_DEBUG_LOG_SIZE = 1000; +/** + * @fileoverview + * Module to support logging debug messages. + */ -function toggleDebugLog() { - debugLog = document.getElementById('debug-log'); - toggleButton = document.getElementById('debug-log-toggle'); +"use strict"; +var remoting = remoting || {}; - if (!debugLog.style.display || debugLog.style.display == 'none') { - debugLog.style.display = 'block'; - toggleButton.value = 'Hide Debug Log'; - } else { - debugLog.style.display = 'none'; - toggleButton.value = 'Show Debug Log'; - } +(function() { + +/** @constructor */ +remoting.DebugLog = function(logElement) { + this.debugLog = logElement; } +// Maximum numer of lines to record in the debug log. +// Only the most recent <n> lines are displayed. +var MAX_DEBUG_LOG_SIZE = 1000; + /** * Add the given message to the debug log. * * @param {string} message The debug info to add to the log. */ -function addToDebugLog(message) { - var debugLog = document.getElementById('debug-log'); - +remoting.DebugLog.prototype.log = function(message) { // Remove lines from top if we've hit our max log size. - if (debugLog.childNodes.length == MAX_DEBUG_LOG_SIZE) { - debugLog.removeChild(debugLog.firstChild); + if (this.debugLog.childNodes.length == MAX_DEBUG_LOG_SIZE) { + this.debugLog.removeChild(this.debugLog.firstChild); } // Add the new <p> to the end of the debug log. var p = document.createElement('p'); p.appendChild(document.createTextNode(message)); - debugLog.appendChild(p); + this.debugLog.appendChild(p); // Scroll to bottom of div - debugLog.scrollTop = debugLog.scrollHeight; + this.debugLog.scrollTop = this.debugLog.scrollHeight; } + +}()); diff --git a/remoting/webapp/me2mom/manifest.json b/remoting/webapp/me2mom/manifest.json index fd08a87..12a4392 100644 --- a/remoting/webapp/me2mom/manifest.json +++ b/remoting/webapp/me2mom/manifest.json @@ -7,7 +7,6 @@ "local_path": "choice.html" } }, - "background_page": "background.html", "icons": { "128": "chromoting128.png" }, diff --git a/remoting/webapp/me2mom/oauth2.js b/remoting/webapp/me2mom/oauth2.js index a8bf740..c7a0c72 100644 --- a/remoting/webapp/me2mom/oauth2.js +++ b/remoting/webapp/me2mom/oauth2.js @@ -2,161 +2,268 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Declare an OAuth2 class to handle retrieval/storage of an OAuth2 token. -// -// Ideally, this should implement the OAuth2 PostMessage flow to avoid needing -// to copy and paste a code, but that does not support extension URL schemes -// quite yet. Instead, we currently use the native app flow with an -// authorization code that the user must cut/paste. +/** + * @fileoverview + * OAuth2 class that handles retrieval/storage of an OAuth2 token. + * + * Uses a content script to trampoline the OAuth redirect page back into the + * extension context. This works around the lack of native support for + * chrome-extensions in OAuth2. + */ -var remoting = chrome.extension.getBackgroundPage().remoting; +"use strict"; -function OAuth2() { - this.OAUTH2_REFRESH_TOKEN_NAME = 'oauth2_refresh_token'; +var remoting = remoting || {}; - this.client_id = encodeURIComponent( +(function() { +/** @constructor */ +remoting.OAuth2 = function() { +} + +// Constants representing keys used for storing persistent state. +remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; +remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; + +// Constants for parameters used in retrieving the OAuth2 credentials. +remoting.OAuth2.prototype.CLIENT_ID_ = '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + - 'apps.googleusercontent.com'); - this.client_secret = encodeURIComponent('W2ieEsG-R1gIA4MMurGrgMc_'); - this.scope = encodeURIComponent( + 'apps.googleusercontent.com'; +remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; +remoting.OAuth2.prototype.SCOPE_ = 'https://www.googleapis.com/auth/chromoting ' + 'https://www.googleapis.com/auth/googletalk ' + - 'https://www.googleapis.com/auth/userinfo#email'); - this.redirect_uri = encodeURIComponent( + 'https://www.googleapis.com/auth/userinfo#email'; +remoting.OAuth2.prototype.REDIRECT_URI_ = 'https://chromoting-httpxmpp-oauth2-dev.corp.google.com' + - '/oauth2_trampoline'); -} + '/oauth2_trampoline'; +remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = + 'https://accounts.google.com/o/oauth2/token'; -OAuth2.prototype.isAuthenticated = function() { - if(this.getRefreshToken()) { +/** @return {boolean} */ +remoting.OAuth2.prototype.isAuthenticated = function() { + if (this.getRefreshToken()) { return true; } return false; } -OAuth2.prototype.clear = function() { - remoting.removeItem(this.OAUTH2_REFRESH_TOKEN_NAME); - delete this.access_token; - delete this.access_token_expiration; +/** + * Removes all storage, and effectively unauthenticates the user. + * + * @return {void} + */ +remoting.OAuth2.prototype.clear = function() { + window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); + this.clearAccessToken(); } -OAuth2.prototype.setRefreshToken = function(token) { - remoting.setItem(this.OAUTH2_REFRESH_TOKEN_NAME, token); +/** + * @param {string} token The new refresh token. + * @return {void} + */ +remoting.OAuth2.prototype.setRefreshToken = function(token) { + window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); + this.clearAccessToken(); } -OAuth2.prototype.getRefreshToken = function(token) { - return remoting.getItem(this.OAUTH2_REFRESH_TOKEN_NAME); +/** @return {string} */ +remoting.OAuth2.prototype.getRefreshToken = function() { + var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); + if (value) { + return unescape(value); + } + return value; } -OAuth2.prototype.setAccessToken = function(token, expiration) { - this.access_token = token; - // Offset by 30 seconds to account for RTT issues. - // TODO(ajwong): See if this is necessary, or of the protocol already - // accounts for RTT. - this.access_token_expiration = expiration - 30000; +/** + * @param {string} token The new access token. + * @param {number} expiration Expiration time in milliseconds since epoch. + * @return {void} + */ +remoting.OAuth2.prototype.setAccessToken = function(token, expiration) { + var access_token = {'token': token, 'expiration': expiration}; + window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, + JSON.stringify(access_token)); } -OAuth2.prototype.needsNewAccessToken = function() { +/** + * Returns the current access token, setting it to a invalid value if none + * existed before. + * + * @return {{token: string, expiration: number}} + */ +remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { + if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { + // Always be able to return structured data. + this.setAccessToken('', 0); + } + var accessToken = + JSON.parse(window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)); + + if (!('token' in accessToken && 'expiration' in accessToken)) { + console.log("Invalid access token stored."); + return {'token': '', 'expiration': 0}; + } + + return accessToken; +} + +/** + * Returns true if the access token is expired, or otherwise invalid. + * + * Will throw if !isAuthenticated(). + * + * @return {boolean} + */ +remoting.OAuth2.prototype.needsNewAccessToken = function() { if (!this.isAuthenticated()) { throw "Not Authenticated."; } - if (!this.access_token) { + var access_token = this.getAccessTokenInternal_(); + if (!access_token['token']) { return true; } - if (Date.now() > this.access_token_expiration) { + if (Date.now() > access_token['expiration']) { return true; } return false; } -OAuth2.prototype.getAccessToken = function() { +/** + * Returns the current access token. + * + * Will throw if !isAuthenticated() or needsNewAccessToken(). + * + * @return {{token: string, expiration: number}} + */ +remoting.OAuth2.prototype.getAccessToken = function() { if (this.needsNewAccessToken()) { throw "Access Token expired."; } - return this.access_token; + return this.getAccessTokenInternal_()['token']; +} + +/** @return {void} */ +remoting.OAuth2.prototype.clearAccessToken = function() { + window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); +} + +/** + * Update state based on token response from the OAuth2 /token endpoint. + * + * @param {XMLHttpRequest} xhr The XHR object for this request. + * @return {void} + */ +remoting.OAuth2.prototype.processTokenResponse_ = function(xhr) { + if (xhr.status == 200) { + var tokens = JSON.parse(xhr.responseText); + if ('refresh_token' in tokens) { + this.setRefreshToken(tokens['refresh_token']); + } + + // Offset by 30 seconds to account for RTT issues. + // TODO(ajwong): See if this is necessary, or of the protocol already + // accounts for RTT. + this.setAccessToken(tokens['access_token'], + tokens['expires_in'] * 1000 + Date.now() - 30000); + } else { + console.log("Failed to get tokens. Status: " + xhr.status + + " response: " + xhr.responseText); + } } -OAuth2.prototype.refreshAccessToken = function(on_done) { +/** + * Asynchronously retrieves a new access token from the server. + * + * Will throw if !isAuthenticated(). + * + * @param {function(): void} onDone Callback to invoke on completion. + * @return {void} + */ +remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { if (!this.isAuthenticated()) { throw "Not Authenticated."; } - var xhr = new XMLHttpRequest(); - var that = this; - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) { - return; - } - if (xhr.status == 200) { - tokens = JSON.parse(xhr.responseText); - that.setAccessToken(tokens['access_token'], - tokens['expires_in'] * 1000 + Date.now()); - } else { - console.log("Refresh access token failed. Status: " + xhr.status + - " response: " + xhr.responseText); - } - on_done(); + + var parameters = { + 'client_id': this.CLIENT_ID_, + 'client_secret': this.CLIENT_SECRET_, + 'refresh_token': this.getRefreshToken(), + 'grant_type': 'refresh_token' }; - xhr.open('POST', 'https://accounts.google.com/o/oauth2/token', true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - var post_data = 'client_id=' + this.client_id - + '&client_secret=' + this.client_secret - + '&refresh_token=' + encodeURIComponent(this.getRefreshToken()) - + '&grant_type=refresh_token'; - xhr.send(post_data); + + var that = this; + remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, + function(xhr) { + that.processTokenResponse_(xhr); + onDone(); + }, + parameters); } -OAuth2.prototype.openOAuth2Window = function() { - var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' - + 'client_id=' + this.client_id - + '&redirect_uri=' + this.redirect_uri - + '&scope=' + this.scope - + '&response_type=code'; - window.open(GET_CODE_URL); +/** + * Redirect page to get a new OAuth2 Refresh Token. + * + * @return {void} + */ +remoting.OAuth2.prototype.doAuthRedirect = function() { + var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' + + remoting.xhr.urlencodeParamHash({ + 'client_id': this.CLIENT_ID_, + 'redirect_uri': this.REDIRECT_URI_, + 'scope': this.SCOPE_, + 'response_type': 'code' + }); + window.location.replace(GET_CODE_URL); } -OAuth2.prototype.exchangeCodeForToken = function(code, on_done) { - var xhr = new XMLHttpRequest(); - var that = this; - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) { - return; - } - if (xhr.status == 200) { - tokens = JSON.parse(xhr.responseText); - that.setRefreshToken(tokens['refresh_token']); - that.setAccessToken(tokens['access_token'], - tokens['expires_in'] + Date.now()); - } else { - console.log("Code exchnage failed. Status: " + xhr.status + - " response: " + xhr.responseText); - } - on_done(); +/** + * Asynchronously exchanges an authorization code for a refresh token. + * + * @param {string} code The new refresh token. + * @param {function():void} onDone Callback to invoke on completion. + * @return {void} + */ +remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { + var parameters = { + 'client_id': this.CLIENT_ID_, + 'client_secret': this.CLIENT_SECRET_, + 'redirect_uri': this.REDIRECT_URI_, + 'code': code, + 'grant_type': 'authorization_code' }; - xhr.open('POST', 'https://accounts.google.com/o/oauth2/token', true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - var post_data = 'client_id=' + this.client_id - + '&client_secret=' + this.client_secret - + '&redirect_uri=' + this.redirect_uri - + '&code=' + encodeURIComponent(code) - + '&grant_type=authorization_code'; - xhr.send(post_data); + + var that = this; + remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, + function(xhr) { + that.processTokenResponse_(xhr); + onDone(); + }, + parameters); } -// Call myfunc with an access token as the only parameter. -// -// This will refresh the access token if necessary. If the access token -// cannot be refreshed, an error is thrown. -OAuth2.prototype.callWithToken = function(myfunc) { +/** + * Call myfunc with an access token as the only parameter. + * + * This will refresh the access token if necessary. If the access token + * cannot be refreshed, an error is thrown. + * + * @param {function(string):void} myfunc Function to invoke with access token. + * @return {void} + */ +remoting.OAuth2.prototype.callWithToken = function(myfunc) { + var that = this; if (remoting.oauth2.needsNewAccessToken()) { remoting.oauth2.refreshAccessToken(function() { if (remoting.oauth2.needsNewAccessToken()) { // If we still need it, we're going to infinite loop. throw "Unable to get access token."; } - myfunc(remoting.oauth2.getAccessToken()); + myfunc(that.getAccessToken()); }); return; } - myfunc(remoting.oauth2.getAccessToken()); + myfunc(this.getAccessToken()); } +}()); diff --git a/remoting/webapp/me2mom/oauth2_callback.html b/remoting/webapp/me2mom/oauth2_callback.html index 385751b..2b34bbd 100644 --- a/remoting/webapp/me2mom/oauth2_callback.html +++ b/remoting/webapp/me2mom/oauth2_callback.html @@ -8,6 +8,7 @@ found in the LICENSE file. <html> <head> <script src="oauth2.js"></script> + <script src="xhr.js"></script> </head> <body> <div id="error" style="display:none;"> @@ -22,7 +23,7 @@ found in the LICENSE file. queryArgs[pair[0]] = pair[1]; } if ('code' in queryArgs) { - var oauth2 = new OAuth2(); + var oauth2 = new remoting.OAuth2(); oauth2.exchangeCodeForToken(queryArgs['code'], function() { window.location.replace(chrome.extension.getURL('choice.html')); }); diff --git a/remoting/webapp/me2mom/plugin_settings.js b/remoting/webapp/me2mom/plugin_settings.js index e8515ea..a863ef7 100644 --- a/remoting/webapp/me2mom/plugin_settings.js +++ b/remoting/webapp/me2mom/plugin_settings.js @@ -6,5 +6,6 @@ // Keeping all that cenetralized here allows us to use symlinks for the other // files making for a faster compile/run cycle when only modifying HTML/JS. -var remoting = chrome.extension.getBackgroundPage().remoting; +var remoting = remoting || {}; + remoting.PLUGIN_MIMETYPE='HOST_PLUGIN_MIMETYPE'; diff --git a/remoting/webapp/me2mom/remoting.js b/remoting/webapp/me2mom/remoting.js index 75ce831..0508374 100644 --- a/remoting/webapp/me2mom/remoting.js +++ b/remoting/webapp/me2mom/remoting.js @@ -2,20 +2,30 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(jamiewalch): strict mode causes the page to crash so it's disabled for -// now. Reinstate this when the associated bug is fixed. -// http://code.google.com/p/v8/issues/detail?id=1423 -//"use strict"; +var remoting = remoting || {}; + +(function() { +"use strict"; + +/** @enum {string} */ +remoting.AppMode = { + CLIENT: 'client', + HOST: 'host', + IN_SESSION: 'in-session' +}; -// TODO(ajwong): This seems like a bad idea to share the exact same object -// with the background page. Why are we doing it like this? -var remoting = chrome.extension.getBackgroundPage().remoting; -remoting.CLIENT_MODE = 'client'; -remoting.HOST_MODE = 'host'; remoting.EMAIL = 'email'; remoting.HOST_PLUGIN_ID = 'host-plugin-id'; -window.addEventListener('load', init_, false); +/** + * Whether or not the plugin should scale itself. + * @type {boolean} + */ +remoting.scaleToFit = false; + +// Constants representing keys used for storing persistent application state. +var KEY_APP_MODE_ = 'remoting-app-mode'; +var KEY_EMAIL_ = 'remoting-email'; // Some constants for pretty-printing the access code. var kSupportIdLen = 7; @@ -28,11 +38,11 @@ function hasClass(element, cls) { } function retrieveEmail_(access_token) { - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) { - return; - } + var headers = { + 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() + }; + + var onResponse = function(xhr) { if (xhr.status != 200) { // TODO(ajwong): Have a better way of showing an error. window.alert("Unable to get e-mail"); @@ -43,13 +53,13 @@ function retrieveEmail_(access_token) { setEmail(xhr.responseText.split('&')[0].split('=')[1]); }; - xhr.open('GET', 'https://www.googleapis.com/userinfo/email', true); - xhr.setRequestHeader('Authorization', 'OAuth ' + access_token); - xhr.send(null); + // TODO(ajwong): Update to new v2 API. + remoting.xhr.get('https://www.googleapis.com/userinfo/email', + onResponse, '', headers); } function refreshEmail_() { - if (!remoting.getItem(remoting.EMAIL) && remoting.oauth2.isAuthenticated()) { + if (!getEmail() && remoting.oauth2.isAuthenticated()) { remoting.oauth2.callWithToken(retrieveEmail_); } } @@ -66,13 +76,10 @@ function removeClass(element, cls) { function showElement(element, visible) { if (visible) { - if (hasClass(element, 'display-inline')) { - element.style.display = 'inline-block'; - } else { - element.style.display = 'block'; - } + // Reset to default visibility. + removeClass(element, 'hidden'); } else { - element.style.display = 'none'; + addClass(element, 'hidden'); } } @@ -96,33 +103,35 @@ function updateAuthStatus_() { showElementById('oauth2-token-button', !oauthValid); showElementById('oauth2-clear-button', oauthValid); - var loginName = remoting.getItem(remoting.EMAIL); + var loginName = getEmail(); if (loginName) { document.getElementById('current-email').innerText = loginName; } var disableControls = !(loginName && oauthValid); - var authPanel = document.getElementById('auth-panel'); + var controlPanel = document.getElementById('control-panel'); + // TODO(ajwong): Do this via a style, or remove if the new auth flow is + // implemented. if (disableControls) { - authPanel.style.backgroundColor = 'rgba(204, 0, 0, 0.15)'; + controlPanel.style.backgroundColor = 'rgba(204, 0, 0, 0.15)'; } else { - authPanel.style.backgroundColor = 'rgba(0, 204, 102, 0.15)'; + controlPanel.style.backgroundColor = 'rgba(0, 204, 102, 0.15)'; } updateControls_(disableControls); } -function initBackgroundFuncs_() { - remoting.getItem = chrome.extension.getBackgroundPage().getItem; - remoting.setItem = chrome.extension.getBackgroundPage().setItem; - remoting.removeItem = chrome.extension.getBackgroundPage().removeItem; - remoting.oauth2 = new OAuth2(); -} - function setEmail(value) { - remoting.setItem(remoting.EMAIL, value); + window.localStorage.setItem(KEY_EMAIL_, value); updateAuthStatus_(); } +/** + * @return {string} + */ +function getEmail() { + return window.localStorage.getItem(KEY_EMAIL_); +} + function exchangedCodeForToken_() { if (!remoting.oauth2.isAuthenticated()) { alert('Your OAuth2 token was invalid. Please try again.'); @@ -133,7 +142,7 @@ function exchangedCodeForToken_() { }); } -function clearOAuth2() { +remoting.clearOAuth2 = function() { remoting.oauth2.clear(); updateAuthStatus_(); } @@ -145,55 +154,87 @@ function setMode_(mode, modes) { } } -function init_() { - initBackgroundFuncs_(); +remoting.toggleDebugLog = function() { + var debugLog = document.getElementById('debug-log'); + var toggleButton = document.getElementById('debug-log-toggle'); + + if (!debugLog.style.display || debugLog.style.display == 'none') { + debugLog.style.display = 'block'; + toggleButton.value = 'Hide Debug Log'; + } else { + debugLog.style.display = 'none'; + toggleButton.value = 'Show Debug Log'; + } +} + +remoting.init = function() { + // Create global objects. + remoting.oauth2 = new remoting.OAuth2(); + remoting.debug = new remoting.DebugLog(document.getElementById('debug-log')); + updateAuthStatus_(); refreshEmail_(); setHostMode('unshared'); setClientMode('unconnected'); - setGlobalMode(remoting.getItem('startup-mode', remoting.HOST_MODE)); - addClass(document.getElementById('loading-panel'), 'hidden'); - removeClass(document.getElementById('main-panel'), 'hidden'); + setGlobalMode(getAppStartupMode()); + addClass(document.getElementById('loading-mode'), 'hidden'); + removeClass(document.getElementById('choice-mode'), 'hidden'); } function setGlobalMode(mode) { var elementsToShow = []; var elementsToHide = []; var hostElements = document.getElementsByClassName('host-element'); + hostElements = Array.prototype.slice.apply(hostElements); var clientElements = document.getElementsByClassName('client-element'); - if (mode == remoting.HOST_MODE) { - elementsToShow = hostElements; - elementsToHide = clientElements; - } else { - elementsToShow = clientElements; - elementsToHide = hostElements; - } - for (var i = 0; i < elementsToShow.length; ++i) { - showElement(elementsToShow[i], true); + clientElements = Array.prototype.slice.apply(clientElements); + var inSessionElements = + document.getElementsByClassName('in-session-element'); + inSessionElements = Array.prototype.slice.apply(inSessionElements); + if (mode == remoting.AppMode.HOST) { + elementsToShow = elementsToShow.concat(hostElements); + elementsToHide = elementsToHide.concat(clientElements, inSessionElements); + } else if (mode == remoting.AppMode.CLIENT) { + elementsToShow = elementsToShow.concat(clientElements); + elementsToHide = elementsToHide.concat(hostElements, inSessionElements); + } else if (mode == remoting.AppMode.IN_SESSION) { + elementsToShow = elementsToShow.concat(inSessionElements); + elementsToHide = elementsToHide.concat(hostElements, clientElements); } + + // Hide first and then show since an element may be in both lists. for (var i = 0; i < elementsToHide.length; ++i) { showElement(elementsToHide[i], false); } + for (var i = 0; i < elementsToShow.length; ++i) { + showElement(elementsToShow[i], true); + } showElement(document.getElementById('waiting-footer', false)); remoting.currentMode = mode; } function setGlobalModePersistent(mode) { setGlobalMode(mode); - remoting.setItem('startup-mode', mode); + // TODO(ajwong): Does it make sense for "in_session" to be a peer to "host" + // or "client mode"? I don't think so, but not sure how to restructure UI. + if (mode != remoting.AppMode.IN_SESSION) { + remoting.storage.setStartupMode(mode); + } else { + remoting.storage.setStartupMode(remoting.AppMode.CLIENT); + } } function setHostMode(mode) { - var section = document.getElementById('host-section'); + var section = document.getElementById('host-panel'); var modes = section.getElementsByClassName('mode'); - addToDebugLog('Host mode: ' + mode); + remoting.debug.log('Host mode: ' + mode); setMode_(mode, modes); } function setClientMode(mode) { - var section = document.getElementById('client-section'); + var section = document.getElementById('client-panel'); var modes = section.getElementsByClassName('mode'); - addToDebugLog('Client mode: ' + mode); + remoting.debug.log('Client mode: ' + mode); setMode_(mode, modes); } @@ -205,9 +246,9 @@ function showWaiting_() { } function tryShare() { - addToDebugLog('Attempting to share...'); + remoting.debug.log('Attempting to share...'); if (remoting.oauth2.needsNewAccessToken()) { - addToDebugLog('Refreshing token...'); + remoting.debug.log('Refreshing token...'); remoting.oauth2.refreshAccessToken(function() { if (remoting.oauth2.needsNewAccessToken()) { // If we still need it, we're going to infinite loop. @@ -221,7 +262,7 @@ function tryShare() { showWaiting_(); - var div = document.getElementById('plugin-wrapper'); + var div = document.getElementById('host-plugin-container'); var plugin = document.createElement('embed'); plugin.setAttribute('type', remoting.PLUGIN_MIMETYPE); plugin.setAttribute('hidden', 'true'); @@ -229,9 +270,10 @@ function tryShare() { div.appendChild(plugin); plugin.onStateChanged = onStateChanged_; plugin.logDebugInfoCallback = debugInfoCallback_; - plugin.connect(remoting.getItem(remoting.EMAIL), + plugin.connect(getEmail(), 'oauth2:' + remoting.oauth2.getAccessToken()); } +remoting.tryShare = tryShare; function onStateChanged_() { var plugin = document.getElementById(remoting.HOST_PLUGIN_ID); @@ -253,11 +295,11 @@ function onStateChanged_() { } else if (state == plugin.CONNECTED) { setHostMode('shared'); } else if (state == plugin.DISCONNECTED) { - setGlobalMode(remoting.HOST_MODE); + setGlobalMode(remoting.AppMode.HOST); setHostMode('unshared'); plugin.parentNode.removeChild(plugin); } else { - addToDebugLog('Unknown state -> ' + state); + remoting.debug.log('Unknown state -> ' + state); } } @@ -266,26 +308,102 @@ function onStateChanged_() { * is additional debug log info to display. */ function debugInfoCallback_(msg) { - addToDebugLog('plugin: ' + msg); + remoting.debug.log('plugin: ' + msg); } function showShareError_(errorCode) { var errorDiv = document.getElementById(errorCode); errorDiv.style.display = 'block'; - addToDebugLog("Sharing error: " + errorCode); + remoting.debug.log("Sharing error: " + errorCode); setHostMode('share-failed'); } function cancelShare() { - addToDebugLog('Canceling share...'); + remoting.debug.log('Canceling share...'); var plugin = document.getElementById(remoting.HOST_PLUGIN_ID); plugin.disconnect(); } +remoting.cancelShare = cancelShare; + +/** + * Show a client message that stays on the screeen until the state changes. + * + * @param {string} message The message to display. + */ +function setClientStateMessage(message) { + var msg = document.getElementById('session-status-message'); + msg.innerText = message; +} + +function updateStatusBarStats() { + if (remoting.session.state != remoting.ClientSession.State.CONNECTED) + return; + var stats = remoting.session.stats(); + + var units = ''; + var videoBandwidth = stats['video_bandwidth']; + if (videoBandwidth < 1024) { + units = 'Bps'; + } else if (videoBandwidth < 1048576) { + units = 'KiBps'; + videoBandwidth = videoBandwidth / 1024; + } else if (videoBandwidth < 1073741824) { + units = 'MiBps'; + videoBandwidth = videoBandwidth / 1048576; + } else { + units = 'GiBps'; + videoBandwidth = videoBandwidth / 1073741824; + } + + setClientStateMessage( + 'Bandwidth: ' + videoBandwidth.toFixed(2) + units + + ', Capture: ' + stats['capture_latency'].toFixed(2) + 'ms' + + ', Encode: ' + stats['encode_latency'].toFixed(2) + 'ms' + + ', Decode: ' + stats['decode_latency'].toFixed(2) + 'ms' + + ', Render: ' + stats['render_latency'].toFixed(2) + 'ms' + + ', Latency: ' + stats['roundtrip_latency'].toFixed(2) + 'ms'); + + // Update the stats once per second. + window.setTimeout(updateStatusBarStats, 1000); +} + +function onClientStateChange_(state) { + if (state == remoting.ClientSession.State.UNKNOWN) { + setClientStateMessage('Unknown'); + } else if (state == remoting.ClientSession.State.CREATED) { + setClientStateMessage('Created'); + } else if (state == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { + setClientStateMessage('Incompatible Plugin Version'); + } else if (state == remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR) { + setClientStateMessage('Unknown error with plugin.'); + } else if (state == remoting.ClientSession.State.CONNECTING) { + setClientStateMessage('Connecting as ' + remoting.username); + } else if (state == remoting.ClientSession.State.INITIALIZING) { + setClientStateMessage('Initializing connection'); + } else if (state == remoting.ClientSession.State.CONNECTED) { + updateStatusBarStats(); + } else if (state == remoting.ClientSession.State.CLOSED) { + setClientStateMessage('Closed'); + } else if (state == remoting.ClientSession.State.CONNECTION_FAILED) { + setClientStateMessage('Failed'); + } else { + setClientStateMessage('Bad State!!'); + } +} function startSession_() { - addToDebugLog('Starting session...'); - remoting.username = remoting.getItem(remoting.EMAIL); - document.location = 'remoting_session.html'; + remoting.debug.log('Starting session...'); + remoting.username = getEmail(); + setGlobalMode(remoting.AppMode.IN_SESSION); + remoting.session = + new remoting.ClientSession(remoting.hostJid, remoting.hostPublicKey, + remoting.accessCode, getEmail(), + onClientStateChange_); + remoting.oauth2.callWithToken(function(token) { + remoting.session.createPluginAndConnect( + document.getElementById('session-mode'), + token); + }); } function showConnectError_(responseCode, responseString) { @@ -324,21 +442,16 @@ function normalizeAccessCode_(accessCode) { } function resolveSupportId(supportId) { - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) { - return; - } - parseServerResponse_(xhr); - }; + var headers = { + 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() + }; - xhr.open('GET', - 'https://www.googleapis.com/chromoting/v1/support-hosts/' + - encodeURIComponent(supportId), - true); - xhr.setRequestHeader('Authorization', - 'OAuth ' + remoting.oauth2.getAccessToken()); - xhr.send(null); + remoting.xhr.get( + 'https://www.googleapis.com/chromoting/v1/support-hosts/' + + encodeURIComponent(supportId), + parseServerResponse_, + '', + headers); } function tryConnect() { @@ -364,10 +477,44 @@ function tryConnect() { resolveSupportId(supportId); } } +remoting.tryConnect = tryConnect; function cancelPendingOperation() { document.getElementById('cancel-button').disabled = true; - if (remoting.currentMode == remoting.HOST_MODE) { + if (remoting.currentMode == remoting.AppMode.HOST) { cancelShare(); } } + +/** + * Changes the major-mode of the application (Eg., client or host). + * + * @param {remoting.AppMode} mode The mode to shift the application into. + * @return {void} + */ +remoting.setAppMode = function(mode) { + setGlobalMode(mode); + window.localStorage.setItem(KEY_APP_MODE_, mode); +} + +/** + * Gets the major-mode that this application should start up in. + * + * @return {remoting.AppMode} + */ +function getAppStartupMode() { + var mode = window.localStorage.getItem(KEY_APP_MODE_); + if (!mode) { + mode = remoting.AppMode.HOST; + } + return mode; +} + +remoting.toggleScaleToFit = function() { + remoting.scaleToFit = !remoting.scaleToFit; + document.getElementById('scale-to-fit-toggle').value = + remoting.scaleToFit ? 'No scaling' : 'Scale to fit'; + remoting.session.toggleScaleToFit(remoting.scaleToFit); +} + +}()); diff --git a/remoting/webapp/me2mom/remoting_session.css b/remoting/webapp/me2mom/remoting_session.css deleted file mode 100644 index cc33d240..0000000 --- a/remoting/webapp/me2mom/remoting_session.css +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright (c) 2011 The Chromium Authors. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -/* Elements */ -body { - margin: 0; - overflow: auto; - padding: 0; - -webkit-user-select: none; -} - -/* Ids */ -#plugin-scroll-panel { - overflow: auto; - width: 100%; - -webkit-user-select: none; -} - -#session-buttons { - float: right; -} - -#session-controls { - background-color: white; - border-bottom: solid 1px black; - height: 1.5em; - left: 0; - opacity: 0.85; - position: fixed; - top: 0; - width: 100%; - z-index: 100; -} - -#session-controls-padding { - /* - Maintains space for session-controls so that the display initially appears - lower than the controls, since session-controls is position:fixed. - */ - height: 1.5em; -} - -#status-msg { - /* - clear: both; forces the session-controls div to size appropriately for - session-buttons. - */ - clear: both; - font-family: monospace; - font-size: small; - margin: 5px; -} diff --git a/remoting/webapp/me2mom/remoting_session.html b/remoting/webapp/me2mom/remoting_session.html deleted file mode 100644 index e1e3eed..0000000 --- a/remoting/webapp/me2mom/remoting_session.html +++ /dev/null @@ -1,38 +0,0 @@ -<!doctype html> -<!-- -Copyright (c) 2011 The Chromium Authors. All rights reserved. -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. ---> - -<html> - <head> - <title>Chromoting Session</title> - <link rel="shortcut icon" href="chromoting128.png" /> - <link rel="stylesheet" href="debug_log.css" /> - <link rel="stylesheet" href="main.css" /> - <link rel="stylesheet" href="remoting_session.css" /> - <script src="debug_log.js"></script> - <script src="oauth2.js"></script> - <script src="remoting_session.js"></script> - </head> - <body id="session-body"> - <div id="session-controls"> - <form id="session-buttons"> - <input id="scale-to-fit-toggle" type="button" value="Scale to fit" - onclick="toggleScaleToFit();"/> - <input id="debug-log-toggle" type="button" value="Show Debug Log" - onclick="toggleDebugLog(); return false;"/> - </form> - <span id="status-msg">Initializing...</span> - </div> - <div id="session-controls-padding"> - </div> - <div id="plugin-scroll-panel"> - <embed name="remoting" id="remoting" - src="about://none" type="pepper-application/x-chromoting"> - </div> - <div id="debug-log"> - </div> - </body> -</html> diff --git a/remoting/webapp/me2mom/remoting_session.js b/remoting/webapp/me2mom/remoting_session.js deleted file mode 100644 index 556d869..0000000 --- a/remoting/webapp/me2mom/remoting_session.js +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -var remoting = chrome.extension.getBackgroundPage().remoting; - -// Chromoting session API version (for this javascript). -// This is compared with the plugin API version to verify that they are -// compatible. -remoting.apiVersion = 2; - -// The oldest API version that we support. -// This will differ from the |apiVersion| if we maintain backward -// compatibility with older API versions. -remoting.apiMinVersion = 1; - -// Message id so that we can identify (and ignore) message fade operations for -// old messages. This starts at 1 and is incremented for each new message. -remoting.messageId = 1; - -remoting.scaleToFit = false; - -remoting.httpXmppProxy = - 'https://chromoting-httpxmpp-oauth2-dev.corp.google.com'; - -window.addEventListener("load", init_, false); - -// This executes a poll loop on the server for more Iq packets, and feeds them -// to the plugin. -function feedIq() { - var xhr = new XMLHttpRequest(); - xhr.open('GET', remoting.httpXmppProxy + '/readIq?host_jid=' + - encodeURIComponent(remoting.hostJid), true); - xhr.withCredentials = true; - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - addToDebugLog('Receiving Iq: --' + xhr.responseText + '--'); - remoting.plugin.onIq(xhr.responseText); - } - if (xhr.status == 200 || xhr.status == 204) { - window.setTimeout(feedIq, 0); - } else { - addToDebugLog('HttpXmpp gateway returned code: ' + xhr.status); - remoting.plugin.disconnect(); - setClientStateMessage('Failed'); - } - } - } - xhr.send(null); -} - -function registerConnection() { - var xhr = new XMLHttpRequest(); - xhr.open('POST', remoting.httpXmppProxy + '/newConnection', true); - xhr.withCredentials = true; - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - addToDebugLog('Receiving Iq: --' + xhr.responseText + '--'); - var clientjid = xhr.responseText; - - remoting.plugin.sendIq = sendIq; - if (remoting.plugin.apiVersion >= 2) { - remoting.plugin.connect(remoting.hostJid, remoting.hostPublicKey, - clientjid, remoting.accessCode); - } else { - remoting.plugin.connect(remoting.hostJid, clientjid, - remoting.accessCode); - } - // TODO(ajwong): This should just be feedIq(); - window.setTimeout(feedIq, 1000); - } else { - addToDebugLog('FailedToConnect: --' + xhr.responseText + - '-- (status=' + xhr.status + ')'); - setClientStateMessage('Failed'); - } - } - } - xhr.send('host_jid=' + encodeURIComponent(remoting.hostJid) + - '&username=' + encodeURIComponent(remoting.username) + - '&password=' + encodeURIComponent(remoting.oauth2.getAccessToken())); - setClientStateMessage('Connecting'); -} - -function sendIq(msg) { - addToDebugLog('Sending Iq: ' + msg); - - // Extract the top level fields of the Iq packet. - // TODO(ajwong): Can the plugin just return these fields broken out. - parser = new DOMParser(); - iqNode = parser.parseFromString(msg, 'text/xml').firstChild; - id = iqNode.getAttribute('id'); - type = iqNode.getAttribute('type'); - to = iqNode.getAttribute('to'); - serializer = new XMLSerializer(); - payload_xml = serializer.serializeToString(iqNode.firstChild); - - var xhr = new XMLHttpRequest(); - xhr.open('POST', remoting.httpXmppProxy + '/sendIq', true); - xhr.withCredentials = true; - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.send('to=' + encodeURIComponent(to) + - '&payload_xml=' + encodeURIComponent(payload_xml) + - '&id=' + id + '&type=' + type + - '&host_jid=' + encodeURIComponent(remoting.hostJid)); -} - -function checkVersion(plugin) { - return remoting.apiVersion >= plugin.apiMinVersion && - plugin.apiVersion >= remoting.apiMinVersion; -} - -function init_() { - // Kick off the connection. - var plugin = document.getElementById('remoting'); - - remoting.plugin = plugin; - - // Only allow https connections to the httpXmppProxy. - var regExp = /^ *https:\/\//; - if (remoting.httpXmppProxy.search(regExp) == -1) { - addToDebugLog('Aborting. httpXmppProxy does not specify https protocol: ' + - remoting.httpXmppProxy); - return; - } - - // Setup the callback that the plugin will call when the connection status - // has changes and the UI needs to be updated. It needs to be an object with - // a 'callback' property that contains the callback function. - plugin.connectionInfoUpdate = connectionInfoUpdateCallback; - plugin.debugInfo = debugInfoCallback; - plugin.desktopSizeUpdate = desktopSizeChanged; - plugin.loginChallenge = loginChallengeCallback; - - if (typeof plugin.connect !== 'function') { - setClientStateMessage("Unable to load plugin. Please make sure that " + - "'Remoting' and 'P2P API' are enabled in chrome://flags."); - return; - } - - if (!checkVersion(plugin)) { - // TODO(garykac): We need better messaging here. Perhaps an install link. - setClientStateMessage("Out of date. Please re-install."); - return; - } - - addToDebugLog('Connect as user ' + remoting.username); - registerConnection(); -} - -function toggleScaleToFit() { - remoting.scaleToFit = !remoting.scaleToFit; - document.getElementById('scale-to-fit-toggle').value = - remoting.scaleToFit ? 'No scaling' : 'Scale to fit'; - remoting.plugin.setScaleToFit(remoting.scaleToFit); -} - -/** - * This is the callback method that the plugin calls to request username and - * password for logging into the remote host. For Me2Mom we are pre-authorized - * so this is a no-op. - */ -function loginChallengeCallback() { -} - -/** - * This is a callback that gets called when the desktop size contained in the - * the plugin has changed. - */ -function desktopSizeChanged() { - var width = remoting.plugin.desktopWidth; - var height = remoting.plugin.desktopHeight; - - addToDebugLog('desktop size changed: ' + width + 'x' + height); - remoting.plugin.style.width = width + 'px'; - remoting.plugin.style.height = height + 'px'; -} - -/** - * This is that callback that the plugin invokes to indicate that the - * host/client connection status has changed. - */ -function connectionInfoUpdateCallback() { - var status = remoting.plugin.status; - var quality = remoting.plugin.quality; - - if (status == remoting.plugin.STATUS_UNKNOWN) { - setClientStateMessage(''); - } else if (status == remoting.plugin.STATUS_CONNECTING) { - setClientStateMessage('Connecting as ' + remoting.username); - } else if (status == remoting.plugin.STATUS_INITIALIZING) { - setClientStateMessageFade('Initializing connection'); - } else if (status == remoting.plugin.STATUS_CONNECTED) { - desktopSizeChanged(); - setClientStateMessageFade('Connected', 1000); - window.setTimeout(updateStatusBarStats, 1000); - } else if (status == remoting.plugin.STATUS_CLOSED) { - setClientStateMessage('Closed'); - } else if (status == remoting.plugin.STATUS_FAILED) { - setClientStateMessage('Failed'); - } -} - -/** - * Show a client message that stays on the screeen until the state changes. - * - * @param {string} message The message to display. - */ -function setClientStateMessage(message) { - // Increment message id to ignore any previous fadeout requests. - remoting.messageId++; - - // Update the status message. - var msg = document.getElementById('status-msg'); - msg.innerText = message; - msg.style.opacity = 1; - msg.style.display = ''; -} - -/** - * Show a client message for the specified amount of time. - * - * @param {string} message The message to display. - * @param {number} duration Milliseconds to show message before fading. - */ -function setClientStateMessageFade(message, duration) { - setClientStateMessage(message); - - // Set message duration. - window.setTimeout("fade('status-msg', " + remoting.messageId + ', ' + - '100, 10, 200)', - duration); -} - -/** - * Fade the specified element. - * For example, to have element 'foo' fade away over 2 seconds, you could use - * either: - * fade('foo', 100, 10, 200) - * - Start at 100%, decrease by 10% each time, wait 200ms between updates. - * fade('foo', 100, 5, 100) - * - Start at 100%, decrease by 5% each time, wait 100ms between updates. - * - * @param {string} name Name of element to fade. - * @param {number} id The id of the message associated with this fade request. - * @param {number} val The new opacity value (0-100) for this element. - * @param {number} delta Amount to adjust the opacity each iteration. - * @param {number} delay Delay (in ms) to wait between each update. - */ -function fade(name, id, val, delta, delay) { - // Ignore the fade call if it does not apply to the current message. - if (id != remoting.messageId) { - return; - } - - var e = document.getElementById(name); - if (e) { - var newVal = val - delta; - if (newVal > 0) { - // Decrease opacity and set timer for next fade event. - e.style.opacity = newVal / 100; - window.setTimeout("fade('status-msg', " + id + ', ' + newVal + ', ' + - delta + ', ' + delay + ')', - delay); - } else { - // Completely hide the text and stop fading. - e.style.opacity = 0; - e.style.display = 'none'; - } - } -} - -/** - * This is that callback that the plugin invokes to indicate that there - * is additional debug log info to display. - */ -function debugInfoCallback(msg) { - addToDebugLog('plugin: ' + msg); -} - -function updateStatusBarStats() { - if (remoting.plugin.status != remoting.plugin.STATUS_CONNECTED) - return; - var videoBandwidth = remoting.plugin.videoBandwidth; - var videoCaptureLatency = remoting.plugin.videoCaptureLatency; - var videoEncodeLatency = remoting.plugin.videoEncodeLatency; - var videoDecodeLatency = remoting.plugin.videoDecodeLatency; - var videoRenderLatency = remoting.plugin.videoRenderLatency; - var roundTripLatency = remoting.plugin.roundTripLatency; - - var units = ''; - if (videoBandwidth < 1024) { - units = 'Bps'; - } else if (videoBandwidth < 1048576) { - units = 'KiBps'; - videoBandwidth = videoBandwidth / 1024; - } else if (videoBandwidth < 1073741824) { - units = 'MiBps'; - videoBandwidth = videoBandwidth / 1048576; - } else { - units = 'GiBps'; - videoBandwidth = videoBandwidth / 1073741824; - } - - setClientStateMessage( - 'Bandwidth: ' + videoBandwidth.toFixed(2) + units + - ', Capture: ' + videoCaptureLatency.toFixed(2) + 'ms' + - ', Encode: ' + videoEncodeLatency.toFixed(2) + 'ms' + - ', Decode: ' + videoDecodeLatency.toFixed(2) + 'ms' + - ', Render: ' + videoRenderLatency.toFixed(2) + 'ms' + - ', Latency: ' + roundTripLatency.toFixed(2) + 'ms'); - - // Update the stats once per second. - window.setTimeout('updateStatusBarStats()', 1000); -} diff --git a/remoting/webapp/me2mom/storage.js b/remoting/webapp/me2mom/storage.js deleted file mode 100644 index 3837d36..0000000 --- a/remoting/webapp/me2mom/storage.js +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -var remoting = remoting || {}; - -(function() { -"use strict"; - -/** - * Creates a storage object abstracting our storage layer. - * - * This class provide accessors/mutators for all stateful items that are - * persisted by the webapp. This ensures that there are no collisions in - * the naming between different modules. - * - * Note that not all data items are stored with the same lifetime. - * - * @constructor - */ -remoting.Storage = function() { - /** - * Key name for storing the OAuth2 refresh token. - * - * @const - * @type {string} - */ - this.KEY_OAUTH2_REFRESH_TOKEN_ = 'storage-oauth2-refresh-token'; - - /** - * Key name for storing the OAuth2 access token. - * - * @const - * @type {string} - */ - this.KEY_OAUTH2_ACCESS_TOKEN_ = 'storage-oauth2-access-token'; - - /** - * Key name for storing the user's e-mail address. - * - * @const - * @type {string} - */ - this.KEY_EMAIL_ = 'storage-email'; -} - -/** - * @return {string} The refresh token. - */ -remoting.Storage.prototype.getOAuth2RefreshToken = function() { - return unescape(window.localStorage.getItem(this.KEY_OAUTH2_REFRESH_TOKEN_)); -} - -/** - * @param token {string} The new refresh token. - * @return {void} - */ -remoting.Storage.prototype.setOAuth2RefreshToken = function(token) { - window.localStorage.setItem(this.KEY_OAUTH2_REFRESH_TOKEN_, escape(token)); -} - -/** - * @return {{access_token: string, expires_in: number}} The access token. - */ -remoting.Storage.prototype.getOAuth2AccessToken = function() { - return JSON.parse( - window.localStorage.getItem(this.KEY_OAUTH2_ACCESS_TOKEN_)); -} - -/** - * @param token {string} The new access token. - * @param expires {number} When token expires in seconds since epoch. - * @return {void} - */ -remoting.Storage.prototype.setOAuth2AccessToken = function(token, expires) { - var access_token = {'access_token': token, 'expires_in': expires} - window.localStorage.setItem(this.KEY_OAUTH2_ACCESS_TOKEN_, - JSON.stringify(access_token)); -} - -/** - * @return {string} The user's e-mail. - */ -remoting.Storage.prototype.getEmail = function() { - return unescape(window.localStorage.getItem(this.KEY_EMAIL_)); -} - -/** - * @param email {string} The user's email. - * @return {void} - */ -remoting.Storage.prototype.setEmail = function(email) { - window.localStorage.setItem(this.KEY_EMAIL_, escape(email)); -} - -}()); diff --git a/remoting/webapp/me2mom/xhr.js b/remoting/webapp/me2mom/xhr.js new file mode 100644 index 0000000..26eb01e --- /dev/null +++ b/remoting/webapp/me2mom/xhr.js @@ -0,0 +1,154 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview + * Simple utilities for makeing XHRs more pleasant. + */ + +"use strict"; + +var remoting = remoting || {}; +remoting.xhr = remoting.xhr || {}; + +(function() { + +/** + * Takes an associative array of parameters and urlencodes it. + * + * @param {Object.<string>} paramHash The parameter key/value pairs. + * @return {string} URLEncoded version of paramHash. + */ +remoting.xhr.urlencodeParamHash = function(paramHash) { + var paramArray = []; + for (var key in paramHash) { + paramArray.push(encodeURIComponent(key) + + '=' + encodeURIComponent(paramHash[key])); + } + if (paramArray.length > 0) { + return paramArray.join('&'); + } + return ''; +} + +/** + * Execute an XHR GET asynchronously. + * + * @param {string} url The base URL to GET, excluding parameters. + * @param {function(XMLHttpRequest):void} onDone The function to call on + * completion. + * @param {(string|Object.<string>)} opt_parameters The request parameters, + * either as an associative array, or a string. If it is a string, do + * not include the ? and be sure it is correctly URLEncoded. + * @param {Object.<string>} opt_headers Additional headers to include on the + * request. + * @param {boolean} opt_withCredentials Set the withCredentials flags in the + * XHR. + * @return {void} + */ +remoting.xhr.get = function(url, onDone, opt_parameters, opt_headers, + opt_withCredentials) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) { + return; + } + onDone(xhr); + }; + + // Add parameters into URL. + if (typeof(opt_parameters) === 'string') { + if (opt_parameters.length > 0) { + url = url + '?' + opt_parameters + } + } else if (typeof(opt_parameters) === 'object') { + var paramString = remoting.xhr.urlencodeParamHash(opt_parameters); + if (paramString.length > 0) { + url = url + '?' + paramString; + } + } else if (opt_parameters === undefined) { + // No problem here. Do nothing. + } else { + throw "opt_parameters must be string or associated array."; + } + + xhr.open('GET', url, true); + + // Add in request headers. + if (typeof(opt_headers) === 'object') { + for (var key in opt_headers) { + xhr.setRequestHeader(key, opt_headers[key]); + } + } else if (opt_headers === undefined) { + // No problem here. Do nothing. + } else { + throw "opt_headers must be associative array."; + } + + if (opt_withCredentials) { + xhr.withCredentials = true; + } + + xhr.send(null); +} + +/** + * Execute an XHR POST asynchronously. + * + * @param {string} url The base URL to POST, excluding parameters. + * @param {function(XMLHttpRequest):void} onDone The function to call on + * completion. + * @param {(string|Object.<string>)} opt_parameters The request parameters, + * either as an associative array, or a string. If it is a string, be + * sure it is correctly URLEncoded. + * @param {Object.<string>} opt_headers Additional headers to include on the + * request. + * @param {boolean} opt_withCredentials Set the withCredentials flags in the + * XHR. + * @return {void} + */ +remoting.xhr.post = function(url, onDone, opt_parameters, opt_headers, + opt_withCredentials) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) { + return; + } + onDone(xhr); + }; + + // Add parameters into URL. + var postData = ''; + if (typeof(opt_parameters) === 'string') { + postData = opt_parameters; + } else if (typeof(opt_parameters) === 'object') { + postData = remoting.xhr.urlencodeParamHash(opt_parameters); + } else if (opt_parameters === undefined) { + // No problem here. Do nothing. + } else { + throw "opt_parameters must be string or associated array."; + } + + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + // Add in request headers. + if (typeof(opt_headers) === 'object') { + for (var key in opt_headers) { + xhr.setRequestHeader(key, opt_headers[key]); + } + } else if (opt_headers === undefined) { + // No problem here. Do nothing. + } else { + throw "opt_headers must be associative array."; + } + + if (opt_withCredentials) { + xhr.withCredentials = true; + } + + xhr.send(postData); +} + +}()); |