OLD | NEW |
| (Empty) |
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, serving three main purposes: | |
8 * 1. Signal hosting extension that an external page is loaded so that the | |
9 * UI around it should be changed accordingly; | |
10 * 2. Provide an API via which the SAML provider can pass user credentials to | |
11 * Chrome OS, allowing the password to be used for encrypting user data and | |
12 * offline login. | |
13 * 3. Scrape password fields, making the password available to Chrome OS even if | |
14 * the SAML provider does not support the credential passing API. | |
15 */ | |
16 | |
17 (function() { | |
18 function APICallForwarder() { | |
19 } | |
20 | |
21 /** | |
22 * The credential passing API is used by sending messages to the SAML page's | |
23 * |window| object. This class forwards API calls from the SAML page to a | |
24 * background script and API responses from the background script to the SAML | |
25 * page. Communication with the background script occurs via a |Channel|. | |
26 */ | |
27 APICallForwarder.prototype = { | |
28 // Channel to which API calls are forwarded. | |
29 channel_: null, | |
30 | |
31 /** | |
32 * Initialize the API call forwarder. | |
33 * @param {!Object} channel Channel to which API calls should be forwarded. | |
34 */ | |
35 init: function(channel) { | |
36 this.channel_ = channel; | |
37 this.channel_.registerMessage('apiResponse', | |
38 this.onAPIResponse_.bind(this)); | |
39 | |
40 window.addEventListener('message', this.onMessage_.bind(this)); | |
41 }, | |
42 | |
43 onMessage_: function(event) { | |
44 if (event.source != window || | |
45 typeof event.data != 'object' || | |
46 !event.data.hasOwnProperty('type') || | |
47 event.data.type != 'gaia_saml_api') { | |
48 return; | |
49 } | |
50 // Forward API calls to the background script. | |
51 this.channel_.send({name: 'apiCall', call: event.data.call}); | |
52 }, | |
53 | |
54 onAPIResponse_: function(msg) { | |
55 // Forward API responses to the SAML page. | |
56 window.postMessage({type: 'gaia_saml_api_reply', response: msg.response}, | |
57 '/'); | |
58 } | |
59 }; | |
60 | |
61 /** | |
62 * A class to scrape password from type=password input elements under a given | |
63 * docRoot and send them back via a Channel. | |
64 */ | |
65 function PasswordInputScraper() { | |
66 } | |
67 | |
68 PasswordInputScraper.prototype = { | |
69 // URL of the page. | |
70 pageURL_: null, | |
71 | |
72 // Channel to send back changed password. | |
73 channel_: null, | |
74 | |
75 // An array to hold password fields. | |
76 passwordFields_: null, | |
77 | |
78 // An array to hold cached password values. | |
79 passwordValues_: null, | |
80 | |
81 // A MutationObserver to watch for dynamic password field creation. | |
82 passwordFieldsObserver: null, | |
83 | |
84 /** | |
85 * Initialize the scraper with given channel and docRoot. Note that the | |
86 * scanning for password fields happens inside the function and does not | |
87 * handle DOM tree changes after the call returns. | |
88 * @param {!Object} channel The channel to send back password. | |
89 * @param {!string} pageURL URL of the page. | |
90 * @param {!HTMLElement} docRoot The root element of the DOM tree that | |
91 * contains the password fields of interest. | |
92 */ | |
93 init: function(channel, pageURL, docRoot) { | |
94 this.pageURL_ = pageURL; | |
95 this.channel_ = channel; | |
96 | |
97 this.passwordFields_ = []; | |
98 this.passwordValues_ = []; | |
99 | |
100 this.findAndTrackChildren(docRoot); | |
101 | |
102 this.passwordFieldsObserver = new MutationObserver(function(mutations) { | |
103 mutations.forEach(function(mutation) { | |
104 Array.prototype.forEach.call( | |
105 mutation.addedNodes, | |
106 function(addedNode) { | |
107 if (addedNode.nodeType != Node.ELEMENT_NODE) | |
108 return; | |
109 | |
110 if (addedNode.matches('input[type=password]')) { | |
111 this.trackPasswordField(addedNode); | |
112 } else { | |
113 this.findAndTrackChildren(addedNode); | |
114 } | |
115 }.bind(this)); | |
116 }.bind(this)); | |
117 }.bind(this)); | |
118 this.passwordFieldsObserver.observe(docRoot, | |
119 {subtree: true, childList: true}); | |
120 }, | |
121 | |
122 /** | |
123 * Find and track password fields that are descendants of the given element. | |
124 * @param {!HTMLElement} element The parent element to search from. | |
125 */ | |
126 findAndTrackChildren: function(element) { | |
127 Array.prototype.forEach.call( | |
128 element.querySelectorAll('input[type=password]'), function(field) { | |
129 this.trackPasswordField(field); | |
130 }.bind(this)); | |
131 }, | |
132 | |
133 /** | |
134 * Start tracking value changes of the given password field if it is | |
135 * not being tracked yet. | |
136 * @param {!HTMLInputElement} passworField The password field to track. | |
137 */ | |
138 trackPasswordField: function(passwordField) { | |
139 var existing = this.passwordFields_.filter(function(element) { | |
140 return element === passwordField; | |
141 }); | |
142 if (existing.length != 0) | |
143 return; | |
144 | |
145 var index = this.passwordFields_.length; | |
146 var fieldId = passwordField.id || passwordField.name || ''; | |
147 passwordField.addEventListener( | |
148 'input', this.onPasswordChanged_.bind(this, index, fieldId)); | |
149 this.passwordFields_.push(passwordField); | |
150 this.passwordValues_.push(passwordField.value); | |
151 }, | |
152 | |
153 /** | |
154 * Check if the password field at |index| has changed. If so, sends back | |
155 * the updated value. | |
156 */ | |
157 maybeSendUpdatedPassword: function(index, fieldId) { | |
158 var newValue = this.passwordFields_[index].value; | |
159 if (newValue == this.passwordValues_[index]) | |
160 return; | |
161 | |
162 this.passwordValues_[index] = newValue; | |
163 | |
164 // Use an invalid char for URL as delimiter to concatenate page url, | |
165 // password field index and id to construct a unique ID for the password | |
166 // field. | |
167 var passwordId = this.pageURL_.split('#')[0].split('?')[0] + | |
168 '|' + index + '|' + fieldId; | |
169 this.channel_.send({ | |
170 name: 'updatePassword', | |
171 id: passwordId, | |
172 password: newValue | |
173 }); | |
174 }, | |
175 | |
176 /** | |
177 * Handles 'change' event in the scraped password fields. | |
178 * @param {number} index The index of the password fields in | |
179 * |passwordFields_|. | |
180 * @param {string} fieldId The id or name of the password field or blank. | |
181 */ | |
182 onPasswordChanged_: function(index, fieldId) { | |
183 this.maybeSendUpdatedPassword(index, fieldId); | |
184 } | |
185 }; | |
186 | |
187 function onGetSAMLFlag(channel, isSAMLPage) { | |
188 if (!isSAMLPage) | |
189 return; | |
190 var pageURL = window.location.href; | |
191 | |
192 channel.send({name: 'pageLoaded', url: pageURL}); | |
193 | |
194 var initPasswordScraper = function() { | |
195 var passwordScraper = new PasswordInputScraper(); | |
196 passwordScraper.init(channel, pageURL, document.documentElement); | |
197 }; | |
198 | |
199 if (document.readyState == 'loading') { | |
200 window.addEventListener('readystatechange', function listener(event) { | |
201 if (document.readyState == 'loading') | |
202 return; | |
203 initPasswordScraper(); | |
204 window.removeEventListener(event.type, listener, true); | |
205 }, true); | |
206 } else { | |
207 initPasswordScraper(); | |
208 } | |
209 } | |
210 | |
211 var channel = Channel.create(); | |
212 channel.connect('injected'); | |
213 channel.sendWithCallback({name: 'getSAMLFlag'}, | |
214 onGetSAMLFlag.bind(undefined, channel)); | |
215 | |
216 var apiCallForwarder = new APICallForwarder(); | |
217 apiCallForwarder.init(channel); | |
218 })(); | |
OLD | NEW |