| // 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 |
| * 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 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. |
| */ |
| function PasswordInputScraper() { |
| } |
| |
| PasswordInputScraper.prototype = { |
| // URL of the page. |
| pageURL_: null, |
| |
| // Channel to send back changed password. |
| channel_: null, |
| |
| // An array to hold password fields. |
| passwordFields_: null, |
| |
| // An array to hold cached password values. |
| passwordValues_: null, |
| |
| /** |
| * Initialize the scraper with given channel and docRoot. Note that the |
| * scanning for password fields happens inside the function and does not |
| * handle DOM tree changes after the call returns. |
| * @param {!Object} channel The channel to send back password. |
| * @param {!string} pageURL URL of the page. |
| * @param {!HTMLElement} docRoot The root element of the DOM tree that |
| * contains the password fields of interest. |
| */ |
| init: function(channel, pageURL, docRoot) { |
| this.pageURL_ = pageURL; |
| this.channel_ = channel; |
| |
| this.passwordFields_ = docRoot.querySelectorAll('input[type=password]'); |
| this.passwordValues_ = []; |
| |
| for (var i = 0; i < this.passwordFields_.length; ++i) { |
| this.passwordFields_[i].addEventListener( |
| 'input', this.onPasswordChanged_.bind(this, i)); |
| |
| this.passwordValues_[i] = this.passwordFields_[i].value; |
| } |
| }, |
| |
| /** |
| * Check if the password field at |index| has changed. If so, sends back |
| * the updated value. |
| */ |
| maybeSendUpdatedPassword: function(index) { |
| var newValue = this.passwordFields_[index].value; |
| if (newValue == this.passwordValues_[index]) |
| return; |
| |
| this.passwordValues_[index] = newValue; |
| |
| // Use an invalid char for URL as delimiter to concatenate page url and |
| // password field index to construct a unique ID for the password field. |
| var passwordId = this.pageURL_ + '|' + index; |
| this.channel_.send({ |
| name: 'updatePassword', |
| id: passwordId, |
| password: newValue |
| }); |
| }, |
| |
| /** |
| * Handles 'change' event in the scraped password fields. |
| * @param {number} index The index of the password fields in |
| * |passwordFields_|. |
| */ |
| onPasswordChanged_: function(index) { |
| this.maybeSendUpdatedPassword(index); |
| } |
| }; |
| |
| /** |
| * Heuristic test whether the current page is a relevant SAML page. |
| * Current implementation checks if it is a http or https page and has |
| * some content in it. |
| * @return {boolean} Whether the current page looks like a SAML page. |
| */ |
| function isSAMLPage() { |
| var url = window.location.href; |
| if (!url.match(/^(http|https):\/\//)) |
| return false; |
| |
| return document.body.scrollWidth > 50 && document.body.scrollHeight > 50; |
| } |
| |
| if (isSAMLPage()) { |
| var pageURL = window.location.href; |
| |
| var channel = new Channel(); |
| channel.connect('injected'); |
| channel.send({name: 'pageLoaded', url: pageURL}); |
| |
| var apiCallForwarder = new APICallForwarder(); |
| apiCallForwarder.init(channel); |
| |
| var passwordScraper = new PasswordInputScraper(); |
| passwordScraper.init(channel, pageURL, document.documentElement); |
| } |
| })(); |