chromium/chrome/browser/resources/settings/site_settings/site_list_entry.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
 * 'site-list-entry' shows an Allowed and Blocked site for a given category.
 */
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/icons.html.js';
import '/shared/settings/controls/cr_policy_pref_indicator.js';
import 'chrome://resources/cr_elements/policy/cr_tooltip_icon.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import '../icons.html.js';
import '../settings_shared.css.js';
import '../site_favicon.js';

import {FocusRowMixin} from 'chrome://resources/cr_elements/focus_row_mixin.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {BaseMixin} from '../base_mixin.js';
import {loadTimeData} from '../i18n_setup.js';
import {routes} from '../route.js';
import {Router} from '../router.js';

import {ChooserType, ContentSettingsTypes, CookiesExceptionType, SITE_EXCEPTION_WILDCARD} from './constants.js';
import {getTemplate} from './site_list_entry.html.js';
import {SiteSettingsMixin} from './site_settings_mixin.js';
import type {SiteException} from './site_settings_prefs_browser_proxy.js';

export interface SiteListEntryElement {
  $: {
    actionMenuButton: HTMLElement,
    resetSite: HTMLElement,
  };
}

const SiteListEntryElementBase =
    FocusRowMixin(BaseMixin(SiteSettingsMixin(I18nMixin(PolymerElement))));

export class SiteListEntryElement extends SiteListEntryElementBase {
  static get is() {
    return 'site-list-entry';
  }

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

  static get properties() {
    return {
      /**
       * Some content types (like Location) do not allow the user to manually
       * edit the exception list from within Settings.
       */
      readOnlyList: {
        type: Boolean,
        value: false,
      },

      /**
       * Site to display in the widget.
       */
      model: {
        type: Object,
        observer: 'onModelChanged_',
      },

      /**
       * If the site represented is part of a chooser exception, the chooser
       * type will be stored here to allow the permission to be manipulated.
       */
      chooserType: {
        type: String,
        value: ChooserType.NONE,
      },

      /**
       * If the site represented is part of a chooser exception, the chooser
       * object will be stored here to allow the permission to be manipulated.
       */
      chooserObject: {
        type: Object,
        value: null,
      },

      showPolicyPrefIndicator_: {
        type: Boolean,
        computed: 'computeShowPolicyPrefIndicator_(model)',
      },

      allowNavigateToSiteDetail_: {
        type: Boolean,
        value: false,
      },

      /**
       * Type of cookies exceptions based on the use of wildcard in the
       * patterns. See `CookiesExceptionType`.
       */
      cookiesExceptionType: String,
    };
  }

  private readOnlyList: boolean;
  model: SiteException;
  private chooserType: ChooserType;
  private chooserObject: object;
  private showPolicyPrefIndicator_: boolean;
  private allowNavigateToSiteDetail_: boolean;
  cookiesExceptionType: CookiesExceptionType;

  private onShowTooltip_() {
    const indicator =
        this.shadowRoot!.querySelector('cr-policy-pref-indicator');
    assert(!!indicator);
    // The tooltip text is used by an cr-tooltip contained inside the
    // cr-policy-pref-indicator. This text is needed here to send up to the
    // common tooltip component.
    const text = indicator.indicatorTooltip;
    this.fire('show-tooltip', {target: indicator, text});
  }

  private onShowIncognitoTooltip_() {
    const tooltip = this.shadowRoot!.querySelector('#incognitoTooltip');
    // The tooltip text is used by an cr-tooltip contained inside the
    // cr-policy-pref-indicator. The text is currently held in a private
    // property. This text is needed here to send up to the common tooltip
    // component.
    const text = loadTimeData.getString('incognitoSiteExceptionDesc');
    this.fire('show-tooltip', {target: tooltip, text});
  }

  private isIsolatedWebApp_(): boolean {
    return this.model.origin.startsWith('isolated-app://');
  }

  /**
   * Returns true if this site exception can be edited by the user. Note that
   * this is not the same as readonly; an exception can be removable but not
   * editable.
   */
  private isUserEditable_(): boolean {
    return !this.readOnlyList && !this.model.embeddingOrigin &&
        !this.isIsolatedWebApp_();
  }

  private shouldShowResetButton_(): boolean {
    if (this.model === undefined) {
      return false;
    }

    return this.model.enforcement !==
        chrome.settingsPrivate.Enforcement.ENFORCED &&
        !this.isUserEditable_();
  }

  private shouldShowActionMenu_(): boolean {
    if (this.model === undefined) {
      return false;
    }

    return this.model.enforcement !==
        chrome.settingsPrivate.Enforcement.ENFORCED &&
        this.isUserEditable_();
  }

  /**
   * A handler for selecting a site (by clicking on the origin).
   */
  private onOriginClick_() {
    if (!this.allowNavigateToSiteDetail_) {
      return;
    }
    Router.getInstance().navigateTo(
        routes.SITE_SETTINGS_SITE_DETAILS,
        new URLSearchParams('site=' + this.model.origin));
  }

  /**
   * Returns the appropriate display name to show for the exception.
   * This can, for example, be the website that is affected itself,
   * or the website whose third parties are also affected.
   */
  private computeDisplayName_(): string {
    if (this.model.embeddingOrigin &&
        ((this.model.category === ContentSettingsTypes.COOKIES &&
          this.model.origin.trim() === SITE_EXCEPTION_WILDCARD) ||
         this.model.category === ContentSettingsTypes.TRACKING_PROTECTION)) {
      return this.model.embeddingOrigin;
    }
    return this.model.displayName;
  }

  /**
   * Returns the appropriate origin that a favicon will be fetched for.
   */
  private computeFaviconOrigin_(): string {
    if (this.model.origin.trim() !== SITE_EXCEPTION_WILDCARD) {
      return this.model.origin.trim();
    }
    if (this.model.embeddingOrigin.trim() !== SITE_EXCEPTION_WILDCARD) {
      return this.model.embeddingOrigin.trim();
    }
    assertNotReached();
  }

  /**
   * Returns the appropriate site description to display. This can, for example,
   * be blank, an 'embedded on <site>' string, or a third-party exception
   * description string.
   */
  private computeSiteDescription_(): string {
    let description = '';

    // If a description has been set by the handler, have it override others.
    // TODO(crbug.com/40276807): Move all possible descriptions in to this
    // field C++ side so this function can be greatly simplified.
    if (this.model.description) {
      description = this.model.description;
    } else if (this.model.isEmbargoed) {
      assert(
          !this.model.embeddingOrigin,
          'Embedding origin should be empty for embargoed origin.');
      description = loadTimeData.getString('siteSettingsSourceEmbargo');
    } else if (this.model.embeddingOrigin) {
      if (this.model.category === ContentSettingsTypes.COOKIES &&
          this.model.origin.trim() === SITE_EXCEPTION_WILDCARD) {
        // Apply special label only if cookies exceptions are displayed in the
        // mixed list.
        if (this.cookiesExceptionType === CookiesExceptionType.COMBINED) {
          description = loadTimeData.getString(
              'siteSettingsCookiesThirdPartyExceptionLabel');
        }
      } else if (
          this.model.category !== ContentSettingsTypes.TRACKING_PROTECTION) {
        description = loadTimeData.getStringF(
            'embeddedOnHost', this.sanitizePort(this.model.embeddingOrigin));
      }
    }

    try {
      const url = new URL(this.model.origin);
      if (url.protocol === 'chrome-extension:') {
        description = loadTimeData.getStringF(
            'siteSettingsExtensionIdDescription', url.hostname);
      }
    } finally {
      return description;
    }
  }

  private computeShowPolicyPrefIndicator_(): boolean {
    return this.model.enforcement ===
        chrome.settingsPrivate.Enforcement.ENFORCED &&
        !!this.model.controlledBy;
  }

  private onResetButtonClick_() {
    this.fire('site-list-entry-reset-click');

    // Use the appropriate method to reset a chooser exception.
    if (this.chooserType !== ChooserType.NONE && this.chooserObject !== null) {
      this.browserProxy.resetChooserExceptionForSite(
          this.chooserType, this.model.origin, this.chooserObject);
      return;
    }

    this.browserProxy.resetCategoryPermissionForPattern(
        this.model.origin, this.model.embeddingOrigin, this.model.category,
        this.model.incognito);
  }

  private onShowActionMenuClick_() {
    // Chooser exceptions do not support the action menu, so do nothing.
    if (this.chooserType !== ChooserType.NONE) {
      return;
    }

    this.fire(
        'show-action-menu',
        {anchor: this.$.actionMenuButton, model: this.model});
  }

  private onModelChanged_() {
    if (!this.model) {
      this.allowNavigateToSiteDetail_ = false;
      return;
    }
    this.browserProxy.isOriginValid(this.model.origin).then((valid) => {
      this.allowNavigateToSiteDetail_ = valid;
    });
  }

  private getActionMenuButtonLabel_() {
    return this.i18n(
        'siteDataPageAddSiteContextMenuLabel', this.computeDisplayName_());
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'site-list-entry': SiteListEntryElement;
  }
}

customElements.define(SiteListEntryElement.is, SiteListEntryElement);