Home | History | Annotate | Download | only in resources
      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.
      4 cr.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.
    557 document.addEventListener(
    558     'DOMContentLoaded',
    559     policy.Page.getInstance().initialize.bind(policy.Page.getInstance()));
    560