blob: f2382259a585ecd7363a965ed5032a392c80113f [file] [log] [blame]
// 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);
}
})();