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 5 cr.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 = 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 } else { 231 return loadTimeData.getStringF('embeddedOnHost', 232 this.dataItem.embeddingOrigin); 233 } 234 235 return this.dataItem.displayPattern; 236 }, 237 set pattern(pattern) { 238 if (!this.editable) 239 console.error('Tried to change uneditable pattern'); 240 241 this.dataItem.displayPattern = pattern; 242 }, 243 244 /** 245 * The setting (allow/block) for the exception. 246 * 247 * @type {string} 248 */ 249 get setting() { 250 return this.dataItem.setting; 251 }, 252 set setting(setting) { 253 this.dataItem.setting = setting; 254 }, 255 256 /** 257 * Gets a human-readable setting string. 258 * 259 * @return {string} The display string. 260 */ 261 settingForDisplay: function() { 262 return this.getDisplayStringForSetting(this.setting); 263 }, 264 265 /** 266 * media video specific function. 267 * Gets a human-readable video setting string. 268 * 269 * @return {string} The display string. 270 */ 271 videoSettingForDisplay: function() { 272 return this.getDisplayStringForSetting(this.dataItem.video); 273 }, 274 275 /** 276 * Gets a human-readable display string for setting. 277 * 278 * @param {string} setting The setting to be displayed. 279 * @return {string} The display string. 280 */ 281 getDisplayStringForSetting: function(setting) { 282 if (setting == 'allow') 283 return loadTimeData.getString('allowException'); 284 else if (setting == 'block') 285 return loadTimeData.getString('blockException'); 286 else if (setting == 'ask') 287 return loadTimeData.getString('askException'); 288 else if (setting == 'session') 289 return loadTimeData.getString('sessionException'); 290 else if (setting == 'default') 291 return ''; 292 293 console.error('Unknown setting: [' + setting + ']'); 294 return ''; 295 }, 296 297 /** 298 * Update this list item to reflect whether the input is a valid pattern. 299 * 300 * @param {boolean} valid Whether said pattern is valid in the context of a 301 * content exception setting. 302 */ 303 setPatternValid: function(valid) { 304 if (valid || !this.input.value) 305 this.input.setCustomValidity(''); 306 else 307 this.input.setCustomValidity(' '); 308 this.inputIsValid = valid; 309 this.inputValidityKnown = true; 310 }, 311 312 /** 313 * Set the <input> to its original contents. Used when the user quits 314 * editing. 315 */ 316 resetInput: function() { 317 this.input.value = this.pattern; 318 }, 319 320 /** 321 * Copy the data model values to the editable nodes. 322 */ 323 updateEditables: function() { 324 this.resetInput(); 325 326 var settingOption = 327 this.select.querySelector('[value=\'' + this.setting + '\']'); 328 if (settingOption) 329 settingOption.selected = true; 330 }, 331 332 /** @override */ 333 get currentInputIsValid() { 334 return this.inputValidityKnown && this.inputIsValid; 335 }, 336 337 /** @override */ 338 get hasBeenEdited() { 339 var livePattern = this.input.value; 340 var liveSetting = this.select.value; 341 return livePattern != this.pattern || liveSetting != this.setting; 342 }, 343 344 /** 345 * Called when committing an edit. 346 * 347 * @param {Event} e The end event. 348 * @private 349 */ 350 onEditCommitted_: function(e) { 351 var newPattern = this.input.value; 352 var newSetting = this.select.value; 353 354 this.finishEdit(newPattern, newSetting); 355 }, 356 357 /** 358 * Called when cancelling an edit; resets the control states. 359 * 360 * @param {Event} e The cancel event. 361 * @private 362 */ 363 onEditCancelled_: function() { 364 this.updateEditables(); 365 this.setPatternValid(true); 366 }, 367 368 /** 369 * Editing is complete; update the model. 370 * 371 * @param {string} newPattern The pattern that the user entered. 372 * @param {string} newSetting The setting the user chose. 373 */ 374 finishEdit: function(newPattern, newSetting) { 375 this.patternLabel.textContent = newPattern; 376 this.settingLabel.textContent = this.settingForDisplay(); 377 var oldPattern = this.pattern; 378 this.pattern = newPattern; 379 this.setting = newSetting; 380 381 // TODO(estade): this will need to be updated if geolocation/notifications 382 // become editable. 383 if (oldPattern != newPattern) { 384 chrome.send('removeException', 385 [this.contentType, this.mode, oldPattern]); 386 } 387 388 chrome.send('setException', 389 [this.contentType, this.mode, newPattern, newSetting]); 390 }, 391 }; 392 393 /** 394 * Creates a new list item for the Add New Item row, which doesn't represent 395 * an actual entry in the exceptions list but allows the user to add new 396 * exceptions. 397 * 398 * @param {string} contentType The type of the list. 399 * @param {string} mode The browser mode, 'otr' or 'normal'. 400 * @param {boolean} enableAskOption Whether to show an 'ask every time' option 401 * in the select. 402 * @constructor 403 * @extends {cr.ui.ExceptionsListItem} 404 */ 405 function ExceptionsAddRowListItem(contentType, mode, enableAskOption) { 406 var el = cr.doc.createElement('div'); 407 el.mode = mode; 408 el.contentType = contentType; 409 el.enableAskOption = enableAskOption; 410 el.dataItem = []; 411 el.__proto__ = ExceptionsAddRowListItem.prototype; 412 el.decorate(); 413 414 return el; 415 } 416 417 ExceptionsAddRowListItem.prototype = { 418 __proto__: ExceptionsListItem.prototype, 419 420 decorate: function() { 421 ExceptionsListItem.prototype.decorate.call(this); 422 423 this.input.placeholder = 424 loadTimeData.getString('addNewExceptionInstructions'); 425 426 // Do we always want a default of allow? 427 this.setting = 'allow'; 428 }, 429 430 /** 431 * Clear the <input> and let the placeholder text show again. 432 */ 433 resetInput: function() { 434 this.input.value = ''; 435 }, 436 437 /** @override */ 438 get hasBeenEdited() { 439 return this.input.value != ''; 440 }, 441 442 /** 443 * Editing is complete; update the model. As long as the pattern isn't 444 * empty, we'll just add it. 445 * 446 * @param {string} newPattern The pattern that the user entered. 447 * @param {string} newSetting The setting the user chose. 448 */ 449 finishEdit: function(newPattern, newSetting) { 450 this.resetInput(); 451 chrome.send('setException', 452 [this.contentType, this.mode, newPattern, newSetting]); 453 }, 454 }; 455 456 /** 457 * Creates a new exceptions list. 458 * 459 * @constructor 460 * @extends {cr.ui.List} 461 */ 462 var ExceptionsList = cr.ui.define('list'); 463 464 ExceptionsList.prototype = { 465 __proto__: InlineEditableItemList.prototype, 466 467 /** 468 * Called when an element is decorated as a list. 469 */ 470 decorate: function() { 471 InlineEditableItemList.prototype.decorate.call(this); 472 473 this.classList.add('settings-list'); 474 475 for (var parentNode = this.parentNode; parentNode; 476 parentNode = parentNode.parentNode) { 477 if (parentNode.hasAttribute('contentType')) { 478 this.contentType = parentNode.getAttribute('contentType'); 479 break; 480 } 481 } 482 483 this.mode = this.getAttribute('mode'); 484 485 // Whether the exceptions in this list allow an 'Ask every time' option. 486 this.enableAskOption = this.contentType == 'plugins'; 487 488 this.autoExpands = true; 489 this.reset(); 490 }, 491 492 /** 493 * Creates an item to go in the list. 494 * 495 * @param {Object} entry The element from the data model for this row. 496 */ 497 createItem: function(entry) { 498 if (entry) { 499 return new ExceptionsListItem(this.contentType, 500 this.mode, 501 this.enableAskOption, 502 entry); 503 } else { 504 var addRowItem = new ExceptionsAddRowListItem(this.contentType, 505 this.mode, 506 this.enableAskOption); 507 addRowItem.deletable = false; 508 return addRowItem; 509 } 510 }, 511 512 /** 513 * Sets the exceptions in the js model. 514 * 515 * @param {Object} entries A list of dictionaries of values, each dictionary 516 * represents an exception. 517 */ 518 setExceptions: function(entries) { 519 var deleteCount = this.dataModel.length; 520 521 if (this.isEditable()) { 522 // We don't want to remove the Add New Exception row. 523 deleteCount = deleteCount - 1; 524 } 525 526 var args = [0, deleteCount]; 527 args.push.apply(args, entries); 528 this.dataModel.splice.apply(this.dataModel, args); 529 }, 530 531 /** 532 * The browser has finished checking a pattern for validity. Update the list 533 * item to reflect this. 534 * 535 * @param {string} pattern The pattern. 536 * @param {bool} valid Whether said pattern is valid in the context of a 537 * content exception setting. 538 */ 539 patternValidityCheckComplete: function(pattern, valid) { 540 var listItems = this.items; 541 for (var i = 0; i < listItems.length; i++) { 542 var listItem = listItems[i]; 543 // Don't do anything for messages for the item if it is not the intended 544 // recipient, or if the response is stale (i.e. the input value has 545 // changed since we sent the request to analyze it). 546 if (pattern == listItem.input.value) 547 listItem.setPatternValid(valid); 548 } 549 }, 550 551 /** 552 * Returns whether the rows are editable in this list. 553 */ 554 isEditable: function() { 555 // Exceptions of the following lists are not editable for now. 556 return !(this.contentType == 'notifications' || 557 this.contentType == 'location' || 558 this.contentType == 'fullscreen' || 559 this.contentType == 'media-stream' || 560 this.contentType == 'zoomlevels'); 561 }, 562 563 /** 564 * Removes all exceptions from the js model. 565 */ 566 reset: function() { 567 if (this.isEditable()) { 568 // The null creates the Add New Exception row. 569 this.dataModel = new ArrayDataModel([null]); 570 } else { 571 this.dataModel = new ArrayDataModel([]); 572 } 573 }, 574 575 /** @override */ 576 deleteItemAtIndex: function(index) { 577 var listItem = this.getListItemByIndex(index); 578 if (!listItem.deletable) 579 return; 580 581 var dataItem = listItem.dataItem; 582 var args = [listItem.contentType]; 583 if (listItem.contentType == 'notifications') 584 args.push(dataItem.origin, dataItem.setting); 585 else 586 args.push(listItem.mode, dataItem.origin, dataItem.embeddingOrigin); 587 588 chrome.send('removeException', args); 589 }, 590 }; 591 592 var OptionsPage = options.OptionsPage; 593 594 /** 595 * Encapsulated handling of content settings list subpage. 596 * 597 * @constructor 598 */ 599 function ContentSettingsExceptionsArea() { 600 OptionsPage.call(this, 'contentExceptions', 601 loadTimeData.getString('contentSettingsPageTabTitle'), 602 'content-settings-exceptions-area'); 603 } 604 605 cr.addSingletonGetter(ContentSettingsExceptionsArea); 606 607 ContentSettingsExceptionsArea.prototype = { 608 __proto__: OptionsPage.prototype, 609 610 initializePage: function() { 611 OptionsPage.prototype.initializePage.call(this); 612 613 var exceptionsLists = this.pageDiv.querySelectorAll('list'); 614 for (var i = 0; i < exceptionsLists.length; i++) { 615 options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]); 616 } 617 618 ContentSettingsExceptionsArea.hideOTRLists(false); 619 620 // If the user types in the URL without a hash, show just cookies. 621 this.showList('cookies'); 622 623 $('content-settings-exceptions-overlay-confirm').onclick = 624 OptionsPage.closeOverlay.bind(OptionsPage); 625 }, 626 627 /** 628 * Shows one list and hides all others. 629 * 630 * @param {string} type The content type. 631 */ 632 showList: function(type) { 633 // Update the title for the type that was shown. 634 this.title = loadTimeData.getString(type + 'TabTitle'); 635 636 var header = this.pageDiv.querySelector('h1'); 637 header.textContent = loadTimeData.getString(type + '_header'); 638 639 var divs = this.pageDiv.querySelectorAll('div[contentType]'); 640 for (var i = 0; i < divs.length; i++) { 641 if (divs[i].getAttribute('contentType') == type) 642 divs[i].hidden = false; 643 else 644 divs[i].hidden = true; 645 } 646 647 var mediaHeader = this.pageDiv.querySelector('.media-header'); 648 mediaHeader.hidden = type != 'media-stream'; 649 650 $('exception-behavior-column').hidden = type == 'zoomlevels'; 651 $('exception-zoom-column').hidden = type != 'zoomlevels'; 652 }, 653 654 /** 655 * Called after the page has been shown. Show the content type for the 656 * location's hash. 657 */ 658 didShowPage: function() { 659 var hash = location.hash; 660 if (hash) 661 this.showList(hash.slice(1)); 662 }, 663 }; 664 665 /** 666 * Called when the last incognito window is closed. 667 */ 668 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() { 669 this.hideOTRLists(true); 670 }; 671 672 /** 673 * Hides the incognito exceptions lists and optionally clears them as well. 674 * @param {boolean} clear Whether to clear the lists. 675 */ 676 ContentSettingsExceptionsArea.hideOTRLists = function(clear) { 677 var otrLists = document.querySelectorAll('list[mode=otr]'); 678 679 for (var i = 0; i < otrLists.length; i++) { 680 otrLists[i].parentNode.hidden = true; 681 if (clear) 682 otrLists[i].reset(); 683 } 684 }; 685 686 return { 687 ExceptionsListItem: ExceptionsListItem, 688 ExceptionsAddRowListItem: ExceptionsAddRowListItem, 689 ExceptionsList: ExceptionsList, 690 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea, 691 }; 692 }); 693