chromium/chrome/browser/resources/settings/site_settings/chooser_exception_list.ts

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * 'chooser-exception-list' shows a list of chooser exceptions for a given
 * chooser type.
 */
import 'chrome://resources/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/cr_elements/cr_tooltip/cr_tooltip.js';
import '../settings_shared.css.js';
import '../i18n_setup.js';
import './chooser_exception_list_entry.js';

import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {ListPropertyUpdateMixin} from 'chrome://resources/cr_elements/list_property_update_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import type {CrTooltipElement} from 'chrome://resources/cr_elements/cr_tooltip/cr_tooltip.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {TooltipMixin} from '../tooltip_mixin.js';

import {getTemplate} from './chooser_exception_list.html.js';
import type {ContentSettingsTypes} from './constants.js';
import {ChooserType} from './constants.js';
import {SiteSettingsMixin} from './site_settings_mixin.js';
import type {ChooserException, RawChooserException, SiteException} from './site_settings_prefs_browser_proxy.js';

export interface ChooserExceptionListElement {
  $: {
    confirmResetSettings: CrDialogElement,
    tooltip: CrTooltipElement,
  };
}

const ChooserExceptionListElementBase = TooltipMixin(ListPropertyUpdateMixin(
    SiteSettingsMixin(WebUiListenerMixin(I18nMixin(PolymerElement)))));

export class ChooserExceptionListElement extends
    ChooserExceptionListElementBase {
  static get is() {
    return 'chooser-exception-list';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /**
       * Array of chooser exceptions to display in the widget.
       */
      chooserExceptions: {
        type: Array,
        value() {
          return [];
        },
      },

      /**
       * The string ID of the chooser type that this element is displaying data
       * for.
       * See site_settings/constants.js for possible values.
       */
      chooserType: {
        observer: 'chooserTypeChanged_',
        type: String,
        value: ChooserType.NONE,
      },

      emptyListMessage_: {
        type: String,
        value: '',
      },

      hasIncognito_: Boolean,

      resetPermissionsMessage_: {
        type: String,
        value: '',
      },

      tooltipText_: String,
    };
  }

  chooserExceptions: ChooserException[];
  chooserType: ChooserType;
  private emptyListMessage_: string;
  private hasIncognito_: boolean;
  private resetPermissionsMessage_: string;
  private tooltipText_: string;

  override connectedCallback() {
    super.connectedCallback();

    this.addWebUiListener(
        'contentSettingChooserPermissionChanged',
        (category: ContentSettingsTypes, chooserType: ChooserType) => {
          this.objectWithinChooserTypeChanged_(category, chooserType);
        });
    this.addWebUiListener(
        'onIncognitoStatusChanged',
        (hasIncognito: boolean) =>
            this.onIncognitoStatusChanged_(hasIncognito));
    this.browserProxy.updateIncognitoStatus();
  }

  /**
   * Called when a chooser exception changes permission and updates the element
   * if |category| is equal to the settings category of this element.
   * @param category The content settings type that represents this permission
   *     category.
   * @param chooserType The content settings type that represents the chooser
   *     data for this permission.
   */
  private objectWithinChooserTypeChanged_(
      category: ContentSettingsTypes, chooserType: ChooserType) {
    if (category === this.category && chooserType === this.chooserType) {
      this.chooserTypeChanged_();
    }
  }

  /**
   * Called for each chooser-exception-list when incognito is enabled or
   * disabled. Only called on change (opening N incognito windows only fires one
   * message). Another message is sent when the *last* incognito window closes.
   */
  private onIncognitoStatusChanged_(hasIncognito: boolean) {
    this.hasIncognito_ = hasIncognito;
    this.populateList_();
  }

  /**
   * Configures the visibility of the widget and shows the list.
   */
  private chooserTypeChanged_() {
    if (this.chooserType === ChooserType.NONE) {
      return;
    }

    // Set the message to display when the exception list is empty.
    switch (this.chooserType) {
      case ChooserType.USB_DEVICES:
        this.emptyListMessage_ = this.i18n('noUsbDevicesFound');
        this.resetPermissionsMessage_ = this.i18n('resetUsbConfirmation');
        break;
      case ChooserType.SERIAL_PORTS:
        this.emptyListMessage_ = this.i18n('noSerialPortsFound');
        this.resetPermissionsMessage_ =
            this.i18n('resetSerialPortsConfirmation');
        break;
      case ChooserType.HID_DEVICES:
        this.emptyListMessage_ = this.i18n('noHidDevicesFound');
        this.resetPermissionsMessage_ = this.i18n('resetHidConfirmation');
        break;
      case ChooserType.BLUETOOTH_DEVICES:
        this.emptyListMessage_ = this.i18n('noBluetoothDevicesFound');
        this.resetPermissionsMessage_ = this.i18n('resetBluetoothConfirmation');
        break;
      default:
        this.emptyListMessage_ = '';
        this.resetPermissionsMessage_ = '';
    }

    this.populateList_();
  }

  /**
   * @return true if there are any chooser exceptions for this chooser type.
   */
  private hasExceptions_(): boolean {
    return this.chooserExceptions.length > 0;
  }

  /**
   * Need to use a common tooltip since the tooltip in the entry is cut off from
   * the iron-list.
   */
  private onShowTooltip_(e: CustomEvent<{target: HTMLElement, text: string}>) {
    this.tooltipText_ = e.detail.text;
    // cr-tooltip normally determines the target from the |for| property,
    // which is a selector. Here cr-tooltip is being reused by multiple
    // potential targets.
    this.showTooltipAtTarget(this.$.tooltip, e.detail.target);
  }

  /**
   * Populate the chooser exception list for display.
   */
  private populateList_() {
    this.browserProxy.getChooserExceptionList(this.chooserType)
        .then(exceptionList => this.processExceptions_(exceptionList));
  }

  /**
   * Process the chooser exception list returned from the native layer.
   */
  private processExceptions_(exceptionList: RawChooserException[]) {
    const exceptions = exceptionList.map(exception => {
      const sites = exception.sites.map(site => this.expandSiteException(site));
      return Object.assign(exception, {sites});
    });

    if (!this.updateList(
            'chooserExceptions', x => x.displayName, exceptions,
            true /* identityBasedUpdate= */)) {
      // The chooser objects have not been changed, so check if their site
      // permissions have changed. The |exceptions| and |this.chooserExceptions|
      // arrays should be the same length.
      const siteUidGetter = (x: SiteException) =>
          x.origin + x.embeddingOrigin + x.incognito;
      exceptions.forEach((exception, index) => {
        const propertyPath = 'chooserExceptions.' + index + '.sites';
        this.updateList(propertyPath, siteUidGetter, exception.sites);
      }, this);
    }
  }

  /**
   * Confirms the resetting of all content settings for an origin.
   */
  private onConfirmClearSettings_(e: Event) {
    e.preventDefault();
    this.$.confirmResetSettings.showModal();
  }

  private onCloseDialog_(e: Event) {
    (e.target as HTMLElement).closest('cr-dialog')!.close();
  }

  /**
   * Resets all permissions for the current origin.
   */
  private onResetSettings_(e: Event) {
    this.chooserExceptions.forEach(exception => {
      exception.sites.forEach(site => {
        this.browserProxy.resetChooserExceptionForSite(
            exception.chooserType, site.origin, exception.object);
      });
    });

    this.onCloseDialog_(e);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'chooser-exception-list': ChooserExceptionListElement;
  }
}

customElements.define(
    ChooserExceptionListElement.is, ChooserExceptionListElement);