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    /**
82     * Initialize the scraper with given channel and docRoot. Note that the
83     * scanning for password fields happens inside the function and does not
84     * handle DOM tree changes after the call returns.
85     * @param {!Object} channel The channel to send back password.
86     * @param {!string} pageURL URL of the page.
87     * @param {!HTMLElement} docRoot The root element of the DOM tree that
88     *     contains the password fields of interest.
89     */
90    init: function(channel, pageURL, docRoot) {
91      this.pageURL_ = pageURL;
92      this.channel_ = channel;
93
94      this.passwordFields_ = docRoot.querySelectorAll('input[type=password]');
95      this.passwordValues_ = [];
96
97      for (var i = 0; i < this.passwordFields_.length; ++i) {
98        this.passwordFields_[i].addEventListener(
99            'input', this.onPasswordChanged_.bind(this, i));
100
101        this.passwordValues_[i] = this.passwordFields_[i].value;
102      }
103    },
104
105    /**
106     * Check if the password field at |index| has changed. If so, sends back
107     * the updated value.
108     */
109    maybeSendUpdatedPassword: function(index) {
110      var newValue = this.passwordFields_[index].value;
111      if (newValue == this.passwordValues_[index])
112        return;
113
114      this.passwordValues_[index] = newValue;
115
116      // Use an invalid char for URL as delimiter to concatenate page url and
117      // password field index to construct a unique ID for the password field.
118      var passwordId = this.pageURL_ + '|' + index;
119      this.channel_.send({
120        name: 'updatePassword',
121        id: passwordId,
122        password: newValue
123      });
124    },
125
126    /**
127     * Handles 'change' event in the scraped password fields.
128     * @param {number} index The index of the password fields in
129     *     |passwordFields_|.
130     */
131    onPasswordChanged_: function(index) {
132      this.maybeSendUpdatedPassword(index);
133    }
134  };
135
136  /**
137   * Heuristic test whether the current page is a relevant SAML page.
138   * Current implementation checks if it is a http or https page and has
139   * some content in it.
140   * @return {boolean} Whether the current page looks like a SAML page.
141   */
142  function isSAMLPage() {
143    var url = window.location.href;
144    if (!url.match(/^(http|https):\/\//))
145      return false;
146
147    return document.body.scrollWidth > 50 && document.body.scrollHeight > 50;
148  }
149
150  if (isSAMLPage()) {
151    var pageURL = window.location.href;
152
153    var channel = new Channel();
154    channel.connect('injected');
155    channel.send({name: 'pageLoaded', url: pageURL});
156
157    var apiCallForwarder = new APICallForwarder();
158    apiCallForwarder.init(channel);
159
160    var passwordScraper = new PasswordInputScraper();
161    passwordScraper.init(channel, pageURL, document.documentElement);
162  }
163})();
164