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

// Copyright 2015 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-details' show the details (permissions and usage) for a given origin
 * under Site Settings.
 */
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link.css.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js';
import 'chrome://resources/cr_elements/icons.html.js';
import 'chrome://resources/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import '../icons.html.js';
import '../privacy_icons.html.js';
import '../settings_shared.css.js';
import './all_sites_icons.html.js';
import './clear_storage_dialog_shared.css.js';
import './site_details_permission.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 {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {loadTimeData} from '../i18n_setup.js';
import {MetricsBrowserProxyImpl, PrivacyElementInteractions} from '../metrics_browser_proxy.js';
import {routes} from '../route.js';
import type {Route} from '../router.js';
import {RouteObserverMixin, Router} from '../router.js';

import {ChooserType, ContentSetting, ContentSettingsTypes} from './constants.js';
import {getTemplate} from './site_details.html.js';
import type {SiteDetailsPermissionElement} from './site_details_permission.js';
import {SiteSettingsMixin} from './site_settings_mixin.js';
import type {WebsiteUsageBrowserProxy} from './website_usage_browser_proxy.js';
import {WebsiteUsageBrowserProxyImpl} from './website_usage_browser_proxy.js';

export interface SiteDetailsElement {
  $: {
    confirmClearStorage: CrDialogElement,
    confirmResetSettings: CrDialogElement,
    rwsMembership: HTMLElement,
    noStorage: HTMLElement,
    storage: HTMLElement,
    usage: HTMLElement,
  };
}

const SiteDetailsElementBase = RouteObserverMixin(
    SiteSettingsMixin(WebUiListenerMixin(I18nMixin(PolymerElement))));

export class SiteDetailsElement extends SiteDetailsElementBase {
  static get is() {
    return 'site-details';
  }

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

  static get properties() {
    return {
      /**
       * Whether unified autoplay blocking is enabled.
       */
      blockAutoplayEnabled: Boolean,

      /**
       * Use the string representing the origin or extension name as the page
       * title of the settings-subpage parent.
       */
      pageTitle: {
        type: String,
        notify: true,
      },

      /**
       * The origin that this widget is showing details for.
       */
      origin_: String,

      /**
       * The amount of data stored for the origin.
       */
      storedData_: {
        type: String,
        value: '',
      },

      /**
       * The number of cookies stored for the origin.
       */
      numCookies_: {
        type: String,
        value: '',
      },

      /**
       * The related website set info for a site including owner and members
       * count.
       */
      rwsMembership_: {
        type: String,
        value: '',
      },

      /**
       * Mock preference used to power managed policy icon for related website
       * sets.
       */
      rwsEnterprisePref_: Object,

      enableExperimentalWebPlatformFeatures_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean(
              'enableExperimentalWebPlatformFeatures');
        },
      },

      enableWebBluetoothNewPermissionsBackend_: {
        type: Boolean,
        value: () =>
            loadTimeData.getBoolean('enableWebBluetoothNewPermissionsBackend'),
      },

      autoPictureInPictureEnabled_: {
        type: Boolean,
        value: () => loadTimeData.getBoolean('autoPictureInPictureEnabled'),
      },

      enableAutomaticFullscreenContentSetting_: {
        type: Boolean,
        value: () =>
            loadTimeData.getBoolean('enableAutomaticFullscreenContentSetting'),
      },

      enableHandTrackingContentSetting_: {
        type: Boolean,
        value: () =>
              loadTimeData.getBoolean('enableHandTrackingContentSetting'),
      },

      capturedSurfaceControlEnabled_: {
        type: Boolean,
        value: () => loadTimeData.getBoolean('capturedSurfaceControlEnabled'),
      },

      contentSettingsTypesEnum_: {
        type: Object,
        value: ContentSettingsTypes,
      },

      chooserTypeEnum_: {
        type: Object,
        value: ChooserType,
      },

      enableKeyboardAndPointerLockPrompt_: {
        type: Boolean,
        value: () =>
            loadTimeData.getBoolean('enableKeyboardAndPointerLockPrompt'),
      },
    };
  }

  blockAutoplayEnabled: boolean;
  pageTitle: string;
  private origin_: string;
  private storedData_: string;
  private numCookies_: string;
  private rwsMembership_: string;
  private rwsEnterprisePref_: chrome.settingsPrivate.PrefObject;
  private enableExperimentalWebPlatformFeatures_: boolean;
  private enableWebBluetoothNewPermissionsBackend_: boolean;
  private autoPictureInPictureEnabled_: boolean;
  private enableAutomaticFullscreenContentSetting_: boolean;
  private enableHandTrackingContentSetting_: boolean;
  private capturedSurfaceControlEnabled_: boolean;
  private websiteUsageProxy_: WebsiteUsageBrowserProxy =
      WebsiteUsageBrowserProxyImpl.getInstance();
  private enableKeyboardAndPointerLockPrompt_: boolean;

  override connectedCallback() {
    super.connectedCallback();

    this.addWebUiListener(
        'usage-total-changed',
        (host: string, data: string, cookies: string, rws: string,
         rwsPolicy: boolean) => {
          this.onUsageTotalChanged_(host, data, cookies, rws, rwsPolicy);
        });

    this.addWebUiListener(
        'contentSettingSitePermissionChanged',
        (category: ContentSettingsTypes, origin: string) =>
            this.onPermissionChanged_(category, origin));

    // Refresh block autoplay status from the backend.
    this.browserProxy.fetchBlockAutoplayStatus();
  }

  /**
   * RouteObserverMixin
   */
  override currentRouteChanged(route: Route) {
    if (route !== routes.SITE_SETTINGS_SITE_DETAILS) {
      return;
    }

    const site = Router.getInstance().getQueryParameters().get('site') ?? '';
    this.origin_ = site;
    this.browserProxy.isOriginValid(this.origin_).then((valid) => {
      if (!valid) {
        Router.getInstance().navigateToPreviousRoute();
      } else {
        this.storedData_ = '';
        this.websiteUsageProxy_.fetchUsageTotal(this.origin_);
        this.browserProxy.getCategoryList(this.origin_).then((categoryList) => {
          this.updatePermissions_(categoryList, /*hideOthers=*/ true);
        });
      }
    });
  }

  /**
   * Called when a site within a category has been changed.
   * @param category The category that changed.
   * @param origin The origin of the site that changed.
   */
  private onPermissionChanged_(category: ContentSettingsTypes, origin: string) {
    if (this.origin_ === undefined || this.origin_ === '' ||
        origin === undefined || origin === '') {
      return;
    }

    this.browserProxy.getCategoryList(this.origin_).then((categoryList) => {
      if (categoryList.includes(category)) {
        this.updatePermissions_([category], /*hideOthers=*/ false);
      }
    });
  }

  /**
   * Callback for when the usage total is known.
   * @param origin The origin that the usage was fetched for.
   * @param usage The string showing how much data the given host is using.
   * @param cookies The string showing how many cookies the given host is using.
   * @param rwsMembership The string showing related website set membership
   *     details.
   * @param rwsPolicy Whether a policy is applied to this RWS member.
   */
  private onUsageTotalChanged_(
      origin: string, usage: string, cookies: string, rwsMembership: string,
      rwsPolicy: boolean) {
    if (this.origin_ === origin) {
      this.storedData_ = usage;
      this.numCookies_ = cookies;
      this.rwsMembership_ = rwsMembership;
      this.rwsEnterprisePref_ = rwsPolicy ? Object.assign({
        enforcement: chrome.settingsPrivate.Enforcement.ENFORCED,
        controlledBy: chrome.settingsPrivate.ControlledBy.DEVICE_POLICY,
      }) :
                                            undefined;
    }
  }

  /**
   * Retrieves the permissions listed in |categoryList| from the backend for
   * |this.origin_|.
   * @param categoryList The list of categories to update permissions for.
   * @param hideOthers If true, permissions for categories not in
   *     |categoryList| will be hidden.
   */
  private updatePermissions_(
      categoryList: ContentSettingsTypes[], hideOthers: boolean) {
    const permissionsMap: {[key: string]: SiteDetailsPermissionElement} =
        Array.prototype.reduce.call(
            this.shadowRoot!.querySelectorAll('site-details-permission'),
            (map, element) => {
              if (categoryList.includes(element.category)) {
                (map as {
                  [key: string]: SiteDetailsPermissionElement,
                })[element.category] = element;
              } else if (hideOthers) {
                // This will hide any permission not in the category list.
                element.site = null;
              }
              return map;
            },
            {}) as {[key: string]: SiteDetailsPermissionElement};

    this.browserProxy.getOriginPermissions(this.origin_, categoryList)
        .then((exceptionList) => {
          exceptionList.forEach((exception, i) => {
            // |exceptionList| should be in the same order as
            // |categoryList|.
            if (permissionsMap[categoryList[i]]) {
              permissionsMap[categoryList[i]].site = exception;
            }
          });

          // The displayName won't change, so just use the first
          // exception.
          assert(exceptionList.length > 0);
          this.pageTitle = exceptionList[0].displayName;
        });
  }

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

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

  /**
   * Confirms the clearing of storage for an origin.
   */
  private onConfirmClearStorage_(e: Event) {
    e.preventDefault();
    this.$.confirmClearStorage.showModal();
  }

  /**
   * Resets all permissions for the current origin.
   */
  private onResetSettings_(e: Event) {
    this.browserProxy.setOriginPermissions(
        this.origin_, null, ContentSetting.DEFAULT);

    this.onCloseDialog_(e);
  }

  /**
   * Clears all data stored, except cookies, for the current origin.
   */
  private onClearStorage_(e: Event) {
    MetricsBrowserProxyImpl.getInstance().recordSettingsPageHistogram(
        PrivacyElementInteractions.SITE_DETAILS_CLEAR_DATA);
    if (this.hasUsage_(this.storedData_, this.numCookies_)) {
      this.websiteUsageProxy_.clearUsage(this.toUrl(this.origin_)!.href);
      this.storedData_ = '';
      this.numCookies_ = '';
    }

    this.onCloseDialog_(e);
    const toFocus =
        this.shadowRoot!.querySelector<HTMLElement>('#resetSettingsButton');
    assert(toFocus);
    focusWithoutInk(toFocus);
  }

  /**
   * Checks whether this site has any usage information to show.
   * @return Whether there is any usage information to show (e.g. disk or
   *     battery).
   */
  private hasUsage_(storage: string, cookies: string): boolean {
    return storage !== '' || cookies !== '';
  }

  /**
   * Checks whether this site has both storage and cookies information to show.
   * @return Whether there are both storage and cookies information to show.
   */
  private hasDataAndCookies_(storage: string, cookies: string): boolean {
    return storage !== '' && cookies !== '';
  }

  private onResetSettingsDialogClosed_() {
    const toFocus =
        this.shadowRoot!.querySelector<HTMLElement>('#resetSettingsButton');
    assert(toFocus);
    focusWithoutInk(toFocus);
  }

  private onClearStorageDialogClosed_() {
    const toFocus =
        this.shadowRoot!.querySelector<HTMLElement>('#clearStorage');
    assert(toFocus);
    focusWithoutInk(toFocus);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'site-details': SiteDetailsElement;
  }
}

customElements.define(SiteDetailsElement.is, SiteDetailsElement);