Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth / saml_injected.js
blobae56f5cc537cce99829cb5b6cb8a1c892306fb11
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.
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  */
17 (function() {
18   function APICallForwarder() {
19   }
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,
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));
40       window.addEventListener('message', this.onMessage_.bind(this));
41     },
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     },
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   };
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   }
68   PasswordInputScraper.prototype = {
69     // URL of the page.
70     pageURL_: null,
72     // Channel to send back changed password.
73     channel_: null,
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,
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;
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(
105             mutation.addedNodes,
106             function(addedNode) {
107               if (addedNode.nodeType != Node.ELEMENT_NODE)
108                 return;
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     },
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     },
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;
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);
150     },
152     /**
153      * Check if the password field at |index| has changed. If so, sends back
154      * the updated value.
155      */
156     maybeSendUpdatedPassword: function(index) {
157       var newValue = this.passwordFields_[index].value;
158       if (newValue == this.passwordValues_[index])
159         return;
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_.split('#')[0].split('?')[0] + '|' + index;
166       this.channel_.send({
167         name: 'updatePassword',
168         id: passwordId,
169         password: newValue
170       });
171     },
173     /**
174      * Handles 'change' event in the scraped password fields.
175      * @param {number} index The index of the password fields in
176      *     |passwordFields_|.
177      */
178     onPasswordChanged_: function(index) {
179       this.maybeSendUpdatedPassword(index);
180     }
181   };
183   function onGetSAMLFlag(channel, isSAMLPage) {
184     if (!isSAMLPage)
185       return;
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);
193     };
195     if (document.readyState == 'loading') {
196       window.addEventListener('readystatechange', function listener(event) {
197         if (document.readyState == 'loading')
198           return;
199         initPasswordScraper();
200         window.removeEventListener(event.type, listener, true);
201       }, true);
202     } else {
203       initPasswordScraper();
204     }
205   }
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);
214 })();