1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview 7 * Script to be injected into SAML provider pages that do not support the 8 * auth service provider postMessage API. It serves two main purposes: 9 * 1. Signal hosting extension that an external page is loaded so that the 10 * UI around it could be changed accordingly; 11 * 2. Scrape password and send it back to be used for encrypt user data and 12 * use for offline login; 13 */ 14 15(function() { 16 /** 17 * A class to scrape password from type=password input elements under a given 18 * docRoot and send them back via a Channel. 19 */ 20 function PasswordInputScraper() { 21 } 22 23 PasswordInputScraper.prototype = { 24 // URL of the page. 25 pageURL_: null, 26 27 // Channel to send back changed password. 28 channel_: null, 29 30 // An array to hold password fields. 31 passwordFields_: null, 32 33 // An array to hold cached password values. 34 passwordValues_: null, 35 36 /** 37 * Initialize the scraper with given channel and docRoot. Note that the 38 * scanning for password fields happens inside the function and does not 39 * handle DOM tree changes after the call returns. 40 * @param {!Object} channel The channel to send back password. 41 * @param {!string} pageURL URL of the page. 42 * @param {!HTMLElement} docRoot The root element of the DOM tree that 43 * contains the password fields of interest. 44 */ 45 init: function(channel, pageURL, docRoot) { 46 this.pageURL_ = pageURL; 47 this.channel_ = channel; 48 49 this.passwordFields_ = docRoot.querySelectorAll('input[type=password]'); 50 this.passwordValues_ = []; 51 52 for (var i = 0; i < this.passwordFields_.length; ++i) { 53 this.passwordFields_[i].addEventListener( 54 'change', this.onPasswordChanged_.bind(this, i)); 55 // 'keydown' event is needed for the case that the form is submitted 56 // on enter key, in which case no 'change' event is dispatched. 57 this.passwordFields_[i].addEventListener( 58 'keydown', this.onPasswordKeyDown_.bind(this, i)); 59 60 this.passwordValues_[i] = this.passwordFields_[i].value; 61 } 62 }, 63 64 /** 65 * Check if the password field at |index| has changed. If so, sends back 66 * the updated value. 67 */ 68 maybeSendUpdatedPassword: function(index) { 69 var newValue = this.passwordFields_[index].value; 70 if (newValue == this.passwordValues_[index]) 71 return; 72 73 this.passwordValues_[index] = newValue; 74 75 // Use an invalid char for URL as delimiter to concatenate page url and 76 // password field index to construct a unique ID for the password field. 77 var passwordId = this.pageURL_ + '|' + index; 78 this.channel_.send({ 79 name: 'updatePassword', 80 id: passwordId, 81 password: newValue 82 }); 83 }, 84 85 /** 86 * Handles 'change' event in the scraped password fields. 87 * @param {number} index The index of the password fields in 88 * |passwordFields_|. 89 */ 90 onPasswordChanged_: function(index) { 91 this.maybeSendUpdatedPassword(index); 92 }, 93 94 /** 95 * Handles 'keydown' event to trigger password change detection and 96 * updates on enter key. 97 * @param {number} index The index of the password fields in 98 * |passwordFields_|. 99 * @param {Event} e The keydown event. 100 */ 101 onPasswordKeyDown_: function(index, e) { 102 if (e.keyIdentifier == 'Enter') 103 this.maybeSendUpdatedPassword(index); 104 } 105 }; 106 107 /** 108 * Returns true if the script is injected into auth main page. 109 */ 110 function isAuthMainPage() { 111 return window.location.href.indexOf( 112 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/main.html') == 0; 113 } 114 115 /** 116 * Heuristic test whether the current page is a relevant SAML page. 117 * Current implementation checks if it is a http or https page and has 118 * some content in it. 119 * @return {boolean} Whether the current page looks like a SAML page. 120 */ 121 function isSAMLPage() { 122 var url = window.location.href; 123 if (!url.match(/^(http|https):\/\//)) 124 return false; 125 126 return document.body.scrollWidth > 50 && document.body.scrollHeight > 50; 127 } 128 129 if (isAuthMainPage()) { 130 // Use an event to signal the auth main to enable SAML support. 131 var e = document.createEvent('Event'); 132 e.initEvent('enableSAML', false, false); 133 document.dispatchEvent(e); 134 } else { 135 var channel; 136 var passwordScraper; 137 if (isSAMLPage()) { 138 var pageURL = window.location.href; 139 140 channel = new Channel(); 141 channel.connect('injected'); 142 channel.send({name: 'pageLoaded', url: pageURL}); 143 144 passwordScraper = new PasswordInputScraper(); 145 passwordScraper.init(channel, pageURL, document.documentElement); 146 } 147 } 148})(); 149