// 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 that do not support the * auth service provider postMessage API. It serves two 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; */ (function() { /** * 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( '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)); 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); }, /** * 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); } }; /** * Returns true if the script is injected into auth main page. */ function isAuthMainPage() { return window.location.href.indexOf( 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/main.html') == 0; } /** * 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 (isAuthMainPage()) { // Use an event to signal the auth main to enable SAML support. var e = document.createEvent('Event'); e.initEvent('enableSAML', false, false); document.dispatchEvent(e); } else { var channel; var passwordScraper; if (isSAMLPage()) { var pageURL = window.location.href; channel = new Channel(); channel.connect('injected'); channel.send({name: 'pageLoaded', url: pageURL}); passwordScraper = new PasswordInputScraper(); passwordScraper.init(channel, pageURL, document.documentElement); } } })();