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