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