Javascript  |  560行  |  19.39 KB

// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('policy', function() {

  /**
   * A hack to check if we are displaying the mobile version of this page by
   * checking if the first column is hidden.
   * @return {boolean} True if this is the mobile version.
   */
  var isMobilePage = function() {
    return document.defaultView.getComputedStyle(document.querySelector(
        '.scope-column')).display == 'none';
  }

  /**
   * A box that shows the status of cloud policy for a device or user.
   * @constructor
   * @extends {HTMLFieldSetElement}
   */
  var StatusBox = cr.ui.define(function() {
    var node = $('status-box-template').cloneNode(true);
    node.removeAttribute('id');
    return node;
  });

  StatusBox.prototype = {
    // Set up the prototype chain.
    __proto__: HTMLFieldSetElement.prototype,

    /**
     * Initialization function for the cr.ui framework.
     */
    decorate: function() {
    },

    /**
     * Populate the box with the given cloud policy status.
     * @param {string} scope The policy scope, either "device" or "user".
     * @param {Object} status Dictionary with information about the status.
     */
    initialize: function(scope, status) {
      if (scope == 'device') {
        // For device policy, set the appropriate title and populate the topmost
        // status item with the domain the device is enrolled into.
        this.querySelector('.legend').textContent =
            loadTimeData.getString('statusDevice');
        var domain = this.querySelector('.domain');
        domain.textContent = status.domain;
        domain.parentElement.hidden = false;
      } else {
        // For user policy, set the appropriate title and populate the topmost
        // status item with the username that policies apply to.
        this.querySelector('.legend').textContent =
            loadTimeData.getString('statusUser');
        // Populate the topmost item with the username.
        var username = this.querySelector('.username');
        username.textContent = status.username;
        username.parentElement.hidden = false;
      }
      // Populate all remaining items.
      this.querySelector('.client-id').textContent = status.clientId || '';
      this.querySelector('.time-since-last-refresh').textContent =
          status.timeSinceLastRefresh || '';
      this.querySelector('.refresh-interval').textContent =
          status.refreshInterval || '';
      this.querySelector('.status').textContent = status.status || '';
    },
  };

  /**
   * A single policy's entry in the policy table.
   * @constructor
   * @extends {HTMLTableSectionElement}
   */
  var Policy = cr.ui.define(function() {
    var node = $('policy-template').cloneNode(true);
    node.removeAttribute('id');
    return node;
  });

  Policy.prototype = {
    // Set up the prototype chain.
    __proto__: HTMLTableSectionElement.prototype,

    /**
     * Initialization function for the cr.ui framework.
     */
    decorate: function() {
      this.updateToggleExpandedValueText_();
      this.querySelector('.toggle-expanded-value').addEventListener(
          'click', this.toggleExpandedValue_.bind(this));
    },

    /**
     * Populate the table columns with information about the policy name, value
     * and status.
     * @param {string} name The policy name.
     * @param {Object} value Dictionary with information about the policy value.
     * @param {boolean} unknown Whether the policy name is not recognized.
     */
    initialize: function(name, value, unknown) {
      this.name = name;
      this.unset = !value;

      // Populate the name column.
      this.querySelector('.name').textContent = name;

      // Populate the remaining columns with policy scope, level and value if a
      // value has been set. Otherwise, leave them blank.
      if (value) {
        this.querySelector('.scope').textContent =
            loadTimeData.getString(value.scope == 'user' ?
                'scopeUser' : 'scopeDevice');
        this.querySelector('.level').textContent =
            loadTimeData.getString(value.level == 'recommended' ?
                'levelRecommended' : 'levelMandatory');
        this.querySelector('.value').textContent = value.value;
        this.querySelector('.expanded-value').textContent = value.value;
      }

      // Populate the status column.
      var status;
      if (!value) {
        // If the policy value has not been set, show an error message.
        status = loadTimeData.getString('unset');
      } else if (unknown) {
        // If the policy name is not recognized, show an error message.
        status = loadTimeData.getString('unknown');
      } else if (value.error) {
        // If an error occurred while parsing the policy value, show the error
        // message.
        status = value.error;
      } else {
        // Otherwise, indicate that the policy value was parsed correctly.
        status = loadTimeData.getString('ok');
      }
      this.querySelector('.status').textContent = status;

      if (isMobilePage()) {
        // The number of columns which are hidden by the css file for the mobile
        // (Android) version of this page.
        /** @const */ var HIDDEN_COLUMNS_IN_MOBILE_VERSION = 2;

        var expandedValue = this.querySelector('.expanded-value');
        expandedValue.setAttribute('colspan',
            expandedValue.colSpan - HIDDEN_COLUMNS_IN_MOBILE_VERSION);
      }
    },

    /**
     * Check the table columns for overflow. Most columns are automatically
     * elided when overflow occurs. The only action required is to add a tooltip
     * that shows the complete content. The value column is an exception. If
     * overflow occurs here, the contents is replaced with a link that toggles
     * the visibility of an additional row containing the complete value.
     */
    checkOverflow: function() {
      // Set a tooltip on all overflowed columns except the value column.
      var divs = this.querySelectorAll('div.elide');
      for (var i = 0; i < divs.length; i++) {
        var div = divs[i];
        div.title = div.offsetWidth < div.scrollWidth ? div.textContent : '';
      }

      // Cache the width of the value column's contents when it is first shown.
      // This is required to be able to check whether the contents would still
      // overflow the column once it has been hidden and replaced by a link.
      var valueContainer = this.querySelector('.value-container');
      if (valueContainer.valueWidth == undefined) {
        valueContainer.valueWidth =
            valueContainer.querySelector('.value').offsetWidth;
      }

      // Determine whether the contents of the value column overflows. The
      // visibility of the contents, replacement link and additional row
      // containing the complete value that depend on this are handled by CSS.
      if (valueContainer.offsetWidth < valueContainer.valueWidth)
        this.classList.add('has-overflowed-value');
      else
        this.classList.remove('has-overflowed-value');
    },

    /**
     * Update the text of the link that toggles the visibility of an additional
     * row containing the complete policy value, depending on the toggle state.
     * @private
     */
    updateToggleExpandedValueText_: function(event) {
      this.querySelector('.toggle-expanded-value').textContent =
          loadTimeData.getString(
              this.classList.contains('show-overflowed-value') ?
                  'hideExpandedValue' : 'showExpandedValue');
    },

    /**
     * Toggle the visibility of an additional row containing the complete policy
     * value.
     * @private
     */
    toggleExpandedValue_: function() {
      this.classList.toggle('show-overflowed-value');
      this.updateToggleExpandedValueText_();
    },
  };

  /**
   * A table of policies and their values.
   * @constructor
   * @extends {HTMLTableSectionElement}
   */
  var PolicyTable = cr.ui.define('tbody');

  PolicyTable.prototype = {
    // Set up the prototype chain.
    __proto__: HTMLTableSectionElement.prototype,

    /**
     * Initialization function for the cr.ui framework.
     */
    decorate: function() {
      this.policies_ = {};
      this.filterPattern_ = '';
      window.addEventListener('resize', this.checkOverflow_.bind(this));
    },

    /**
     * Initialize the list of all known policies.
     * @param {Object} names Dictionary containing all known policy names.
     */
    setPolicyNames: function(names) {
      this.policies_ = names;
      this.setPolicyValues({});
    },

    /**
     * Populate the table with the currently set policy values and any errors
     * detected while parsing these.
     * @param {Object} values Dictionary containing the current policy values.
     */
    setPolicyValues: function(values) {
      // Remove all policies from the table.
      var policies = this.getElementsByTagName('tbody');
      while (policies.length > 0)
        this.removeChild(policies.item(0));

      // First, add known policies whose value is currently set.
      var unset = [];
      for (var name in this.policies_) {
        if (name in values)
          this.setPolicyValue_(name, values[name], false);
        else
          unset.push(name);
      }

      // Second, add policies whose value is currently set but whose name is not
      // recognized.
      for (var name in values) {
        if (!(name in this.policies_))
          this.setPolicyValue_(name, values[name], true);
      }

      // Finally, add known policies whose value is not currently set.
      for (var i = 0; i < unset.length; i++)
        this.setPolicyValue_(unset[i], undefined, false);

      // Filter the policies.
      this.filter();
    },

    /**
     * Set the filter pattern. Only policies whose name contains |pattern| are
     * shown in the policy table. The filter is case insensitive. It can be
     * disabled by setting |pattern| to an empty string.
     * @param {string} pattern The filter pattern.
     */
    setFilterPattern: function(pattern) {
      this.filterPattern_ = pattern.toLowerCase();
      this.filter();
    },

    /**
     * Filter policies. Only policies whose name contains the filter pattern are
     * shown in the table. Furthermore, policies whose value is not currently
     * set are only shown if the corresponding checkbox is checked.
     */
    filter: function() {
      var showUnset = $('show-unset').checked;
      var policies = this.getElementsByTagName('tbody');
      for (var i = 0; i < policies.length; i++) {
        var policy = policies[i];
        policy.hidden =
            policy.unset && !showUnset ||
            policy.name.toLowerCase().indexOf(this.filterPattern_) == -1;
      }
      if (this.querySelector('tbody:not([hidden])'))
        this.parentElement.classList.remove('empty');
      else
        this.parentElement.classList.add('empty');
      setTimeout(this.checkOverflow_.bind(this), 0);
    },

    /**
     * Check the table columns for overflow.
     * @private
     */
    checkOverflow_: function() {
      var policies = this.getElementsByTagName('tbody');
      for (var i = 0; i < policies.length; i++) {
        if (!policies[i].hidden)
          policies[i].checkOverflow();
      }
    },

    /**
     * Add a policy with the given |name| and |value| to the table.
     * @param {string} name The policy name.
     * @param {Object} value Dictionary with information about the policy value.
     * @param {boolean} unknown Whether the policy name is not recoginzed.
     * @private
     */
    setPolicyValue_: function(name, value, unknown) {
      var policy = new Policy;
      policy.initialize(name, value, unknown);
      this.appendChild(policy);
    },
  };

  /**
   * A singelton object that handles communication between browser and WebUI.
   * @constructor
   */
  function Page() {
  }

  // Make Page a singleton.
  cr.addSingletonGetter(Page);

  /**
   * Provide a list of all known policies to the UI. Called by the browser on
   * page load.
   * @param {Object} names Dictionary containing all known policy names.
   */
  Page.setPolicyNames = function(names) {
    var page = this.getInstance();

    // Clear all policy tables.
    page.mainSection.innerHTML = '';
    page.policyTables = {};

    // Create tables and set known policy names for Chrome and extensions.
    if (names.hasOwnProperty('chromePolicyNames')) {
      var table = page.appendNewTable('chrome', 'Chrome policies', '');
      table.setPolicyNames(names.chromePolicyNames);
    }

    if (names.hasOwnProperty('extensionPolicyNames')) {
      for (var ext in names.extensionPolicyNames) {
        var table = page.appendNewTable('extension-' + ext,
            names.extensionPolicyNames[ext].name, 'ID: ' + ext);
        table.setPolicyNames(names.extensionPolicyNames[ext].policyNames);
      }
    }
  };

  /**
   * Provide a list of the currently set policy values and any errors detected
   * while parsing these to the UI. Called by the browser on page load and
   * whenever policy values change.
   * @param {Object} values Dictionary containing the current policy values.
   */
  Page.setPolicyValues = function(values) {
    var page = this.getInstance();
    if (values.hasOwnProperty('chromePolicies')) {
      var table = page.policyTables['chrome'];
      table.setPolicyValues(values.chromePolicies);
    }

    if (values.hasOwnProperty('extensionPolicies')) {
      for (var extensionId in values.extensionPolicies) {
        var table = page.policyTables['extension-' + extensionId];
        if (table)
          table.setPolicyValues(values.extensionPolicies[extensionId]);
      }
    }
  };

  /**
   * Provide the current cloud policy status to the UI. Called by the browser on
   * page load if cloud policy is present and whenever the status changes.
   * @param {Object} status Dictionary containing the current policy status.
   */
  Page.setStatus = function(status) {
    this.getInstance().setStatus(status);
  };

  /**
   * Notify the UI that a request to reload policy values has completed. Called
   * by the browser after a request to reload policy has been sent by the UI.
   */
  Page.reloadPoliciesDone = function() {
    this.getInstance().reloadPoliciesDone();
  };

  Page.prototype = {
    /**
     * Main initialization function. Called by the browser on page load.
     */
    initialize: function() {
      uber.onContentFrameLoaded();
      cr.ui.FocusOutlineManager.forDocument(document);

      this.mainSection = $('main-section');
      this.policyTables = {};

      // Place the initial focus on the filter input field.
      $('filter').focus();

      var self = this;
      $('filter').onsearch = function(event) {
        for (policyTable in self.policyTables) {
          self.policyTables[policyTable].setFilterPattern(this.value);
        }
      };
      $('reload-policies').onclick = function(event) {
        this.disabled = true;
        chrome.send('reloadPolicies');
      };

      $('show-unset').onchange = function() {
        for (policyTable in self.policyTables) {
          self.policyTables[policyTable].filter();
        }
      };

      // Notify the browser that the page has loaded, causing it to send the
      // list of all known policies, the current policy values and the cloud
      // policy status.
      chrome.send('initialized');
    },

   /**
     * Creates a new policy table section, adds the section to the page,
     * and returns the new table from that section.
     * @param {string} id The key for storing the new table in policyTables.
     * @param {string} label_title Title for this policy table.
     * @param {string} label_content Description for the policy table.
     * @return {Element} The newly created table.
     */
    appendNewTable: function(id, label_title, label_content) {
      var newSection = this.createPolicyTableSection(id, label_title,
          label_content);
      this.mainSection.appendChild(newSection);
      return this.policyTables[id];
    },

    /**
     * Creates a new section containing a title, description and table of
     * policies.
     * @param {id} id The key for storing the new table in policyTables.
     * @param {string} label_title Title for this policy table.
     * @param {string} label_content Description for the policy table.
     * @return {Element} The newly created section.
     */
    createPolicyTableSection: function(id, label_title, label_content) {
      var section = document.createElement('section');
      section.setAttribute('class', 'policy-table-section');

      // Add title and description.
      var title = window.document.createElement('h3');
      title.textContent = label_title;
      section.appendChild(title);

      if (label_content) {
        var description = window.document.createElement('div');
        description.classList.add('table-description');
        description.textContent = label_content;
        section.appendChild(description);
      }

      // Add 'No Policies Set' element.
      var noPolicies = window.document.createElement('div');
      noPolicies.classList.add('no-policies-set');
      noPolicies.textContent = loadTimeData.getString('noPoliciesSet');
      section.appendChild(noPolicies);

      // Add table of policies.
      var newTable = this.createPolicyTable();
      this.policyTables[id] = newTable;
      section.appendChild(newTable);

      return section;
    },

    /**
     * Creates a new table for displaying policies.
     * @return {Element} The newly created table.
     */
    createPolicyTable: function() {
      var newTable = window.document.createElement('table');
      var tableHead = window.document.createElement('thead');
      var tableRow = window.document.createElement('tr');
      var tableHeadings = ['Scope', 'Level', 'Name', 'Value', 'Status'];
      for (var i = 0; i < tableHeadings.length; i++) {
        var tableHeader = window.document.createElement('th');
        tableHeader.classList.add(tableHeadings[i].toLowerCase() + '-column');
        tableHeader.textContent = loadTimeData.getString('header' +
                                                         tableHeadings[i]);
        tableRow.appendChild(tableHeader);
      }
      tableHead.appendChild(tableRow);
      newTable.appendChild(tableHead);
      cr.ui.decorate(newTable, PolicyTable);
      return newTable;
    },

    /**
     * Update the status section of the page to show the current cloud policy
     * status.
     * @param {Object} status Dictionary containing the current policy status.
     */
    setStatus: function(status) {
      // Remove any existing status boxes.
      var container = $('status-box-container');
      while (container.firstChild)
        container.removeChild(container.firstChild);
      // Hide the status section.
      var section = $('status-section');
      section.hidden = true;

      // Add a status box for each scope that has a cloud policy status.
      for (var scope in status) {
        var box = new StatusBox;
        box.initialize(scope, status[scope]);
        container.appendChild(box);
        // Show the status section.
        section.hidden = false;
      }
    },

    /**
     * Re-enable the reload policies button when the previous request to reload
     * policies values has completed.
     */
    reloadPoliciesDone: function() {
      $('reload-policies').disabled = false;
    },
  };

  return {
    Page: Page
  };
});

// Have the main initialization function be called when the page finishes
// loading.
document.addEventListener(
    'DOMContentLoaded',
    policy.Page.getInstance().initialize.bind(policy.Page.getInstance()));