// 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; // Populate the device naming information. // Populate the asset identifier. var assetId = this.querySelector('.asset-id'); assetId.textContent = status.assetId || loadTimeData.getString('notSpecified'); assetId.parentElement.hidden = false; // Populate the device location. var location = this.querySelector('.location'); location.textContent = status.location || loadTimeData.getString('notSpecified'); location.parentElement.hidden = false; // Populate the directory API ID. var directoryApiId = this.querySelector('.directory-api-id'); directoryApiId.textContent = status.directoryApiId || loadTimeData.getString('notSpecified'); directoryApiId.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 {HTMLTableElement} */ var PolicyTable = cr.ui.define('tbody'); PolicyTable.prototype = { // Set up the prototype chain. __proto__: HTMLTableElement.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()));