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.
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
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.
18 function APICallForwarder() {
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|.
27 APICallForwarder
.prototype = {
28 // Channel to which API calls are forwarded.
32 * Initialize the API call forwarder.
33 * @param {!Object} channel Channel to which API calls should be forwarded.
35 init: function(channel
) {
36 this.channel_
= channel
;
37 this.channel_
.registerMessage('apiResponse',
38 this.onAPIResponse_
.bind(this));
40 window
.addEventListener('message', this.onMessage_
.bind(this));
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') {
50 // Forward API calls to the background script.
51 this.channel_
.send({name
: 'apiCall', call
: event
.data
.call
});
54 onAPIResponse_: function(msg
) {
55 // Forward API responses to the SAML page.
56 window
.postMessage({type
: 'gaia_saml_api_reply', response
: msg
.response
},
62 * A class to scrape password from type=password input elements under a given
63 * docRoot and send them back via a Channel.
65 function PasswordInputScraper() {
68 PasswordInputScraper
.prototype = {
72 // Channel to send back changed password.
75 // An array to hold password fields.
76 passwordFields_
: null,
78 // An array to hold cached password values.
79 passwordValues_
: null,
81 // A MutationObserver to watch for dynamic password field creation.
82 passwordFieldsObserver
: null,
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.
93 init: function(channel
, pageURL
, docRoot
) {
94 this.pageURL_
= pageURL
;
95 this.channel_
= channel
;
97 this.passwordFields_
= [];
98 this.passwordValues_
= [];
100 this.findAndTrackChildren(docRoot
);
102 this.passwordFieldsObserver
= new MutationObserver(function(mutations
) {
103 mutations
.forEach(function(mutation
) {
104 Array
.prototype.forEach
.call(
106 function(addedNode
) {
107 if (addedNode
.nodeType
!= Node
.ELEMENT_NODE
)
110 if (addedNode
.matches('input[type=password]')) {
111 this.trackPasswordField(addedNode
);
113 this.findAndTrackChildren(addedNode
);
118 this.passwordFieldsObserver
.observe(docRoot
,
119 {subtree
: true, childList
: true});
123 * Find and track password fields that are descendants of the given element.
124 * @param {!HTMLElement} element The parent element to search from.
126 findAndTrackChildren: function(element
) {
127 Array
.prototype.forEach
.call(
128 element
.querySelectorAll('input[type=password]'), function(field
) {
129 this.trackPasswordField(field
);
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.
138 trackPasswordField: function(passwordField
) {
139 var existing
= this.passwordFields_
.filter(function(element
) {
140 return element
=== passwordField
;
142 if (existing
.length
!= 0)
145 var index
= this.passwordFields_
.length
;
146 passwordField
.addEventListener(
147 'input', this.onPasswordChanged_
.bind(this, index
));
148 this.passwordFields_
.push(passwordField
);
149 this.passwordValues_
.push(passwordField
.value
);
153 * Check if the password field at |index| has changed. If so, sends back
156 maybeSendUpdatedPassword: function(index
) {
157 var newValue
= this.passwordFields_
[index
].value
;
158 if (newValue
== this.passwordValues_
[index
])
161 this.passwordValues_
[index
] = newValue
;
163 // Use an invalid char for URL as delimiter to concatenate page url and
164 // password field index to construct a unique ID for the password field.
165 var passwordId
= this.pageURL_
+ '|' + index
;
167 name
: 'updatePassword',
174 * Handles 'change' event in the scraped password fields.
175 * @param {number} index The index of the password fields in
178 onPasswordChanged_: function(index
) {
179 this.maybeSendUpdatedPassword(index
);
183 function onGetSAMLFlag(channel
, isSAMLPage
) {
186 var pageURL
= window
.location
.href
;
188 channel
.send({name
: 'pageLoaded', url
: pageURL
});
190 var initPasswordScraper = function() {
191 var passwordScraper
= new PasswordInputScraper();
192 passwordScraper
.init(channel
, pageURL
, document
.documentElement
);
195 if (document
.readyState
== 'loading') {
196 window
.addEventListener('readystatechange', function listener(event
) {
197 if (document
.readyState
== 'loading')
199 initPasswordScraper();
200 window
.removeEventListener(event
.type
, listener
, true);
203 initPasswordScraper();
207 var channel
= Channel
.create();
208 channel
.connect('injected');
209 channel
.sendWithCallback({name
: 'getSAMLFlag'},
210 onGetSAMLFlag
.bind(undefined, channel
));
212 var apiCallForwarder
= new APICallForwarder();
213 apiCallForwarder
.init(channel
);