1// Copyright (c) 2012 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 5cr.define('options.contentSettings', function() { 6 /** @const */ var ControlledSettingIndicator = 7 options.ControlledSettingIndicator; 8 /** @const */ var InlineEditableItemList = options.InlineEditableItemList; 9 /** @const */ var InlineEditableItem = options.InlineEditableItem; 10 /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; 11 12 /** 13 * Creates a new exceptions list item. 14 * 15 * @param {string} contentType The type of the list. 16 * @param {string} mode The browser mode, 'otr' or 'normal'. 17 * @param {boolean} enableAskOption Whether to show an 'ask every time' 18 * option in the select. 19 * @param {Object} exception A dictionary that contains the data of the 20 * exception. 21 * @constructor 22 * @extends {options.InlineEditableItem} 23 */ 24 function ExceptionsListItem(contentType, mode, enableAskOption, exception) { 25 var el = cr.doc.createElement('div'); 26 el.mode = mode; 27 el.contentType = contentType; 28 el.enableAskOption = enableAskOption; 29 el.dataItem = exception; 30 el.__proto__ = ExceptionsListItem.prototype; 31 el.decorate(); 32 33 return el; 34 } 35 36 ExceptionsListItem.prototype = { 37 __proto__: InlineEditableItem.prototype, 38 39 /** 40 * Called when an element is decorated as a list item. 41 */ 42 decorate: function() { 43 InlineEditableItem.prototype.decorate.call(this); 44 45 this.isPlaceholder = !this.pattern; 46 var patternCell = this.createEditableTextCell(this.pattern); 47 patternCell.className = 'exception-pattern'; 48 patternCell.classList.add('weakrtl'); 49 this.contentElement.appendChild(patternCell); 50 if (this.pattern) 51 this.patternLabel = patternCell.querySelector('.static-text'); 52 var input = patternCell.querySelector('input'); 53 54 // TODO(stuartmorgan): Create an createEditableSelectCell abstracting 55 // this code. 56 // Setting label for display mode. |pattern| will be null for the 'add new 57 // exception' row. 58 if (this.pattern) { 59 var settingLabel = cr.doc.createElement('span'); 60 settingLabel.textContent = this.settingForDisplay(); 61 settingLabel.className = 'exception-setting'; 62 settingLabel.setAttribute('displaymode', 'static'); 63 this.contentElement.appendChild(settingLabel); 64 this.settingLabel = settingLabel; 65 } 66 67 // Setting select element for edit mode. 68 var select = cr.doc.createElement('select'); 69 var optionAllow = cr.doc.createElement('option'); 70 optionAllow.textContent = loadTimeData.getString('allowException'); 71 optionAllow.value = 'allow'; 72 select.appendChild(optionAllow); 73 74 if (this.enableAskOption) { 75 var optionAsk = cr.doc.createElement('option'); 76 optionAsk.textContent = loadTimeData.getString('askException'); 77 optionAsk.value = 'ask'; 78 select.appendChild(optionAsk); 79 } 80 81 if (this.contentType == 'cookies') { 82 var optionSession = cr.doc.createElement('option'); 83 optionSession.textContent = loadTimeData.getString('sessionException'); 84 optionSession.value = 'session'; 85 select.appendChild(optionSession); 86 } 87 88 if (this.contentType != 'fullscreen') { 89 var optionBlock = cr.doc.createElement('option'); 90 optionBlock.textContent = loadTimeData.getString('blockException'); 91 optionBlock.value = 'block'; 92 select.appendChild(optionBlock); 93 } 94 95 if (this.isEmbeddingRule()) { 96 this.patternLabel.classList.add('sublabel'); 97 this.editable = false; 98 } 99 100 if (this.setting == 'default') { 101 // Items that don't have their own settings (parents of 'embedded on' 102 // items) aren't deletable. 103 this.deletable = false; 104 this.editable = false; 105 } 106 107 if (this.contentType != 'zoomlevels') { 108 this.addEditField(select, this.settingLabel); 109 this.contentElement.appendChild(select); 110 } 111 select.className = 'exception-setting'; 112 select.setAttribute('aria-labelledby', 'exception-behavior-column'); 113 114 if (this.pattern) 115 select.setAttribute('displaymode', 'edit'); 116 117 if (this.contentType == 'media-stream') { 118 this.settingLabel.classList.add('media-audio-setting'); 119 120 var videoSettingLabel = cr.doc.createElement('span'); 121 videoSettingLabel.textContent = this.videoSettingForDisplay(); 122 videoSettingLabel.className = 'exception-setting'; 123 videoSettingLabel.classList.add('media-video-setting'); 124 videoSettingLabel.setAttribute('displaymode', 'static'); 125 this.contentElement.appendChild(videoSettingLabel); 126 } 127 128 if (this.contentType == 'zoomlevels') { 129 this.deletable = true; 130 this.editable = false; 131 132 var zoomLabel = cr.doc.createElement('span'); 133 zoomLabel.textContent = this.dataItem.zoom; 134 zoomLabel.className = 'exception-setting'; 135 zoomLabel.setAttribute('displaymode', 'static'); 136 zoomLabel.setAttribute('aria-labelledby', 'exception-zoom-column'); 137 this.contentElement.appendChild(zoomLabel); 138 this.zoomLabel = zoomLabel; 139 } 140 141 // Used to track whether the URL pattern in the input is valid. 142 // This will be true if the browser process has informed us that the 143 // current text in the input is valid. Changing the text resets this to 144 // false, and getting a response from the browser sets it back to true. 145 // It starts off as false for empty string (new exceptions) or true for 146 // already-existing exceptions (which we assume are valid). 147 this.inputValidityKnown = this.pattern; 148 // This one tracks the actual validity of the pattern in the input. This 149 // starts off as true so as not to annoy the user when he adds a new and 150 // empty input. 151 this.inputIsValid = true; 152 153 this.input = input; 154 this.select = select; 155 156 this.updateEditables(); 157 158 // Editing notifications, geolocation and media-stream is disabled for 159 // now. 160 if (this.contentType == 'notifications' || 161 this.contentType == 'location' || 162 this.contentType == 'media-stream') { 163 this.editable = false; 164 } 165 166 // If the source of the content setting exception is not a user 167 // preference, that source controls the exception and the user cannot edit 168 // or delete it. 169 var controlledBy = 170 this.dataItem.source && this.dataItem.source != 'preference' ? 171 this.dataItem.source : null; 172 173 if (controlledBy) { 174 this.setAttribute('controlled-by', controlledBy); 175 this.deletable = false; 176 this.editable = false; 177 } 178 179 if (controlledBy == 'policy' || controlledBy == 'extension') { 180 this.querySelector('.row-delete-button').hidden = true; 181 var indicator = new ControlledSettingIndicator(); 182 indicator.setAttribute('content-exception', this.contentType); 183 // Create a synthetic pref change event decorated as 184 // CoreOptionsHandler::CreateValueForPref() does. 185 var event = new Event(this.contentType); 186 event.value = { controlledBy: controlledBy }; 187 indicator.handlePrefChange(event); 188 this.appendChild(indicator); 189 } 190 191 // If the exception comes from a hosted app, display the name and the 192 // icon of the app. 193 if (controlledBy == 'HostedApp') { 194 this.title = 195 loadTimeData.getString('set_by') + ' ' + this.dataItem.appName; 196 var button = this.querySelector('.row-delete-button'); 197 // Use the host app's favicon (16px, match bigger size). 198 // See c/b/ui/webui/extensions/extension_icon_source.h 199 // for a description of the chrome://extension-icon URL. 200 button.style.backgroundImage = 201 'url(\'chrome://extension-icon/' + this.dataItem.appId + '/16/1\')'; 202 } 203 204 var listItem = this; 205 // Handle events on the editable nodes. 206 input.oninput = function(event) { 207 listItem.inputValidityKnown = false; 208 chrome.send('checkExceptionPatternValidity', 209 [listItem.contentType, listItem.mode, input.value]); 210 }; 211 212 // Listen for edit events. 213 this.addEventListener('canceledit', this.onEditCancelled_); 214 this.addEventListener('commitedit', this.onEditCommitted_); 215 }, 216 217 isEmbeddingRule: function() { 218 return this.dataItem.embeddingOrigin && 219 this.dataItem.embeddingOrigin !== this.dataItem.origin; 220 }, 221 222 /** 223 * The pattern (e.g., a URL) for the exception. 224 * 225 * @type {string} 226 */ 227 get pattern() { 228 if (!this.isEmbeddingRule()) 229 return this.dataItem.origin; 230 231 return loadTimeData.getStringF('embeddedOnHost', 232 this.dataItem.embeddingOrigin); 233 }, 234 set pattern(pattern) { 235 if (!this.editable) 236 console.error('Tried to change uneditable pattern'); 237 238 this.dataItem.displayPattern = pattern; 239 }, 240 241 /** 242 * The setting (allow/block) for the exception. 243 * 244 * @type {string} 245 */ 246 get setting() { 247 return this.dataItem.setting; 248 }, 249 set setting(setting) { 250 this.dataItem.setting = setting; 251 }, 252 253 /** 254 * Gets a human-readable setting string. 255 * 256 * @return {string} The display string. 257 */ 258 settingForDisplay: function() { 259 return this.getDisplayStringForSetting(this.setting); 260 }, 261 262 /** 263 * media video specific function. 264 * Gets a human-readable video setting string. 265 * 266 * @return {string} The display string. 267 */ 268 videoSettingForDisplay: function() { 269 return this.getDisplayStringForSetting(this.dataItem.video); 270 }, 271 272 /** 273 * Gets a human-readable display string for setting. 274 * 275 * @param {string} setting The setting to be displayed. 276 * @return {string} The display string. 277 */ 278 getDisplayStringForSetting: function(setting) { 279 if (setting == 'allow') 280 return loadTimeData.getString('allowException'); 281 else if (setting == 'block') 282 return loadTimeData.getString('blockException'); 283 else if (setting == 'ask') 284 return loadTimeData.getString('askException'); 285 else if (setting == 'session') 286 return loadTimeData.getString('sessionException'); 287 else if (setting == 'default') 288 return ''; 289 290 console.error('Unknown setting: [' + setting + ']'); 291 return ''; 292 }, 293 294 /** 295 * Update this list item to reflect whether the input is a valid pattern. 296 * 297 * @param {boolean} valid Whether said pattern is valid in the context of a 298 * content exception setting. 299 */ 300 setPatternValid: function(valid) { 301 if (valid || !this.input.value) 302 this.input.setCustomValidity(''); 303 else 304 this.input.setCustomValidity(' '); 305 this.inputIsValid = valid; 306 this.inputValidityKnown = true; 307 }, 308 309 /** 310 * Set the <input> to its original contents. Used when the user quits 311 * editing. 312 */ 313 resetInput: function() { 314 this.input.value = this.pattern; 315 }, 316 317 /** 318 * Copy the data model values to the editable nodes. 319 */ 320 updateEditables: function() { 321 this.resetInput(); 322 323 var settingOption = 324 this.select.querySelector('[value=\'' + this.setting + '\']'); 325 if (settingOption) 326 settingOption.selected = true; 327 }, 328 329 /** @override */ 330 get currentInputIsValid() { 331 return this.inputValidityKnown && this.inputIsValid; 332 }, 333 334 /** @override */ 335 get hasBeenEdited() { 336 var livePattern = this.input.value; 337 var liveSetting = this.select.value; 338 return livePattern != this.pattern || liveSetting != this.setting; 339 }, 340 341 /** 342 * Called when committing an edit. 343 * 344 * @param {Event} e The end event. 345 * @private 346 */ 347 onEditCommitted_: function(e) { 348 var newPattern = this.input.value; 349 var newSetting = this.select.value; 350 351 this.finishEdit(newPattern, newSetting); 352 }, 353 354 /** 355 * Called when cancelling an edit; resets the control states. 356 * 357 * @param {Event} e The cancel event. 358 * @private 359 */ 360 onEditCancelled_: function(e) { 361 this.updateEditables(); 362 this.setPatternValid(true); 363 }, 364 365 /** 366 * Editing is complete; update the model. 367 * 368 * @param {string} newPattern The pattern that the user entered. 369 * @param {string} newSetting The setting the user chose. 370 */ 371 finishEdit: function(newPattern, newSetting) { 372 this.patternLabel.textContent = newPattern; 373 this.settingLabel.textContent = this.settingForDisplay(); 374 var oldPattern = this.pattern; 375 this.pattern = newPattern; 376 this.setting = newSetting; 377 378 // TODO(estade): this will need to be updated if geolocation/notifications 379 // become editable. 380 if (oldPattern != newPattern) { 381 chrome.send('removeException', 382 [this.contentType, this.mode, oldPattern]); 383 } 384 385 chrome.send('setException', 386 [this.contentType, this.mode, newPattern, newSetting]); 387 }, 388 }; 389 390 /** 391 * Creates a new list item for the Add New Item row, which doesn't represent 392 * an actual entry in the exceptions list but allows the user to add new 393 * exceptions. 394 * 395 * @param {string} contentType The type of the list. 396 * @param {string} mode The browser mode, 'otr' or 'normal'. 397 * @param {boolean} enableAskOption Whether to show an 'ask every time' option 398 * in the select. 399 * @constructor 400 * @extends {options.contentSettings.ExceptionsListItem} 401 */ 402 function ExceptionsAddRowListItem(contentType, mode, enableAskOption) { 403 var el = cr.doc.createElement('div'); 404 el.mode = mode; 405 el.contentType = contentType; 406 el.enableAskOption = enableAskOption; 407 el.dataItem = []; 408 el.__proto__ = ExceptionsAddRowListItem.prototype; 409 el.decorate(); 410 411 return el; 412 } 413 414 ExceptionsAddRowListItem.prototype = { 415 __proto__: ExceptionsListItem.prototype, 416 417 decorate: function() { 418 ExceptionsListItem.prototype.decorate.call(this); 419 420 this.input.placeholder = 421 loadTimeData.getString('addNewExceptionInstructions'); 422 423 // Do we always want a default of allow? 424 this.setting = 'allow'; 425 }, 426 427 /** 428 * Clear the <input> and let the placeholder text show again. 429 */ 430 resetInput: function() { 431 this.input.value = ''; 432 }, 433 434 /** @override */ 435 get hasBeenEdited() { 436 return this.input.value != ''; 437 }, 438 439 /** 440 * Editing is complete; update the model. As long as the pattern isn't 441 * empty, we'll just add it. 442 * 443 * @param {string} newPattern The pattern that the user entered. 444 * @param {string} newSetting The setting the user chose. 445 */ 446 finishEdit: function(newPattern, newSetting) { 447 this.resetInput(); 448 chrome.send('setException', 449 [this.contentType, this.mode, newPattern, newSetting]); 450 }, 451 }; 452 453 /** 454 * Creates a new exceptions list. 455 * 456 * @constructor 457 * @extends {options.InlineEditableItemList} 458 */ 459 var ExceptionsList = cr.ui.define('list'); 460 461 ExceptionsList.prototype = { 462 __proto__: InlineEditableItemList.prototype, 463 464 /** 465 * Called when an element is decorated as a list. 466 */ 467 decorate: function() { 468 InlineEditableItemList.prototype.decorate.call(this); 469 470 this.classList.add('settings-list'); 471 472 for (var parentNode = this.parentNode; parentNode; 473 parentNode = parentNode.parentNode) { 474 if (parentNode.hasAttribute('contentType')) { 475 this.contentType = parentNode.getAttribute('contentType'); 476 break; 477 } 478 } 479 480 this.mode = this.getAttribute('mode'); 481 482 // Whether the exceptions in this list allow an 'Ask every time' option. 483 this.enableAskOption = this.contentType == 'plugins'; 484 485 this.autoExpands = true; 486 this.reset(); 487 }, 488 489 /** 490 * Creates an item to go in the list. 491 * 492 * @param {Object} entry The element from the data model for this row. 493 */ 494 createItem: function(entry) { 495 if (entry) { 496 return new ExceptionsListItem(this.contentType, 497 this.mode, 498 this.enableAskOption, 499 entry); 500 } else { 501 var addRowItem = new ExceptionsAddRowListItem(this.contentType, 502 this.mode, 503 this.enableAskOption); 504 addRowItem.deletable = false; 505 return addRowItem; 506 } 507 }, 508 509 /** 510 * Sets the exceptions in the js model. 511 * 512 * @param {Object} entries A list of dictionaries of values, each dictionary 513 * represents an exception. 514 */ 515 setExceptions: function(entries) { 516 var deleteCount = this.dataModel.length; 517 518 if (this.isEditable()) { 519 // We don't want to remove the Add New Exception row. 520 deleteCount = deleteCount - 1; 521 } 522 523 var args = [0, deleteCount]; 524 args.push.apply(args, entries); 525 this.dataModel.splice.apply(this.dataModel, args); 526 }, 527 528 /** 529 * The browser has finished checking a pattern for validity. Update the list 530 * item to reflect this. 531 * 532 * @param {string} pattern The pattern. 533 * @param {boolean} valid Whether said pattern is valid in the context of a 534 * content exception setting. 535 */ 536 patternValidityCheckComplete: function(pattern, valid) { 537 var listItems = this.items; 538 for (var i = 0; i < listItems.length; i++) { 539 var listItem = listItems[i]; 540 // Don't do anything for messages for the item if it is not the intended 541 // recipient, or if the response is stale (i.e. the input value has 542 // changed since we sent the request to analyze it). 543 if (pattern == listItem.input.value) 544 listItem.setPatternValid(valid); 545 } 546 }, 547 548 /** 549 * Returns whether the rows are editable in this list. 550 */ 551 isEditable: function() { 552 // Exceptions of the following lists are not editable for now. 553 return !(this.contentType == 'notifications' || 554 this.contentType == 'location' || 555 this.contentType == 'fullscreen' || 556 this.contentType == 'media-stream' || 557 this.contentType == 'zoomlevels'); 558 }, 559 560 /** 561 * Removes all exceptions from the js model. 562 */ 563 reset: function() { 564 if (this.isEditable()) { 565 // The null creates the Add New Exception row. 566 this.dataModel = new ArrayDataModel([null]); 567 } else { 568 this.dataModel = new ArrayDataModel([]); 569 } 570 }, 571 572 /** @override */ 573 deleteItemAtIndex: function(index) { 574 var listItem = this.getListItemByIndex(index); 575 if (!listItem.deletable) 576 return; 577 578 var dataItem = listItem.dataItem; 579 chrome.send('removeException', [listItem.contentType, 580 listItem.mode, 581 dataItem.origin, 582 dataItem.embeddingOrigin]); 583 }, 584 }; 585 586 var Page = cr.ui.pageManager.Page; 587 var PageManager = cr.ui.pageManager.PageManager; 588 589 /** 590 * Encapsulated handling of content settings list subpage. 591 * 592 * @constructor 593 * @extends {cr.ui.pageManager.Page} 594 */ 595 function ContentSettingsExceptionsArea() { 596 Page.call(this, 'contentExceptions', 597 loadTimeData.getString('contentSettingsPageTabTitle'), 598 'content-settings-exceptions-area'); 599 } 600 601 cr.addSingletonGetter(ContentSettingsExceptionsArea); 602 603 ContentSettingsExceptionsArea.prototype = { 604 __proto__: Page.prototype, 605 606 /** @override */ 607 initializePage: function() { 608 Page.prototype.initializePage.call(this); 609 610 var exceptionsLists = this.pageDiv.querySelectorAll('list'); 611 for (var i = 0; i < exceptionsLists.length; i++) { 612 options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]); 613 } 614 615 ContentSettingsExceptionsArea.hideOTRLists(false); 616 617 // If the user types in the URL without a hash, show just cookies. 618 this.showList('cookies'); 619 620 $('content-settings-exceptions-overlay-confirm').onclick = 621 PageManager.closeOverlay.bind(PageManager); 622 }, 623 624 /** 625 * Shows one list and hides all others. 626 * 627 * @param {string} type The content type. 628 */ 629 showList: function(type) { 630 // Update the title for the type that was shown. 631 this.title = loadTimeData.getString(type + 'TabTitle'); 632 633 var header = this.pageDiv.querySelector('h1'); 634 header.textContent = loadTimeData.getString(type + '_header'); 635 636 var divs = this.pageDiv.querySelectorAll('div[contentType]'); 637 for (var i = 0; i < divs.length; i++) { 638 if (divs[i].getAttribute('contentType') == type) 639 divs[i].hidden = false; 640 else 641 divs[i].hidden = true; 642 } 643 644 var mediaHeader = this.pageDiv.querySelector('.media-header'); 645 mediaHeader.hidden = type != 'media-stream'; 646 647 $('exception-behavior-column').hidden = type == 'zoomlevels'; 648 $('exception-zoom-column').hidden = type != 'zoomlevels'; 649 }, 650 651 /** 652 * Called after the page has been shown. Show the content type for the 653 * location's hash. 654 */ 655 didShowPage: function() { 656 if (this.hash) 657 this.showList(this.hash.slice(1)); 658 }, 659 }; 660 661 /** 662 * Called when the last incognito window is closed. 663 */ 664 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() { 665 this.hideOTRLists(true); 666 }; 667 668 /** 669 * Hides the incognito exceptions lists and optionally clears them as well. 670 * @param {boolean} clear Whether to clear the lists. 671 */ 672 ContentSettingsExceptionsArea.hideOTRLists = function(clear) { 673 var otrLists = document.querySelectorAll('list[mode=otr]'); 674 675 for (var i = 0; i < otrLists.length; i++) { 676 otrLists[i].parentNode.hidden = true; 677 if (clear) 678 otrLists[i].reset(); 679 } 680 }; 681 682 return { 683 ExceptionsListItem: ExceptionsListItem, 684 ExceptionsAddRowListItem: ExceptionsAddRowListItem, 685 ExceptionsList: ExceptionsList, 686 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea, 687 }; 688}); 689