summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/resources/gaia_auth
diff options
context:
space:
mode:
authorJocelyn Turcotte <jocelyn.turcotte@digia.com>2014-08-08 14:30:41 +0200
committerJocelyn Turcotte <jocelyn.turcotte@digia.com>2014-08-12 13:49:54 +0200
commitab0a50979b9eb4dfa3320eff7e187e41efedf7a9 (patch)
tree498dfb8a97ff3361a9f7486863a52bb4e26bb898 /chromium/chrome/browser/resources/gaia_auth
parent4ce69f7403811819800e7c5ae1318b2647e778d1 (diff)
downloadqtwebengine-chromium-ab0a50979b9eb4dfa3320eff7e187e41efedf7a9.tar.gz
Update Chromium to beta version 37.0.2062.68
Change-Id: I188e3b5aff1bec75566014291b654eb19f5bc8ca Reviewed-by: Andras Becsi <andras.becsi@digia.com>
Diffstat (limited to 'chromium/chrome/browser/resources/gaia_auth')
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/OWNERS1
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/background.js330
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/inline_injected.js46
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/inline_main.html13
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/main.html4
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/main.js393
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/manifest_inline.json32
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/manifest_saml.json1
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/offline.css18
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/saml_injected.js76
-rw-r--r--chromium/chrome/browser/resources/gaia_auth/util.js21
11 files changed, 623 insertions, 312 deletions
diff --git a/chromium/chrome/browser/resources/gaia_auth/OWNERS b/chromium/chrome/browser/resources/gaia_auth/OWNERS
index cac125975e2..932dfbf8be8 100644
--- a/chromium/chrome/browser/resources/gaia_auth/OWNERS
+++ b/chromium/chrome/browser/resources/gaia_auth/OWNERS
@@ -1,3 +1,4 @@
nkostylev@chromium.org
xiyuan@chromium.org
zelidrag@chromium.org
+guohui@chromium.org \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/gaia_auth/background.js b/chromium/chrome/browser/resources/gaia_auth/background.js
index 96f9e39c858..3baafc2c332 100644
--- a/chromium/chrome/browser/resources/gaia_auth/background.js
+++ b/chromium/chrome/browser/resources/gaia_auth/background.js
@@ -4,90 +4,182 @@
/**
* @fileoverview
- * The background script of auth extension that bridges the communications
- * between main and injected script.
- * Here are the communications along a SAML sign-in flow:
- * 1. Main script sends an 'onAuthStarted' signal to indicate the authentication
- * flow is started and SAML pages might be loaded from now on;
- * 2. After the 'onAuthTstarted' signal, injected script starts to scraping
- * all password fields on normal page (i.e. http or https) and sends page
- * load signal as well as the passwords to the background script here;
+ * A background script of the auth extension that bridges the communication
+ * between the main and injected scripts.
+ *
+ * Here is an overview of the communication flow when SAML is being used:
+ * 1. The main script sends the |startAuth| signal to this background script,
+ * indicating that the authentication flow has started and SAML pages may be
+ * loaded from now on.
+ * 2. A script is injected into each SAML page. The injected script sends three
+ * main types of messages to this background script:
+ * a) A |pageLoaded| message is sent when the page has been loaded. This is
+ * forwarded to the main script as |onAuthPageLoaded|.
+ * b) If the SAML provider supports the credential passing API, the API calls
+ * are sent to this background script as |apiCall| messages. These
+ * messages are forwarded unmodified to the main script.
+ * c) The injected script scrapes passwords. They are sent to this background
+ * script in |updatePassword| messages. The main script can request a list
+ * of the scraped passwords by sending the |getScrapedPasswords| message.
*/
/**
- * BackgroundBridge holds the main script's state and the scraped passwords
- * from the injected script to help the two collaborate.
+ * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by
+ * the associated tab id.
*/
-function BackgroundBridge() {
+function BackgroundBridgeManager() {
}
-BackgroundBridge.prototype = {
- // Gaia URL base that is set from main auth script.
- gaiaUrl_: null,
-
- // Whether auth flow has started. It is used as a signal of whether the
- // injected script should scrape passwords.
- authStarted_: false,
-
- passwordStore_: {},
-
- channelMain_: null,
- channelInjected_: null,
+BackgroundBridgeManager.prototype = {
+ // Maps a tab id to its associated BackgroundBridge.
+ bridges_: {},
run: function() {
chrome.runtime.onConnect.addListener(this.onConnect_.bind(this));
- // Workarounds for loading SAML page in an iframe.
+ chrome.webRequest.onBeforeRequest.addListener(
+ function(details) {
+ if (this.bridges_[details.tabId])
+ return this.bridges_[details.tabId].onInsecureRequest(details.url);
+ }.bind(this),
+ {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
+ ['blocking']);
+
+ chrome.webRequest.onBeforeSendHeaders.addListener(
+ function(details) {
+ if (this.bridges_[details.tabId])
+ return this.bridges_[details.tabId].onBeforeSendHeaders(details);
+ else
+ return {requestHeaders: details.requestHeaders};
+ }.bind(this),
+ {urls: ['*://*/*'], types: ['sub_frame']},
+ ['blocking', 'requestHeaders']);
+
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
- if (!this.authStarted_)
- return;
+ if (this.bridges_[details.tabId])
+ this.bridges_[details.tabId].onHeadersReceived(details);
+ }.bind(this),
+ {urls: ['*://*/*'], types: ['sub_frame']},
+ ['responseHeaders']);
- var headers = details.responseHeaders;
- for (var i = 0; headers && i < headers.length; ++i) {
- if (headers[i].name.toLowerCase() == 'x-frame-options') {
- headers.splice(i, 1);
- break;
- }
- }
- return {responseHeaders: headers};
+ chrome.webRequest.onCompleted.addListener(
+ function(details) {
+ if (this.bridges_[details.tabId])
+ this.bridges_[details.tabId].onCompleted(details);
}.bind(this),
- {urls: ['<all_urls>'], types: ['sub_frame']},
- ['blocking', 'responseHeaders']);
+ {urls: ['*://*/*'], types: ['sub_frame']},
+ ['responseHeaders']);
},
onConnect_: function(port) {
- if (port.name == 'authMain')
- this.setupForAuthMain_(port);
- else if (port.name == 'injected')
- this.setupForInjected_(port);
- else
+ var tabId = this.getTabIdFromPort_(port);
+ if (!this.bridges_[tabId])
+ this.bridges_[tabId] = new BackgroundBridge(tabId);
+ if (port.name == 'authMain') {
+ this.bridges_[tabId].setupForAuthMain(port);
+ port.onDisconnect.addListener(function() {
+ delete this.bridges_[tabId];
+ }.bind(this));
+ } else if (port.name == 'injected') {
+ this.bridges_[tabId].setupForInjected(port);
+ } else {
console.error('Unexpected connection, port.name=' + port.name);
+ }
},
+ getTabIdFromPort_: function(port) {
+ return port.sender.tab ? port.sender.tab.id : -1;
+ }
+};
+
+/**
+ * BackgroundBridge allows the main script and the injected script to
+ * collaborate. It forwards credentials API calls to the main script and
+ * maintains a list of scraped passwords.
+ * @param {string} tabId The associated tab ID.
+ */
+function BackgroundBridge(tabId) {
+ this.tabId_ = tabId;
+}
+
+BackgroundBridge.prototype = {
+ // The associated tab ID. Only used for debugging now.
+ tabId: null,
+
+ isDesktopFlow_: false,
+
+ // Continue URL that is set from main auth script.
+ continueUrl_: null,
+
+ // Whether the extension is loaded in a constrained window.
+ // Set from main auth script.
+ isConstrainedWindow_: null,
+
+ // Email of the newly authenticated user based on the gaia response header
+ // 'google-accounts-signin'.
+ email_: null,
+
+ // Session index of the newly authenticated user based on the gaia response
+ // header 'google-accounts-signin'.
+ sessionIndex_: null,
+
+ // Gaia URL base that is set from main auth script.
+ gaiaUrl_: null,
+
+ // Whether to abort the authentication flow and show an error messagen when
+ // content served over an unencrypted connection is detected.
+ blockInsecureContent_: false,
+
+ // Whether auth flow has started. It is used as a signal of whether the
+ // injected script should scrape passwords.
+ authStarted_: false,
+
+ passwordStore_: {},
+
+ channelMain_: null,
+ channelInjected_: null,
+
/**
* Sets up the communication channel with the main script.
*/
- setupForAuthMain_: function(port) {
+ setupForAuthMain: function(port) {
this.channelMain_ = new Channel();
this.channelMain_.init(port);
+
+ // Registers for desktop related messages.
+ this.channelMain_.registerMessage(
+ 'initDesktopFlow', this.onInitDesktopFlow_.bind(this));
+
+ // Registers for SAML related messages.
this.channelMain_.registerMessage(
'setGaiaUrl', this.onSetGaiaUrl_.bind(this));
this.channelMain_.registerMessage(
+ 'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this));
+ this.channelMain_.registerMessage(
'resetAuth', this.onResetAuth_.bind(this));
this.channelMain_.registerMessage(
'startAuth', this.onAuthStarted_.bind(this));
this.channelMain_.registerMessage(
'getScrapedPasswords',
this.onGetScrapedPasswords_.bind(this));
+ this.channelMain_.registerMessage(
+ 'apiResponse', this.onAPIResponse_.bind(this));
+
+ this.channelMain_.send({
+ 'name': 'channelConnected'
+ });
},
/**
* Sets up the communication channel with the injected script.
*/
- setupForInjected_: function(port) {
+ setupForInjected: function(port) {
this.channelInjected_ = new Channel();
this.channelInjected_.init(port);
+
+ this.channelInjected_.registerMessage(
+ 'apiCall', this.onAPICall_.bind(this));
this.channelInjected_.registerMessage(
'updatePassword', this.onUpdatePassword_.bind(this));
this.channelInjected_.registerMessage(
@@ -95,22 +187,133 @@ BackgroundBridge.prototype = {
},
/**
+ * Handler for 'initDesktopFlow' signal sent from the main script.
+ * Only called in desktop mode.
+ */
+ onInitDesktopFlow_: function(msg) {
+ this.isDesktopFlow_ = true;
+ this.gaiaUrl_ = msg.gaiaUrl;
+ this.continueUrl_ = msg.continueUrl;
+ this.isConstrainedWindow_ = msg.isConstrainedWindow;
+ },
+
+ /**
+ * Handler for webRequest.onCompleted. It 1) detects loading of continue URL
+ * and notifies the main script of signin completion; 2) detects if the
+ * current page could be loaded in a constrained window and signals the main
+ * script of switching to full tab if necessary.
+ */
+ onCompleted: function(details) {
+ // Only monitors requests in the gaia frame whose parent frame ID must be
+ // positive.
+ if (!this.isDesktopFlow_ || details.parentFrameId <= 0)
+ return;
+
+ var msg = null;
+ if (this.continueUrl_ &&
+ details.url.lastIndexOf(this.continueUrl_, 0) == 0) {
+ var skipForNow = false;
+ if (details.url.indexOf('ntp=1') >= 0)
+ skipForNow = true;
+
+ // TOOD(guohui): Show password confirmation UI.
+ var passwords = this.onGetScrapedPasswords_();
+ msg = {
+ 'name': 'completeLogin',
+ 'email': this.email_,
+ 'password': passwords[0],
+ 'sessionIndex': this.sessionIndex_,
+ 'skipForNow': skipForNow
+ };
+ this.channelMain_.send(msg);
+ } else if (this.isConstrainedWindow_) {
+ // The header google-accounts-embedded is only set on gaia domain.
+ if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) {
+ var headers = details.responseHeaders;
+ for (var i = 0; headers && i < headers.length; ++i) {
+ if (headers[i].name.toLowerCase() == 'google-accounts-embedded')
+ return;
+ }
+ }
+ msg = {
+ 'name': 'switchToFullTab',
+ 'url': details.url
+ };
+ this.channelMain_.send(msg);
+ }
+ },
+
+ /**
+ * Handler for webRequest.onBeforeRequest, invoked when content served over an
+ * unencrypted connection is detected. Determines whether the request should
+ * be blocked and if so, signals that an error message needs to be shown.
+ * @param {string} url The URL that was blocked.
+ * @return {!Object} Decision whether to block the request.
+ */
+ onInsecureRequest: function(url) {
+ if (!this.blockInsecureContent_)
+ return {};
+ this.channelMain_.send({name: 'onInsecureContentBlocked', url: url});
+ return {cancel: true};
+ },
+
+ /**
+ * Handler or webRequest.onHeadersReceived. It reads the authenticated user
+ * email from google-accounts-signin-header.
+ */
+ onHeadersReceived: function(details) {
+ if (!this.isDesktopFlow_ ||
+ !this.gaiaUrl_ ||
+ details.url.lastIndexOf(this.gaiaUrl_) != 0) {
+ // TODO(xiyuan, guohui): CrOS should reuse the logic below for reading the
+ // email for SAML users and cut off the /ListAccount call.
+ return;
+ }
+
+ var headers = details.responseHeaders;
+ for (var i = 0; headers && i < headers.length; ++i) {
+ if (headers[i].name.toLowerCase() == 'google-accounts-signin') {
+ var headerValues = headers[i].value.toLowerCase().split(',');
+ var signinDetails = {};
+ headerValues.forEach(function(e) {
+ var pair = e.split('=');
+ signinDetails[pair[0].trim()] = pair[1].trim();
+ });
+ // Remove "" around.
+ this.email_ = signinDetails['email'].slice(1, -1);
+ this.sessionIndex_ = signinDetails['sessionindex'];
+ return;
+ }
+ }
+ },
+
+ /**
+ * Handler for webRequest.onBeforeSendHeaders.
+ * @return {!Object} Modified request headers.
+ */
+ onBeforeSendHeaders: function(details) {
+ if (!this.isDesktopFlow_ && this.gaiaUrl_ &&
+ details.url.indexOf(this.gaiaUrl_) == 0) {
+ details.requestHeaders.push({
+ name: 'X-Cros-Auth-Ext-Support',
+ value: 'SAML'
+ });
+ }
+ return {requestHeaders: details.requestHeaders};
+ },
+
+ /**
* Handler for 'setGaiaUrl' signal sent from the main script.
*/
onSetGaiaUrl_: function(msg) {
this.gaiaUrl_ = msg.gaiaUrl;
+ },
- // Set request header to let Gaia know that saml support is on.
- chrome.webRequest.onBeforeSendHeaders.addListener(
- function(details) {
- details.requestHeaders.push({
- name: 'X-Cros-Auth-Ext-Support',
- value: 'SAML'
- });
- return {requestHeaders: details.requestHeaders};
- },
- {urls: [this.gaiaUrl_ + '*'], types: ['sub_frame']},
- ['blocking', 'requestHeaders']);
+ /**
+ * Handler for 'setBlockInsecureContent' signal sent from the main script.
+ */
+ onSetBlockInsecureContent_: function(msg) {
+ this.blockInsecureContent_ = msg.blockInsecureContent;
},
/**
@@ -141,6 +344,18 @@ BackgroundBridge.prototype = {
return Object.keys(passwords);
},
+ /**
+ * Handler for 'apiResponse' signal sent from the main script. Passes on the
+ * |msg| to the injected script.
+ */
+ onAPIResponse_: function(msg) {
+ this.channelInjected_.send(msg);
+ },
+
+ onAPICall_: function(msg) {
+ this.channelMain_.send(msg);
+ },
+
onUpdatePassword_: function(msg) {
if (!this.authStarted_)
return;
@@ -149,9 +364,10 @@ BackgroundBridge.prototype = {
},
onPageLoaded_: function(msg) {
- this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url});
+ if (this.channelMain_)
+ this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url});
}
};
-var backgroundBridge = new BackgroundBridge();
-backgroundBridge.run();
+var backgroundBridgeManager = new BackgroundBridgeManager();
+backgroundBridgeManager.run();
diff --git a/chromium/chrome/browser/resources/gaia_auth/inline_injected.js b/chromium/chrome/browser/resources/gaia_auth/inline_injected.js
deleted file mode 100644
index b0b84738160..00000000000
--- a/chromium/chrome/browser/resources/gaia_auth/inline_injected.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2013 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 Code injected into Gaia sign in page for inline sign in flow.
- * On load stop, it receives a message from the embedder extension with a
- * JavaScript reference to the embedder window. Then upon submit of the sign
- * in form, it posts the username and password to the embedder window.
- * Prototype Only.
- */
-
-(function() {
- var extWindow;
-
- var $ = function(id) { return document.getElementById(id); };
- var gaiaLoginForm = $('gaia_loginform');
-
- var onMessage = function(e) {
- extWindow = e.source;
- };
- window.addEventListener('message', onMessage);
-
- var onLoginSubmit = function(e) {
- if (!extWindow) {
- console.log('ERROR: no initial message received from the gaia ext');
- e.preventDefault();
- return;
- }
-
- var checkboxElement = $('advanced-box');
- var chooseWhatToSync = checkboxElement && checkboxElement.checked;
- var msg = {method: 'attemptLogin',
- email: gaiaLoginForm['Email'].value,
- password: gaiaLoginForm['Passwd'].value,
- attemptToken: new Date().getTime(),
- chooseWhatToSync: chooseWhatToSync};
-
- extWindow.postMessage(msg, 'chrome://chrome-signin');
- console.log('Credentials sent');
-
- return;
- };
- // Overrides the submit handler for the gaia login form.
- gaiaLoginForm.onsubmit = onLoginSubmit;
-})();
diff --git a/chromium/chrome/browser/resources/gaia_auth/inline_main.html b/chromium/chrome/browser/resources/gaia_auth/inline_main.html
deleted file mode 100644
index 00ed78b4982..00000000000
--- a/chromium/chrome/browser/resources/gaia_auth/inline_main.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <link rel="stylesheet" href="main.css">
- <meta charset="utf-8">
- <script src="channel.js"></script>
- <script src="util.js"></script>
- <script src="main.js"></script>
-</head>
-<body>
- <webview id="gaia-frame"></webview>
-</body>
-</html>
diff --git a/chromium/chrome/browser/resources/gaia_auth/main.html b/chromium/chrome/browser/resources/gaia_auth/main.html
index 944d3fffd9a..a53b3fe919c 100644
--- a/chromium/chrome/browser/resources/gaia_auth/main.html
+++ b/chromium/chrome/browser/resources/gaia_auth/main.html
@@ -8,6 +8,8 @@
<script src="main.js"></script>
</head>
<body>
- <iframe id="gaia-frame" src="about:blank" frameborder="0"></iframe>
+ <iframe id="gaia-frame" name="gaia-frame" src="about:blank" frameborder="0"
+ sandbox="allow-same-origin allow-scripts allow-popups allow-forms
+ allow-pointer-lock"></iframe>
</body>
</html>
diff --git a/chromium/chrome/browser/resources/gaia_auth/main.js b/chromium/chrome/browser/resources/gaia_auth/main.js
index 7641a848546..41f5116fff6 100644
--- a/chromium/chrome/browser/resources/gaia_auth/main.js
+++ b/chromium/chrome/browser/resources/gaia_auth/main.js
@@ -16,6 +16,26 @@ Authenticator.THIS_EXTENSION_ORIGIN =
'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
/**
+ * The lowest version of the credentials passing API supported.
+ * @type {number}
+ */
+Authenticator.MIN_API_VERSION_VERSION = 1;
+
+/**
+ * The highest version of the credentials passing API supported.
+ * @type {number}
+ */
+Authenticator.MAX_API_VERSION_VERSION = 1;
+
+/**
+ * The key types supported by the credentials passing API.
+ * @type {Array} Array of strings.
+ */
+Authenticator.API_KEY_TYPES = [
+ 'KEY_TYPE_PASSWORD_PLAIN',
+];
+
+/**
* Singleton getter of Authenticator.
* @return {Object} The singleton instance of Authenticator.
*/
@@ -28,21 +48,30 @@ Authenticator.getInstance = function() {
Authenticator.prototype = {
email_: null,
- password_: null,
+
+ // Depending on the key type chosen, this will contain the plain text password
+ // or a credential derived from it along with the information required to
+ // repeat the derivation, such as a salt. The information will be encoded so
+ // that it contains printable ASCII characters only. The exact encoding is TBD
+ // when support for key types other than plain text password is added.
+ passwordBytes_: null,
+
attemptToken_: null,
// Input params from extension initialization URL.
inputLang_: undefined,
intputEmail_: undefined,
- samlPageLoaded_: false,
- samlSupportChannel_: null,
+ isSAMLFlow_: false,
+ isSAMLEnabled_: false,
+ supportChannel_: null,
GAIA_URL: 'https://accounts.google.com/',
GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
PARENT_PAGE: 'chrome://oobe/',
SERVICE_ID: 'chromeoslogin',
CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
+ CONSTRAINED_FLOW_SOURCE: 'chrome',
initialize: function() {
var params = getUrlSearchParams(location.search);
@@ -53,16 +82,17 @@ Authenticator.prototype = {
this.inputEmail_ = params.email;
this.service_ = params.service || this.SERVICE_ID;
this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
- this.continueUrlWithoutParams_ = stripParams(this.continueUrl_);
- this.inlineMode_ = params.inlineMode == '1';
- this.constrained_ = params.constrained == '1';
- this.partitionId_ = params.partitionId || '';
+ this.desktopMode_ = params.desktopMode == '1';
+ this.isConstrainedWindow_ = params.constrained == '1';
this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
- this.loaded_ = false;
- document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this));
- document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
+ document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
+ if (!this.desktopMode_) {
+ // SAML is always enabled in desktop mode, thus no need to listen for
+ // enableSAML event.
+ document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
+ }
},
isGaiaMessage_: function(msg) {
@@ -88,116 +118,135 @@ Authenticator.prototype = {
url = appendParam(url, 'hl', this.inputLang_);
if (this.inputEmail_)
url = appendParam(url, 'Email', this.inputEmail_);
-
+ if (this.isConstrainedWindow_)
+ url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
return url;
},
- /** Callback when all loads in the gaia webview is complete. */
- onWebviewLoadstop_: function(gaiaFrame) {
- // Report the current state to the parent which will then update the
- // browser history so that later it could respond properly to back/forward.
- var msg = {
- 'method': 'reportState',
- 'src': gaiaFrame.src
- };
- window.parent.postMessage(msg, this.parentPage_);
+ onPageLoad_: function() {
+ window.addEventListener('message', this.onMessage.bind(this), false);
- if (gaiaFrame.src.lastIndexOf(
- this.continueUrlWithoutParams_, 0) == 0) {
- // Detect when login is finished by the load stop event of the continue
- // URL. Cannot reuse the login complete flow in success.html, because
- // webview does not support extension pages yet.
- gaiaFrame.hidden = true;
- msg = {'method': 'completeLogin'};
- window.parent.postMessage(msg, this.parentPage_);
- return;
- }
+ var gaiaFrame = $('gaia-frame');
+ gaiaFrame.src = this.initialFrameUrl_;
- if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) {
- gaiaFrame.executeScript({file: 'inline_injected.js'}, function() {
- // Send an initial message to gaia so that it has an JavaScript
- // reference to the embedder.
- gaiaFrame.contentWindow.postMessage('', gaiaFrame.src);
- });
- }
+ if (this.desktopMode_) {
+ var handler = function() {
+ this.onLoginUILoaded_();
+ gaiaFrame.removeEventListener('load', handler);
- this.loaded_ || this.onLoginUILoaded();
+ this.initDesktopChannel_();
+ }.bind(this);
+ gaiaFrame.addEventListener('load', handler);
+ }
},
- /**
- * Callback when the gaia webview attempts to open a new window.
- */
- onWebviewNewWindow_: function(gaiaFrame, e) {
- window.open(e.targetUrl, '_blank');
- e.window.discard();
- },
+ initDesktopChannel_: function() {
+ this.supportChannel_ = new Channel();
+ this.supportChannel_.connect('authMain');
- onWebviewRequestCompleted_: function(details) {
- if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
- return;
- }
+ var channelConnected = false;
+ this.supportChannel_.registerMessage('channelConnected', function() {
+ channelConnected = true;
- var headers = details.responseHeaders;
- for (var i = 0; headers && i < headers.length; ++i) {
- if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
- return;
+ this.supportChannel_.send({
+ name: 'initDesktopFlow',
+ gaiaUrl: this.gaiaUrl_,
+ continueUrl: stripParams(this.continueUrl_),
+ isConstrainedWindow: this.isConstrainedWindow_
+ });
+ this.supportChannel_.registerMessage(
+ 'switchToFullTab', this.switchToFullTab_.bind(this));
+ this.supportChannel_.registerMessage(
+ 'completeLogin', this.completeLogin_.bind(this));
+
+ this.onEnableSAML_();
+ }.bind(this));
+
+ window.setTimeout(function() {
+ if (!channelConnected) {
+ // Re-initialize the channel if it is not connected properly, e.g.
+ // connect may be called before background script started running.
+ this.initDesktopChannel_();
}
- }
+ }.bind(this), 200);
+ },
+
+ /**
+ * Invoked when the login UI is initialized or reset.
+ */
+ onLoginUILoaded_: function() {
var msg = {
- 'method': 'switchToFullTab',
- 'url': details.url
+ 'method': 'loginUILoaded'
};
window.parent.postMessage(msg, this.parentPage_);
},
- loadFrame_: function() {
- var gaiaFrame = $('gaia-frame');
- gaiaFrame.partition = this.partitionId_;
- gaiaFrame.src = this.initialFrameUrl_;
- if (this.inlineMode_) {
- gaiaFrame.addEventListener(
- 'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame));
- gaiaFrame.addEventListener(
- 'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame));
- }
- if (this.constrained_) {
- gaiaFrame.request.onCompleted.addListener(
- this.onWebviewRequestCompleted_.bind(this),
- {urls: ['<all_urls>'], types: ['main_frame']},
- ['responseHeaders']);
- }
+ /**
+ * Invoked when the background script sends a message to indicate that the
+ * current content does not fit in a constrained window.
+ * @param {Object=} opt_extraMsg Optional extra info to send.
+ */
+ switchToFullTab_: function(msg) {
+ var parentMsg = {
+ 'method': 'switchToFullTab',
+ 'url': msg.url
+ };
+ window.parent.postMessage(parentMsg, this.parentPage_);
},
- completeLogin: function(username, password) {
+ /**
+ * Invoked when the signin flow is complete.
+ * @param {Object=} opt_extraMsg Optional extra info to send.
+ */
+ completeLogin_: function(opt_extraMsg) {
var msg = {
'method': 'completeLogin',
- 'email': username,
- 'password': password
+ 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
+ 'password': (opt_extraMsg && opt_extraMsg.password) ||
+ this.passwordBytes_,
+ 'usingSAML': this.isSAMLFlow_,
+ 'chooseWhatToSync': this.chooseWhatToSync_ || false,
+ 'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
+ 'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
};
window.parent.postMessage(msg, this.parentPage_);
- if (this.samlSupportChannel_)
- this.samlSupportChannel_.send({name: 'resetAuth'});
- },
-
- onPageLoad: function(e) {
- window.addEventListener('message', this.onMessage.bind(this), false);
- this.loadFrame_();
+ if (this.isSAMLEnabled_)
+ this.supportChannel_.send({name: 'resetAuth'});
},
/**
- * Invoked when 'enableSAML' event is received to initialize SAML support.
+ * Invoked when 'enableSAML' event is received to initialize SAML support on
+ * Chrome OS, or when initDesktopChannel_ is called on desktop.
*/
onEnableSAML_: function() {
- this.samlPageLoaded_ = false;
+ this.isSAMLEnabled_ = true;
+ this.isSAMLFlow_ = false;
- this.samlSupportChannel_ = new Channel();
- this.samlSupportChannel_.connect('authMain');
- this.samlSupportChannel_.registerMessage(
+ if (!this.supportChannel_) {
+ this.supportChannel_ = new Channel();
+ this.supportChannel_.connect('authMain');
+ }
+
+ this.supportChannel_.registerMessage(
'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
- this.samlSupportChannel_.send({
+ this.supportChannel_.registerMessage(
+ 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this));
+ this.supportChannel_.registerMessage(
+ 'apiCall', this.onAPICall_.bind(this));
+ this.supportChannel_.send({
name: 'setGaiaUrl',
gaiaUrl: this.gaiaUrl_
});
+ if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) {
+ // Abort the login flow when content served over an unencrypted connection
+ // is detected on Chrome OS. This does not apply to tests that explicitly
+ // set a non-https GAIA URL and want to perform all authentication over
+ // http.
+ this.supportChannel_.send({
+ name: 'setBlockInsecureContent',
+ blockInsecureContent: true
+ });
+ }
},
/**
@@ -205,52 +254,135 @@ Authenticator.prototype = {
* @param {!Object} msg Details sent with the message.
*/
onAuthPageLoaded_: function(msg) {
- this.samlPageLoaded_ = msg.url.indexOf(this.gaiaUrl_) != 0;
+ var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
+
+ if (isSAMLPage && !this.isSAMLFlow_) {
+ // GAIA redirected to a SAML login page. The credentials provided to this
+ // page will determine what user gets logged in. The credentials obtained
+ // from the GAIA login form are no longer relevant and can be discarded.
+ this.isSAMLFlow_ = true;
+ this.email_ = null;
+ this.passwordBytes_ = null;
+ }
+
window.parent.postMessage({
'method': 'authPageLoaded',
- 'isSAML': this.samlPageLoaded_
+ 'isSAML': this.isSAMLFlow_,
+ 'domain': extractDomain(msg.url)
}, this.parentPage_);
},
- onLoginUILoaded: function() {
- var msg = {
- 'method': 'loginUILoaded'
- };
- window.parent.postMessage(msg, this.parentPage_);
- if (this.inlineMode_) {
- $('gaia-frame').focus();
+ /**
+ * Invoked when the background page sends an 'onInsecureContentBlocked'
+ * message.
+ * @param {!Object} msg Details sent with the message.
+ */
+ onInsecureContentBlocked_: function(msg) {
+ window.parent.postMessage({
+ 'method': 'insecureContentBlocked',
+ 'url': stripParams(msg.url)
+ }, this.parentPage_);
+ },
+
+ /**
+ * Invoked when one of the credential passing API methods is called by a SAML
+ * provider.
+ * @param {!Object} msg Details of the API call.
+ */
+ onAPICall_: function(msg) {
+ var call = msg.call;
+ if (call.method == 'initialize') {
+ if (!Number.isInteger(call.requestedVersion) ||
+ call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
+ this.sendInitializationFailure_();
+ return;
+ }
+
+ this.apiVersion_ = Math.min(call.requestedVersion,
+ Authenticator.MAX_API_VERSION_VERSION);
+ this.initialized_ = true;
+ this.sendInitializationSuccess_();
+ return;
+ }
+
+ if (call.method == 'add') {
+ if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
+ console.error('Authenticator.onAPICall_: unsupported key type');
+ return;
+ }
+ this.apiToken_ = call.token;
+ this.email_ = call.user;
+ this.passwordBytes_ = call.passwordBytes;
+ } else if (call.method == 'confirm') {
+ if (call.token != this.apiToken_)
+ console.error('Authenticator.onAPICall_: token mismatch');
+ } else {
+ console.error('Authenticator.onAPICall_: unknown message');
}
- this.loaded_ = true;
+ },
+
+ sendInitializationSuccess_: function() {
+ this.supportChannel_.send({name: 'apiResponse', response: {
+ result: 'initialized',
+ version: this.apiVersion_,
+ keyTypes: Authenticator.API_KEY_TYPES
+ }});
+ },
+
+ sendInitializationFailure_: function() {
+ this.supportChannel_.send({
+ name: 'apiResponse',
+ response: {result: 'initialization_failed'}
+ });
},
onConfirmLogin_: function() {
- if (!this.samlPageLoaded_) {
- this.completeLogin(this.email_, this.password_);
+ if (!this.isSAMLFlow_) {
+ this.completeLogin_();
return;
}
- this.samlSupportChannel_.sendWithCallback(
- {name: 'getScrapedPasswords'},
- function(passwords) {
- if (passwords.length == 0) {
- window.parent.postMessage(
- {method: 'noPassword', email: this.email_},
- this.parentPage_);
- } else {
- window.parent.postMessage(
- {method: 'confirmPassword', email: this.email_},
- this.parentPage_);
- }
- }.bind(this));
+ var apiUsed = !!this.passwordBytes_;
+
+ // Retrieve the e-mail address of the user who just authenticated from GAIA.
+ window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
+ attemptToken: this.attemptToken_,
+ apiUsed: apiUsed},
+ this.parentPage_);
+
+ if (!apiUsed) {
+ this.supportChannel_.sendWithCallback(
+ {name: 'getScrapedPasswords'},
+ function(passwords) {
+ if (passwords.length == 0) {
+ window.parent.postMessage(
+ {method: 'noPassword', email: this.email_},
+ this.parentPage_);
+ } else {
+ window.parent.postMessage({method: 'confirmPassword',
+ email: this.email_,
+ passwordCount: passwords.length},
+ this.parentPage_);
+ }
+ }.bind(this));
+ }
+ },
+
+ maybeCompleteSAMLLogin_: function() {
+ // SAML login is complete when the user's e-mail address has been retrieved
+ // from GAIA and the user has successfully confirmed the password.
+ if (this.email_ !== null && this.passwordBytes_ !== null)
+ this.completeLogin_();
},
onVerifyConfirmedPassword_: function(password) {
- this.samlSupportChannel_.sendWithCallback(
+ this.supportChannel_.sendWithCallback(
{name: 'getScrapedPasswords'},
function(passwords) {
for (var i = 0; i < passwords.length; ++i) {
if (passwords[i] == password) {
- this.completeLogin(this.email_, passwords[i]);
+ this.passwordBytes_ = passwords[i];
+ this.maybeCompleteSAMLLogin_();
return;
}
}
@@ -264,19 +396,26 @@ Authenticator.prototype = {
var msg = e.data;
if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
this.email_ = msg.email;
- this.password_ = msg.password;
+ this.passwordBytes_ = msg.password;
this.attemptToken_ = msg.attemptToken;
- this.samlPageLoaded_ = false;
- if (this.samlSupportChannel_)
- this.samlSupportChannel_.send({name: 'startAuth'});
+ this.chooseWhatToSync_ = msg.chooseWhatToSync;
+ this.isSAMLFlow_ = false;
+ if (this.isSAMLEnabled_)
+ this.supportChannel_.send({name: 'startAuth'});
} else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
this.email_ = null;
- this.password_ = null;
+ this.passwordBytes_ = null;
this.attemptToken_ = null;
- this.samlPageLoaded_ = false;
- this.onLoginUILoaded();
- if (this.samlSupportChannel_)
- this.samlSupportChannel_.send({name: 'resetAuth'});
+ this.isSAMLFlow_ = false;
+ this.onLoginUILoaded_();
+ if (this.isSAMLEnabled_)
+ this.supportChannel_.send({name: 'resetAuth'});
+ } else if (msg.method == 'setAuthenticatedUserEmail' &&
+ this.isParentMessage_(e)) {
+ if (this.attemptToken_ == msg.attemptToken) {
+ this.email_ = msg.email;
+ this.maybeCompleteSAMLLogin_();
+ }
} else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
if (this.attemptToken_ == msg.attemptToken)
this.onConfirmLogin_();
@@ -285,11 +424,11 @@ Authenticator.prototype = {
} else if (msg.method == 'verifyConfirmedPassword' &&
this.isParentMessage_(e)) {
this.onVerifyConfirmedPassword_(msg.password);
- } else if (msg.method == 'navigate' &&
+ } else if (msg.method == 'redirectToSignin' &&
this.isParentMessage_(e)) {
- $('gaia-frame').src = msg.src;
+ $('gaia-frame').src = this.constructInitialFrameUrl_();
} else {
- console.error('Authenticator.onMessage: unknown message + origin!?');
+ console.error('Authenticator.onMessage: unknown message + origin!?');
}
}
};
diff --git a/chromium/chrome/browser/resources/gaia_auth/manifest_inline.json b/chromium/chrome/browser/resources/gaia_auth/manifest_inline.json
deleted file mode 100644
index 2040360615f..00000000000
--- a/chromium/chrome/browser/resources/gaia_auth/manifest_inline.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- // chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/
- "key": "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC4L17nAfeTd6Xhtx96WhQ6DSr8KdHeQmfzgCkieKLCgUkWdwB9G1DCuh0EPMDn1MdtSwUAT7xE36APEzi0X/UpKjOVyX8tCC3aQcLoRAE0aJAvCcGwK7qIaQaczHmHKvPC2lrRdzSoMMTC5esvHX+ZqIBMi123FOL0dGW6OPKzIwIBIw==",
- "name": "GaiaAuthExtension",
- "version": "0.0.1",
- "manifest_version": 2,
- "content_security_policy": "default-src 'self'; script-src 'self'; frame-src *; style-src 'self' 'unsafe-inline'",
- "description": "GAIA Component Extension",
- "web_accessible_resources": [
- "main.css",
- "inline_main.html",
- "main.js",
- "offline.css",
- "offline.html",
- "offline.js",
- "success.html",
- "success.js",
- "util.js"
- ],
- // <all_urls> for intercepting all URL requests in the main frame, and
- // switching to a full tab if needed.
- // cookies for getting hash passed back from GAIA on login success.
- // tabs for calling current webui's login. This might not be needed once
- // we have extension API.
- // webview for interacting with the GAIA sign in page loaded in a webview.
- "permissions": [
- "<all_urls>",
- "cookies",
- "tabs",
- "webview"
- ]
-}
diff --git a/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json b/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json
index c30bdaa9f0f..afc5e93b92d 100644
--- a/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json
+++ b/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json
@@ -31,7 +31,6 @@
],
"permissions": [
"<all_urls>",
- "background",
"webRequest",
"webRequestBlocking"
]
diff --git a/chromium/chrome/browser/resources/gaia_auth/offline.css b/chromium/chrome/browser/resources/gaia_auth/offline.css
index 6f5712b285a..b36d3c942b6 100644
--- a/chromium/chrome/browser/resources/gaia_auth/offline.css
+++ b/chromium/chrome/browser/resources/gaia_auth/offline.css
@@ -102,8 +102,7 @@ input[type=url][disabled=disabled]:hover {
-webkit-transition: all 218ms;
-webkit-user-select: none;
background-color: #f5f5f5;
- background-image: -webkit-linear-gradient(top,#f5f5f5,#f1f1f1);
- background-image: linear-gradient(top,#f5f5f5,#f1f1f1);
+ background-image: linear-gradient(to bottom, #f5f5f5, #f1f1f1);
border: 1px solid rgba(0,0,0,0.1);
border-radius: 2px;
color: #555;
@@ -137,8 +136,7 @@ input[type=submit].g-button {
-webkit-box-shadow: 0 1px 1px rgba(0,0,0,0.1);
-webkit-transition: all 0;
background-color: #f8f8f8;
- background-image: -webkit-linear-gradient(top,#f8f8f8,#f1f1f1);
- background-image: linear-gradient(top,#f8f8f8,#f1f1f1);
+ background-image: linear-gradient(to bottom, #f8f8f8, #f1f1f1);
border: 1px solid #c6c6c6;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
color: #333;
@@ -148,8 +146,7 @@ input[type=submit].g-button {
.g-button:active {
-webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
background-color: #f6f6f6;
- background-image: -webkit-linear-gradient(top,#f6f6f6,#f1f1f1);
- background-image: linear-gradient(top,#f6f6f6,#f1f1f1);
+ background-image: linear-gradient(to bottom, #f6f6f6, #f1f1f1);
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
}
.g-button:visited {
@@ -157,19 +154,14 @@ input[type=submit].g-button {
}
.g-button-submit {
background-color: #4d90fe;
- background-image: -webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#4787ed));
- background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed);
- background-image: -ms-linear-gradient(top,#4d90fe,#4787ed);
- background-image: -o-linear-gradient(top,#4d90fe,#4787ed);
- background-image: linear-gradient(top,#4d90fe,#4787ed);
+ background-image: linear-gradient(to bottom, #4d90fe, #4787ed);
border: 1px solid #3079ed;
color: #fff;
text-shadow: 0 1px rgba(0,0,0,0.1);
}
.g-button-submit:hover {
background-color: #357ae8;
- background-image: -webkit-linear-gradient(top,#4d90fe,#357ae8);
- background-image: linear-gradient(top,#4d90fe,#357ae8);
+ background-image: linear-gradient(to bottom, #4d90fe, #357ae8);
border: 1px solid #2f5bb7;
color: #fff;
text-shadow: 0 1px rgba(0,0,0,0.3);
diff --git a/chromium/chrome/browser/resources/gaia_auth/saml_injected.js b/chromium/chrome/browser/resources/gaia_auth/saml_injected.js
index d7fd1b8e37e..e41dfebec62 100644
--- a/chromium/chrome/browser/resources/gaia_auth/saml_injected.js
+++ b/chromium/chrome/browser/resources/gaia_auth/saml_injected.js
@@ -4,15 +4,60 @@
/**
* @fileoverview
- * Script to be injected into SAML provider pages that do not support the
- * auth service provider postMessage API. It serves two main purposes:
+ * Script to be injected into SAML provider pages, serving three main purposes:
* 1. Signal hosting extension that an external page is loaded so that the
- * UI around it could be changed accordingly;
- * 2. Scrape password and send it back to be used for encrypt user data and
- * use for offline login;
+ * UI around it should be changed accordingly;
+ * 2. Provide an API via which the SAML provider can pass user credentials to
+ * Chrome OS, allowing the password to be used for encrypting user data and
+ * offline login.
+ * 3. Scrape password fields, making the password available to Chrome OS even if
+ * the SAML provider does not support the credential passing API.
*/
(function() {
+ function APICallForwarder() {
+ }
+
+ /**
+ * The credential passing API is used by sending messages to the SAML page's
+ * |window| object. This class forwards API calls from the SAML page to a
+ * background script and API responses from the background script to the SAML
+ * page. Communication with the background script occurs via a |Channel|.
+ */
+ APICallForwarder.prototype = {
+ // Channel to which API calls are forwarded.
+ channel_: null,
+
+ /**
+ * Initialize the API call forwarder.
+ * @param {!Object} channel Channel to which API calls should be forwarded.
+ */
+ init: function(channel) {
+ this.channel_ = channel;
+ this.channel_.registerMessage('apiResponse',
+ this.onAPIResponse_.bind(this));
+
+ window.addEventListener('message', this.onMessage_.bind(this));
+ },
+
+ onMessage_: function(event) {
+ if (event.source != window ||
+ typeof event.data != 'object' ||
+ !event.data.hasOwnProperty('type') ||
+ event.data.type != 'gaia_saml_api') {
+ return;
+ }
+ // Forward API calls to the background script.
+ this.channel_.send({name: 'apiCall', call: event.data.call});
+ },
+
+ onAPIResponse_: function(msg) {
+ // Forward API responses to the SAML page.
+ window.postMessage({type: 'gaia_saml_api_reply', response: msg.response},
+ '/');
+ }
+ };
+
/**
* A class to scrape password from type=password input elements under a given
* docRoot and send them back via a Channel.
@@ -51,11 +96,7 @@
for (var i = 0; i < this.passwordFields_.length; ++i) {
this.passwordFields_[i].addEventListener(
- 'change', this.onPasswordChanged_.bind(this, i));
- // 'keydown' event is needed for the case that the form is submitted
- // on enter key, in which case no 'change' event is dispatched.
- this.passwordFields_[i].addEventListener(
- 'keydown', this.onPasswordKeyDown_.bind(this, i));
+ 'input', this.onPasswordChanged_.bind(this, i));
this.passwordValues_[i] = this.passwordFields_[i].value;
}
@@ -89,18 +130,6 @@
*/
onPasswordChanged_: function(index) {
this.maybeSendUpdatedPassword(index);
- },
-
- /**
- * Handles 'keydown' event to trigger password change detection and
- * updates on enter key.
- * @param {number} index The index of the password fields in
- * |passwordFields_|.
- * @param {Event} e The keydown event.
- */
- onPasswordKeyDown_: function(index, e) {
- if (e.keyIdentifier == 'Enter')
- this.maybeSendUpdatedPassword(index);
}
};
@@ -141,6 +170,9 @@
channel.connect('injected');
channel.send({name: 'pageLoaded', url: pageURL});
+ apiCallForwarder = new APICallForwarder();
+ apiCallForwarder.init(channel);
+
passwordScraper = new PasswordInputScraper();
passwordScraper.init(channel, pageURL, document.documentElement);
}
diff --git a/chromium/chrome/browser/resources/gaia_auth/util.js b/chromium/chrome/browser/resources/gaia_auth/util.js
index 72d13c2f2f6..9d8ebd7237d 100644
--- a/chromium/chrome/browser/resources/gaia_auth/util.js
+++ b/chromium/chrome/browser/resources/gaia_auth/util.js
@@ -2,10 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+/**
+ * Alias for document.getElementById.
+ * @param {string} id The ID of the element to find.
+ * @return {HTMLElement} The found element or null if not found.
+ */
function $(id) {
return document.getElementById(id);
}
+/**
+ * Extract query params from given search string of an URL.
+ * @param {string} search The search portion of an URL to extract params.
+ * @return {Object} The key value pairs of the extracted params.
+ */
function getUrlSearchParams(search) {
var params = {};
@@ -51,3 +61,14 @@ function appendParam(url, key, value) {
function stripParams(url) {
return url.substring(0, url.indexOf('?')) || url;
}
+
+/**
+ * Extract domain name from an URL.
+ * @param {string} url An URL string.
+ * @return {string} The host name of the URL.
+ */
+function extractDomain(url) {
+ var a = document.createElement('a');
+ a.href = url;
+ return a.hostname;
+}