Home | History | Annotate | Download | only in options
      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