blob: aeba195a96d51ea0f477abe68262ac876ae418fd [file] [log] [blame]
// 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.
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_radio_group/cr_radio_group.js';
import 'chrome://resources/cr_elements/cr_radio_button/cr_radio_button.js';
import 'chrome://resources/cr_elements/icons.html.js';
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/cr_icon/cr_icon.js';
import './runtime_hosts_dialog.js';
import '/strings.m.js';
import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {assert} from 'chrome://resources/js/assert.js';
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {ItemDelegate} from './item.js';
import {DummyItemDelegate} from './item.js';
import {getCss} from './runtime_host_permissions.css.js';
import {getHtml} from './runtime_host_permissions.html.js';
import {getFaviconUrl} from './url_util.js';
function getEventTargetIndex(e: Event): number {
return Number((e.target as HTMLElement).dataset['index']);
}
export interface ExtensionsRuntimeHostPermissionsElement {
$: {
hostActionMenu: CrActionMenuElement,
};
}
export class ExtensionsRuntimeHostPermissionsElement extends CrLitElement {
static get is() {
return 'extensions-runtime-host-permissions';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
/**
* The underlying permissions data.
*/
permissions: {type: Object},
itemId: {type: String},
delegate: {type: Object},
enableEnhancedSiteControls: {type: Boolean},
/**
* Whether the dialog to add a new host permission is shown.
*/
showHostDialog_: {type: Boolean},
/**
* Whether the dialog warning the user that the list of sites added will
* be removed is shown.
*/
showRemoveSiteDialog_: {type: Boolean},
/**
* The current site of the entry that the host dialog is editing, if the
* dialog is open for editing.
*/
hostDialogModel_: {type: String},
/**
* The element to return focus to once the host dialog closes.
*/
hostDialogAnchorElement_: {type: Object},
/**
* If the action menu is open, the site of the entry it is open for.
* Otherwise null.
*/
actionMenuModel_: {type: String},
/**
* The element that triggered the action menu, so that the page will
* return focus once the action menu (or dialog) closes.
*/
actionMenuAnchorElement_: {type: Object},
/**
* The old host access setting; used when we don't immediately commit the
* change to host access so that we can reset it if the user cancels.
*/
oldHostAccess_: {type: String},
/**
* Indicator to track if an onHostAccessChange_ event is coming from the
* setting being automatically reverted to the previous value, after a
* change to a new value was canceled.
*/
revertingHostAccess_: {type: Boolean},
};
}
accessor permissions: chrome.developerPrivate.RuntimeHostPermissions = {
hasAllHosts: true,
hostAccess: chrome.developerPrivate.HostAccess.ON_CLICK,
hosts: [],
};
accessor itemId: string = '';
accessor delegate: ItemDelegate = new DummyItemDelegate();
accessor enableEnhancedSiteControls: boolean = false;
protected accessor showHostDialog_: boolean = false;
protected accessor showRemoveSiteDialog_: boolean = false;
protected accessor hostDialogModel_: string|null = null;
private accessor hostDialogAnchorElement_: HTMLElement|null = null;
private accessor actionMenuModel_: string|null = null;
private accessor actionMenuAnchorElement_: HTMLElement|null = null;
private accessor oldHostAccess_: string|null = null;
private accessor revertingHostAccess_: boolean = false;
getSelectMenu(): HTMLSelectElement {
const selectMenuId =
this.enableEnhancedSiteControls ? '#newHostAccess' : '#hostAccess';
return this.shadowRoot.querySelector<HTMLSelectElement>(selectMenuId)!;
}
getRemoveSiteDialog(): CrDialogElement {
return this.shadowRoot.querySelector<CrDialogElement>('#removeSitesDialog')!
;
}
protected onHostAccessChange_() {
const selectMenu = this.getSelectMenu();
const access = selectMenu.value as chrome.developerPrivate.HostAccess;
// Log a user action when the host access selection is changed by the user,
// but not when reverting from a canceled change to another setting.
if (!this.revertingHostAccess_) {
switch (access) {
case chrome.developerPrivate.HostAccess.ON_CLICK:
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.OnClickSelected');
break;
case chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES:
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.OnSpecificSitesSelected');
break;
case chrome.developerPrivate.HostAccess.ON_ALL_SITES:
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.OnAllSitesSelected');
break;
}
}
const kOnSpecificSites =
chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES;
if (access === kOnSpecificSites &&
this.permissions.hostAccess !== kOnSpecificSites) {
// If the user is transitioning to the "on specific sites" option, show
// the "add host" dialog. This serves two purposes:
// - The user is prompted to add a host immediately, since otherwise
// "on specific sites" is meaningless, and
// - The way the C++ code differentiates between "on click" and "on
// specific sites" is by checking if there are any specific sites.
// This ensures there will be at least one, so that the host access
// is properly calculated.
this.oldHostAccess_ = this.permissions.hostAccess;
this.doShowHostDialog_(selectMenu, null);
} else if (
this.enableEnhancedSiteControls && access !== kOnSpecificSites &&
this.permissions.hostAccess === kOnSpecificSites) {
// If the user is transitioning from the "on specific sites" option to
// another one, show a dialog asking the user to confirm the transition
// because in C++, only the "on specific sites" option will store sites
// the user has added and transitioning away from it will clear these
// sites.
this.showRemoveSiteDialog_ = true;
} else {
this.delegate.setItemHostAccess(this.itemId, access);
}
}
protected showSpecificSites_(): boolean {
return this.permissions.hostAccess ===
chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES;
}
/**
* @return The granted host permissions as a sorted set of strings.
*/
protected getRuntimeHosts_(): string[] {
if (!this.permissions.hosts) {
return [];
}
// Only show granted hosts in the list.
// TODO(devlin): For extensions that request a finite set of hosts,
// display them in a toggle list. https://crbug.com/891803.
return this.permissions.hosts.filter(control => control.granted)
.map(control => control.host)
.sort();
}
protected onAddHostClick_(e: Event) {
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.AddHostActivated');
this.doShowHostDialog_(e.target as HTMLElement, null);
}
protected isHostAccessSelected_(access: chrome.developerPrivate.HostAccess) {
return access === this.permissions.hostAccess;
}
/**
* @param anchorElement The element to return focus to once the dialog closes.
* @param currentSite The site entry currently being edited, or null if this
* is to add a new entry.
*/
private doShowHostDialog_(
anchorElement: HTMLElement, currentSite: string|null) {
this.hostDialogAnchorElement_ = anchorElement;
this.hostDialogModel_ = currentSite;
this.showHostDialog_ = true;
}
protected onHostDialogClose_() {
this.hostDialogModel_ = null;
this.showHostDialog_ = false;
assert(this.hostDialogAnchorElement_);
focusWithoutInk(this.hostDialogAnchorElement_);
this.hostDialogAnchorElement_ = null;
this.oldHostAccess_ = null;
}
protected onHostDialogCancel_() {
// The user canceled the dialog. Set hostAccess back to the old value,
// if the dialog was shown when just transitioning to a new state.
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.AddHostDialogCanceled');
if (this.oldHostAccess_) {
assert(this.permissions.hostAccess === this.oldHostAccess_);
this.revertingHostAccess_ = true;
this.getSelectMenu().value = this.oldHostAccess_;
this.revertingHostAccess_ = false;
this.oldHostAccess_ = null;
}
}
protected dialogShouldUpdateHostAccess_(): boolean {
return !!this.oldHostAccess_;
}
protected onOpenEditHostClick_(e: Event) {
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.ActionMenuOpened');
const index = getEventTargetIndex(e);
this.actionMenuModel_ = this.getRuntimeHosts_()[index] || null;
this.actionMenuAnchorElement_ = e.target as HTMLElement;
this.$.hostActionMenu.showAt(e.target as HTMLElement);
}
protected onActionMenuEditClick_() {
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.ActionMenuEditActivated');
// Cache the site before closing the action menu, since it's cleared.
const site = this.actionMenuModel_;
// Cache and reset actionMenuAnchorElement_ so focus is not returned
// to the action menu's trigger (since the dialog will be shown next).
// Instead, curry the element to the dialog, so once it closes, focus
// will be returned.
assert(this.actionMenuAnchorElement_, 'Menu Anchor');
const anchorElement = this.actionMenuAnchorElement_;
this.actionMenuAnchorElement_ = null;
this.closeActionMenu_();
this.doShowHostDialog_(anchorElement, site);
}
protected onActionMenuRemoveClick_() {
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.ActionMenuRemoveActivated');
assert(this.actionMenuModel_, 'Action Menu Model');
this.delegate.removeRuntimeHostPermission(
this.itemId, this.actionMenuModel_);
this.closeActionMenu_();
}
private closeActionMenu_() {
const menu = this.$.hostActionMenu;
assert(menu.open);
menu.close();
}
protected onLearnMoreClick_() {
chrome.metricsPrivate.recordUserAction(
'Extensions.Settings.Hosts.LearnMoreActivated');
}
protected onEditHostClick_(e: Event) {
const index = getEventTargetIndex(e);
const item = this.getRuntimeHosts_()[index]!;
this.doShowHostDialog_(e.target as HTMLElement, item);
}
protected onDeleteHostClick_(e: Event) {
const index = getEventTargetIndex(e);
const item = this.getRuntimeHosts_()[index]!;
this.delegate.removeRuntimeHostPermission(this.itemId, item);
}
protected getFaviconUrl_(url: string): string {
return getFaviconUrl(url);
}
protected onRemoveSitesWarningConfirm_() {
this.delegate.setItemHostAccess(
this.itemId,
this.getSelectMenu().value as chrome.developerPrivate.HostAccess);
this.getRemoveSiteDialog().close();
this.showRemoveSiteDialog_ = false;
}
protected onRemoveSitesWarningCancel_() {
assert(
this.permissions.hostAccess ===
chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES);
this.revertingHostAccess_ = true;
this.getSelectMenu().value = this.permissions.hostAccess;
this.revertingHostAccess_ = false;
this.getRemoveSiteDialog().close();
this.showRemoveSiteDialog_ = false;
}
}
declare global {
interface HTMLElementTagNameMap {
'extensions-runtime-host-permissions':
ExtensionsRuntimeHostPermissionsElement;
}
}
customElements.define(
ExtensionsRuntimeHostPermissionsElement.is,
ExtensionsRuntimeHostPermissionsElement);