1// Copyright (c) 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.
4cr.define('policy', function() {
5
6  /**
7   * A hack to check if we are displaying the mobile version of this page by
8   * checking if the first column is hidden.
9   * @return {boolean} True if this is the mobile version.
10   */
11  var isMobilePage = function() {
12    return document.defaultView.getComputedStyle(document.querySelector(
13        '.scope-column')).display == 'none';
14  }
15
16  /**
17   * A box that shows the status of cloud policy for a device or user.
18   * @constructor
19   * @extends {HTMLFieldSetElement}
20   */
21  var StatusBox = cr.ui.define(function() {
22    var node = $('status-box-template').cloneNode(true);
23    node.removeAttribute('id');
24    return node;
25  });
26
27  StatusBox.prototype = {
28    // Set up the prototype chain.
29    __proto__: HTMLFieldSetElement.prototype,
30
31    /**
32     * Initialization function for the cr.ui framework.
33     */
34    decorate: function() {
35    },
36
37    /**
38     * Populate the box with the given cloud policy status.
39     * @param {string} scope The policy scope, either "device" or "user".
40     * @param {Object} status Dictionary with information about the status.
41     */
42    initialize: function(scope, status) {
43      if (scope == 'device') {
44        // For device policy, set the appropriate title and populate the topmost
45        // status item with the domain the device is enrolled into.
46        this.querySelector('.legend').textContent =
47            loadTimeData.getString('statusDevice');
48        var domain = this.querySelector('.domain');
49        domain.textContent = status.domain;
50        domain.parentElement.hidden = false;
51      } else {
52        // For user policy, set the appropriate title and populate the topmost
53        // status item with the username that policies apply to.
54        this.querySelector('.legend').textContent =
55            loadTimeData.getString('statusUser');
56        // Populate the topmost item with the username.
57        var username = this.querySelector('.username');
58        username.textContent = status.username;
59        username.parentElement.hidden = false;
60      }
61      // Populate all remaining items.
62      this.querySelector('.client-id').textContent = status.clientId || '';
63      this.querySelector('.time-since-last-refresh').textContent =
64          status.timeSinceLastRefresh || '';
65      this.querySelector('.refresh-interval').textContent =
66          status.refreshInterval || '';
67      this.querySelector('.status').textContent = status.status || '';
68    },
69  };
70
71  /**
72   * A single policy's entry in the policy table.
73   * @constructor
74   * @extends {HTMLTableSectionElement}
75   */
76  var Policy = cr.ui.define(function() {
77    var node = $('policy-template').cloneNode(true);
78    node.removeAttribute('id');
79    return node;
80  });
81
82  Policy.prototype = {
83    // Set up the prototype chain.
84    __proto__: HTMLTableSectionElement.prototype,
85
86    /**
87     * Initialization function for the cr.ui framework.
88     */
89    decorate: function() {
90      this.updateToggleExpandedValueText_();
91      this.querySelector('.toggle-expanded-value').addEventListener(
92          'click', this.toggleExpandedValue_.bind(this));
93    },
94
95    /**
96     * Populate the table columns with information about the policy name, value
97     * and status.
98     * @param {string} name The policy name.
99     * @param {Object} value Dictionary with information about the policy value.
100     * @param {boolean} unknown Whether the policy name is not recognized.
101     */
102    initialize: function(name, value, unknown) {
103      this.name = name;
104      this.unset = !value;
105
106      // Populate the name column.
107      this.querySelector('.name').textContent = name;
108
109      // Populate the remaining columns with policy scope, level and value if a
110      // value has been set. Otherwise, leave them blank.
111      if (value) {
112        this.querySelector('.scope').textContent =
113            loadTimeData.getString(value.scope == 'user' ?
114                'scopeUser' : 'scopeDevice');
115        this.querySelector('.level').textContent =
116            loadTimeData.getString(value.level == 'recommended' ?
117                'levelRecommended' : 'levelMandatory');
118        this.querySelector('.value').textContent = value.value;
119        this.querySelector('.expanded-value').textContent = value.value;
120      }
121
122      // Populate the status column.
123      var status;
124      if (!value) {
125        // If the policy value has not been set, show an error message.
126        status = loadTimeData.getString('unset');
127      } else if (unknown) {
128        // If the policy name is not recognized, show an error message.
129        status = loadTimeData.getString('unknown');
130      } else if (value.error) {
131        // If an error occurred while parsing the policy value, show the error
132        // message.
133        status = value.error;
134      } else {
135        // Otherwise, indicate that the policy value was parsed correctly.
136        status = loadTimeData.getString('ok');
137      }
138      this.querySelector('.status').textContent = status;
139
140      if (isMobilePage()) {
141        // The number of columns which are hidden by the css file for the mobile
142        // (Android) version of this page.
143        /** @const */ var HIDDEN_COLUMNS_IN_MOBILE_VERSION = 2;
144
145        var expandedValue = this.querySelector('.expanded-value');
146        expandedValue.setAttribute('colspan',
147            expandedValue.colSpan - HIDDEN_COLUMNS_IN_MOBILE_VERSION);
148      }
149    },
150
151    /**
152     * Check the table columns for overflow. Most columns are automatically
153     * elided when overflow occurs. The only action required is to add a tooltip
154     * that shows the complete content. The value column is an exception. If
155     * overflow occurs here, the contents is replaced with a link that toggles
156     * the visibility of an additional row containing the complete value.
157     */
158    checkOverflow: function() {
159      // Set a tooltip on all overflowed columns except the value column.
160      var divs = this.querySelectorAll('div.elide');
161      for (var i = 0; i < divs.length; i++) {
162        var div = divs[i];
163        div.title = div.offsetWidth < div.scrollWidth ? div.textContent : '';
164      }
165
166      // Cache the width of the value column's contents when it is first shown.
167      // This is required to be able to check whether the contents would still
168      // overflow the column once it has been hidden and replaced by a link.
169      var valueContainer = this.querySelector('.value-container');
170      if (valueContainer.valueWidth == undefined) {
171        valueContainer.valueWidth =
172            valueContainer.querySelector('.value').offsetWidth;
173      }
174
175      // Determine whether the contents of the value column overflows. The
176      // visibility of the contents, replacement link and additional row
177      // containing the complete value that depend on this are handled by CSS.
178      if (valueContainer.offsetWidth < valueContainer.valueWidth)
179        this.classList.add('has-overflowed-value');
180      else
181        this.classList.remove('has-overflowed-value');
182    },
183
184    /**
185     * Update the text of the link that toggles the visibility of an additional
186     * row containing the complete policy value, depending on the toggle state.
187     * @private
188     */
189    updateToggleExpandedValueText_: function(event) {
190      this.querySelector('.toggle-expanded-value').textContent =
191          loadTimeData.getString(
192              this.classList.contains('show-overflowed-value') ?
193                  'hideExpandedValue' : 'showExpandedValue');
194    },
195
196    /**
197     * Toggle the visibility of an additional row containing the complete policy
198     * value.
199     * @private
200     */
201    toggleExpandedValue_: function() {
202      this.classList.toggle('show-overflowed-value');
203      this.updateToggleExpandedValueText_();
204    },
205  };
206
207  /**
208   * A table of policies and their values.
209   * @constructor
210   * @extends {HTMLTableSectionElement}
211   */
212  var PolicyTable = cr.ui.define('tbody');
213
214  PolicyTable.prototype = {
215    // Set up the prototype chain.
216    __proto__: HTMLTableSectionElement.prototype,
217
218    /**
219     * Initialization function for the cr.ui framework.
220     */
221    decorate: function() {
222      this.policies_ = {};
223      this.filterPattern_ = '';
224      window.addEventListener('resize', this.checkOverflow_.bind(this));
225    },
226
227    /**
228     * Initialize the list of all known policies.
229     * @param {Object} names Dictionary containing all known policy names.
230     */
231    setPolicyNames: function(names) {
232      this.policies_ = names;
233      this.setPolicyValues({});
234    },
235
236    /**
237     * Populate the table with the currently set policy values and any errors
238     * detected while parsing these.
239     * @param {Object} values Dictionary containing the current policy values.
240     */
241    setPolicyValues: function(values) {
242      // Remove all policies from the table.
243      var policies = this.getElementsByTagName('tbody');
244      while (policies.length > 0)
245        this.removeChild(policies.item(0));
246
247      // First, add known policies whose value is currently set.
248      var unset = [];
249      for (var name in this.policies_) {
250        if (name in values)
251          this.setPolicyValue_(name, values[name], false);
252        else
253          unset.push(name);
254      }
255
256      // Second, add policies whose value is currently set but whose name is not
257      // recognized.
258      for (var name in values) {
259        if (!(name in this.policies_))
260          this.setPolicyValue_(name, values[name], true);
261      }
262
263      // Finally, add known policies whose value is not currently set.
264      for (var i = 0; i < unset.length; i++)
265        this.setPolicyValue_(unset[i], undefined, false);
266
267      // Filter the policies.
268      this.filter();
269    },
270
271    /**
272     * Set the filter pattern. Only policies whose name contains |pattern| are
273     * shown in the policy table. The filter is case insensitive. It can be
274     * disabled by setting |pattern| to an empty string.
275     * @param {string} pattern The filter pattern.
276     */
277    setFilterPattern: function(pattern) {
278      this.filterPattern_ = pattern.toLowerCase();
279      this.filter();
280    },
281
282    /**
283     * Filter policies. Only policies whose name contains the filter pattern are
284     * shown in the table. Furthermore, policies whose value is not currently
285     * set are only shown if the corresponding checkbox is checked.
286     */
287    filter: function() {
288      var showUnset = $('show-unset').checked;
289      var policies = this.getElementsByTagName('tbody');
290      for (var i = 0; i < policies.length; i++) {
291        var policy = policies[i];
292        policy.hidden =
293            policy.unset && !showUnset ||
294            policy.name.toLowerCase().indexOf(this.filterPattern_) == -1;
295      }
296      if (this.querySelector('tbody:not([hidden])'))
297        this.parentElement.classList.remove('empty');
298      else
299        this.parentElement.classList.add('empty');
300      setTimeout(this.checkOverflow_.bind(this), 0);
301    },
302
303    /**
304     * Check the table columns for overflow.
305     * @private
306     */
307    checkOverflow_: function() {
308      var policies = this.getElementsByTagName('tbody');
309      for (var i = 0; i < policies.length; i++) {
310        if (!policies[i].hidden)
311          policies[i].checkOverflow();
312      }
313    },
314
315    /**
316     * Add a policy with the given |name| and |value| to the table.
317     * @param {string} name The policy name.
318     * @param {Object} value Dictionary with information about the policy value.
319     * @param {boolean} unknown Whether the policy name is not recoginzed.
320     * @private
321     */
322    setPolicyValue_: function(name, value, unknown) {
323      var policy = new Policy;
324      policy.initialize(name, value, unknown);
325      this.appendChild(policy);
326    },
327  };
328
329  /**
330   * A singelton object that handles communication between browser and WebUI.
331   * @constructor
332   */
333  function Page() {
334  }
335
336  // Make Page a singleton.
337  cr.addSingletonGetter(Page);
338
339  /**
340   * Provide a list of all known policies to the UI. Called by the browser on
341   * page load.
342   * @param {Object} names Dictionary containing all known policy names.
343   */
344  Page.setPolicyNames = function(names) {
345    var page = this.getInstance();
346
347    // Clear all policy tables.
348    page.mainSection.innerHTML = '';
349    page.policyTables = {};
350
351    // Create tables and set known policy names for Chrome and extensions.
352    if (names.hasOwnProperty('chromePolicyNames')) {
353      var table = page.appendNewTable('chrome', 'Chrome policies', '');
354      table.setPolicyNames(names.chromePolicyNames);
355    }
356
357    if (names.hasOwnProperty('extensionPolicyNames')) {
358      for (var ext in names.extensionPolicyNames) {
359        var table = page.appendNewTable('extension-' + ext,
360            names.extensionPolicyNames[ext].name, 'ID: ' + ext);
361        table.setPolicyNames(names.extensionPolicyNames[ext].policyNames);
362      }
363    }
364  };
365
366  /**
367   * Provide a list of the currently set policy values and any errors detected
368   * while parsing these to the UI. Called by the browser on page load and
369   * whenever policy values change.
370   * @param {Object} values Dictionary containing the current policy values.
371   */
372  Page.setPolicyValues = function(values) {
373    var page = this.getInstance();
374    if (values.hasOwnProperty('chromePolicies')) {
375      var table = page.policyTables['chrome'];
376      table.setPolicyValues(values.chromePolicies);
377    }
378
379    if (values.hasOwnProperty('extensionPolicies')) {
380      for (var extensionId in values.extensionPolicies) {
381        var table = page.policyTables['extension-' + extensionId];
382        if (table)
383          table.setPolicyValues(values.extensionPolicies[extensionId]);
384      }
385    }
386  };
387
388  /**
389   * Provide the current cloud policy status to the UI. Called by the browser on
390   * page load if cloud policy is present and whenever the status changes.
391   * @param {Object} status Dictionary containing the current policy status.
392   */
393  Page.setStatus = function(status) {
394    this.getInstance().setStatus(status);
395  };
396
397  /**
398   * Notify the UI that a request to reload policy values has completed. Called
399   * by the browser after a request to reload policy has been sent by the UI.
400   */
401  Page.reloadPoliciesDone = function() {
402    this.getInstance().reloadPoliciesDone();
403  };
404
405  Page.prototype = {
406    /**
407     * Main initialization function. Called by the browser on page load.
408     */
409    initialize: function() {
410      uber.onContentFrameLoaded();
411      cr.ui.FocusOutlineManager.forDocument(document);
412
413      this.mainSection = $('main-section');
414      this.policyTables = {};
415
416      // Place the initial focus on the filter input field.
417      $('filter').focus();
418
419      var self = this;
420      $('filter').onsearch = function(event) {
421        for (policyTable in self.policyTables) {
422          self.policyTables[policyTable].setFilterPattern(this.value);
423        }
424      };
425      $('reload-policies').onclick = function(event) {
426        this.disabled = true;
427        chrome.send('reloadPolicies');
428      };
429
430      $('show-unset').onchange = function() {
431        for (policyTable in self.policyTables) {
432          self.policyTables[policyTable].filter();
433        }
434      };
435
436      // Notify the browser that the page has loaded, causing it to send the
437      // list of all known policies, the current policy values and the cloud
438      // policy status.
439      chrome.send('initialized');
440    },
441
442   /**
443     * Creates a new policy table section, adds the section to the page,
444     * and returns the new table from that section.
445     * @param {string} id The key for storing the new table in policyTables.
446     * @param {string} label_title Title for this policy table.
447     * @param {string} label_content Description for the policy table.
448     * @return {Element} The newly created table.
449     */
450    appendNewTable: function(id, label_title, label_content) {
451      var newSection = this.createPolicyTableSection(id, label_title,
452          label_content);
453      this.mainSection.appendChild(newSection);
454      return this.policyTables[id];
455    },
456
457    /**
458     * Creates a new section containing a title, description and table of
459     * policies.
460     * @param {id} id The key for storing the new table in policyTables.
461     * @param {string} label_title Title for this policy table.
462     * @param {string} label_content Description for the policy table.
463     * @return {Element} The newly created section.
464     */
465    createPolicyTableSection: function(id, label_title, label_content) {
466      var section = document.createElement('section');
467      section.setAttribute('class', 'policy-table-section');
468
469      // Add title and description.
470      var title = window.document.createElement('h3');
471      title.textContent = label_title;
472      section.appendChild(title);
473
474      if (label_content) {
475        var description = window.document.createElement('div');
476        description.classList.add('table-description');
477        description.textContent = label_content;
478        section.appendChild(description);
479      }
480
481      // Add 'No Policies Set' element.
482      var noPolicies = window.document.createElement('div');
483      noPolicies.classList.add('no-policies-set');
484      noPolicies.textContent = loadTimeData.getString('noPoliciesSet');
485      section.appendChild(noPolicies);
486
487      // Add table of policies.
488      var newTable = this.createPolicyTable();
489      this.policyTables[id] = newTable;
490      section.appendChild(newTable);
491
492      return section;
493    },
494
495    /**
496     * Creates a new table for displaying policies.
497     * @return {Element} The newly created table.
498     */
499    createPolicyTable: function() {
500      var newTable = window.document.createElement('table');
501      var tableHead = window.document.createElement('thead');
502      var tableRow = window.document.createElement('tr');
503      var tableHeadings = ['Scope', 'Level', 'Name', 'Value', 'Status'];
504      for (var i = 0; i < tableHeadings.length; i++) {
505        var tableHeader = window.document.createElement('th');
506        tableHeader.classList.add(tableHeadings[i].toLowerCase() + '-column');
507        tableHeader.textContent = loadTimeData.getString('header' +
508                                                         tableHeadings[i]);
509        tableRow.appendChild(tableHeader);
510      }
511      tableHead.appendChild(tableRow);
512      newTable.appendChild(tableHead);
513      cr.ui.decorate(newTable, PolicyTable);
514      return newTable;
515    },
516
517    /**
518     * Update the status section of the page to show the current cloud policy
519     * status.
520     * @param {Object} status Dictionary containing the current policy status.
521     */
522    setStatus: function(status) {
523      // Remove any existing status boxes.
524      var container = $('status-box-container');
525      while (container.firstChild)
526        container.removeChild(container.firstChild);
527      // Hide the status section.
528      var section = $('status-section');
529      section.hidden = true;
530
531      // Add a status box for each scope that has a cloud policy status.
532      for (var scope in status) {
533        var box = new StatusBox;
534        box.initialize(scope, status[scope]);
535        container.appendChild(box);
536        // Show the status section.
537        section.hidden = false;
538      }
539    },
540
541    /**
542     * Re-enable the reload policies button when the previous request to reload
543     * policies values has completed.
544     */
545    reloadPoliciesDone: function() {
546      $('reload-policies').disabled = false;
547    },
548  };
549
550  return {
551    Page: Page
552  };
553});
554
555// Have the main initialization function be called when the page finishes
556// loading.
557document.addEventListener(
558    'DOMContentLoaded',
559    policy.Page.getInstance().initialize.bind(policy.Page.getInstance()));
560