blob: 36a5fec56a3ee41e005a4a4c8b48c092a4b05379 [file] [log] [blame] [edit]
// Copyright 2023 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_expand_button/cr_expand_button.js';
import 'chrome://resources/cr_elements/cr_collapse/cr_collapse.js';
import 'chrome://resources/cr_elements/cr_icon/cr_icon.js';
import 'chrome://resources/cr_elements/icons.html.js';
import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import type {CrExpandButtonElement} from 'chrome://resources/cr_elements/cr_expand_button/cr_expand_button.js';
import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {ItemDelegate} from './item.js';
import {convertSafetyCheckReason, SAFETY_HUB_EXTENSION_KEPT_HISTOGRAM_NAME, SAFETY_HUB_EXTENSION_REMOVED_HISTOGRAM_NAME, SAFETY_HUB_EXTENSION_SHOWN_HISTOGRAM_NAME, SAFETY_HUB_WARNING_REASON_MAX_SIZE} from './item_util.js';
import {navigation, Page} from './navigation_helper.js';
import {getCss} from './review_panel.css.js';
import {getHtml} from './review_panel.html.js';
export interface ExtensionsReviewPanelElement {
$: {
makeExceptionMenu: CrActionMenuElement,
reviewPanelContainer: HTMLElement,
expandButton: CrExpandButtonElement,
safetyHubTitleContainer: HTMLElement,
headingText: HTMLElement,
secondaryText: HTMLElement,
removeAllButton: CrButtonElement,
};
}
const ExtensionsReviewPanelElementBase = I18nMixinLit(CrLitElement);
const SAFETY_HUB_EXTENSION_THREE_DOT_DETAILS =
'SafeBrowsing.ExtensionSafetyHub.ThreeDotDetails';
export class ExtensionsReviewPanelElement extends
ExtensionsReviewPanelElementBase {
static get is() {
return 'extensions-review-panel';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
delegate: {type: Object},
/**
* List of potentially unsafe extensions. If this list is empty, all the
* unsafe extensions were reviewed and the completion info should be
* visible.
*/
extensions: {type: Array},
/**
* The string for the primary header label.
*/
headerString_: {type: String},
/**
* The string for secondary text under the header string.
*/
subtitleString_: {type: String},
/**
* The text of the safety check completion state.
*/
completionMessage_: {type: String},
/**
* Indicates whether to show the potentially unsafe extensions or not.
*/
shouldShowUnsafeExtensions_: {type: Boolean},
/**
* Indicates whether to show completion info after user has finished the
* review process.
*/
shouldShowCompletionInfo_: {type: Boolean},
/**
* Indicates if the list of unsafe extensions is expanded or collapsed.
*/
unsafeExtensionsReviewListExpanded_: {type: Boolean},
};
}
accessor delegate: ItemDelegate|undefined;
accessor extensions: chrome.developerPrivate.ExtensionInfo[] = [];
protected accessor headerString_: string = '';
protected accessor subtitleString_: string = '';
protected accessor unsafeExtensionsReviewListExpanded_: boolean = true;
protected accessor completionMessage_: string = '';
protected accessor shouldShowCompletionInfo_: boolean = false;
protected accessor shouldShowUnsafeExtensions_: boolean = false;
/**
* Tracks if the last action that led to the number of extensions
* under review going to 0 was taken in the review panel. If it was
* the completion state is shown. If not the review panel is removed.
* This prevents actions like toggling dev mode or removing a
* extension using the item card's Remove button from triggering the
* completion message.
*/
private numberOfExtensionsChangedByLastReviewPanelAction_: number = 0;
private completionMetricLogged_: boolean = false;
private lastClickedExtensionId_: string = '';
private lastClickedExtensionTriggerReason_:
chrome.developerPrivate.SafetyCheckWarningReason =
chrome.developerPrivate.SafetyCheckWarningReason.UNPUBLISHED;
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has('extensions')) {
this.shouldShowCompletionInfo_ = this.computeShouldShowCompletionInfo_();
this.shouldShowUnsafeExtensions_ =
this.computeShouldShowUnsafeExtensions_();
this.onExtensionsChanged_();
}
}
private async onExtensionsChanged_() {
this.headerString_ =
await PluralStringProxyImpl.getInstance().getPluralString(
'safetyCheckTitle', this.extensions.length);
this.subtitleString_ =
await PluralStringProxyImpl.getInstance().getPluralString(
'safetyCheckDescription', this.extensions.length);
this.completionMessage_ =
await PluralStringProxyImpl.getInstance().getPluralString(
'safetyCheckAllDoneForNow',
this.numberOfExtensionsChangedByLastReviewPanelAction_);
}
/**
* Determines whether or not to show the completion info when there are no
* unsafe extensions left.
*/
private computeShouldShowCompletionInfo_(): boolean {
if (this.extensions?.length === 0 &&
this.numberOfExtensionsChangedByLastReviewPanelAction_ !== 0) {
if (!this.completionMetricLogged_) {
this.completionMetricLogged_ = true;
chrome.metricsPrivate.recordUserAction('SafetyCheck.ReviewCompletion');
}
return true;
} else {
return false;
}
}
private computeShouldShowUnsafeExtensions_(): boolean {
if (this.extensions?.length !== 0) {
if (!this.shouldShowUnsafeExtensions_) {
chrome.metricsPrivate.recordUserAction('SafetyCheck.ReviewPanelShown');
for (const extension of this.extensions) {
chrome.metricsPrivate.recordEnumerationValue(
SAFETY_HUB_EXTENSION_SHOWN_HISTOGRAM_NAME,
convertSafetyCheckReason(extension.safetyCheckWarningReason),
SAFETY_HUB_WARNING_REASON_MAX_SIZE);
}
}
this.completionMetricLogged_ = false;
// Reset the `numberOfExtensionsChangedByLastReviewPanelAction_` if
// the last action completed the review, i.e., a completion message
// will be shown. Resetting ensures that the completion message is
// only shown once after a review panel action.
if (this.shouldShowCompletionInfo_) {
this.numberOfExtensionsChangedByLastReviewPanelAction_ = 0;
}
return true;
}
return false;
}
protected shouldShowExtensionsSafetyHub_(): boolean {
return this.shouldShowUnsafeExtensions_ || this.shouldShowCompletionInfo_;
}
protected shouldShowSafetyHubRemoveAllButton_(): boolean {
return this.extensions?.length !== 1;
}
protected shouldShowThreeDotDetails_(): boolean {
return loadTimeData.getBoolean('safetyHubThreeDotDetails');
}
protected onDetailsClick_() {
chrome.metricsPrivate.recordCount(
SAFETY_HUB_EXTENSION_THREE_DOT_DETAILS, 1);
navigation.navigateTo(
{page: Page.DETAILS, extensionId: this.lastClickedExtensionId_});
this.$.makeExceptionMenu.close();
}
protected onUnsafeExtensionsReviewListExpandedChanged_(
e: CustomEvent<{value: boolean}>) {
this.unsafeExtensionsReviewListExpanded_ = e.detail.value;
}
/**
* Opens the extension action menu.
*/
protected onMakeExceptionMenuClick_(e: Event) {
const index = Number((e.target as HTMLElement).dataset['index']);
const item = this.extensions[index]!;
this.lastClickedExtensionId_ = item.id;
this.lastClickedExtensionTriggerReason_ = item.safetyCheckWarningReason;
this.$.makeExceptionMenu.showAt(e.target as HTMLElement);
}
/**
* Acknowledges the extension safety check warning.
*/
protected onKeepExtensionClick_() {
chrome.metricsPrivate.recordUserAction(
'SafetyCheck.ReviewPanelKeepClicked');
chrome.metricsPrivate.recordEnumerationValue(
SAFETY_HUB_EXTENSION_KEPT_HISTOGRAM_NAME,
convertSafetyCheckReason(this.lastClickedExtensionTriggerReason_),
SAFETY_HUB_WARNING_REASON_MAX_SIZE);
if (this.extensions?.length === 1) {
this.numberOfExtensionsChangedByLastReviewPanelAction_ = 1;
}
this.$.makeExceptionMenu.close();
if (this.lastClickedExtensionId_) {
assert(this.delegate);
this.delegate.setItemSafetyCheckWarningAcknowledged(
this.lastClickedExtensionId_,
this.lastClickedExtensionTriggerReason_);
}
}
protected getRemoveButtonA11yLabel_(extensionName: string): string {
return loadTimeData.substituteString(
this.i18n('safetyCheckRemoveButtonA11yLabel'), extensionName);
}
protected getOptionMenuA11yLabel_(extensionName: string) {
return loadTimeData.substituteString(
this.i18n('safetyCheckOptionMenuA11yLabel'), extensionName);
}
protected async onRemoveExtensionClick_(e: Event): Promise<void> {
const index = Number((e.target as HTMLElement).dataset['index']);
const item = this.extensions[index]!;
chrome.metricsPrivate.recordUserAction(
'SafetyCheck.ReviewPanelRemoveClicked');
chrome.metricsPrivate.recordEnumerationValue(
SAFETY_HUB_EXTENSION_REMOVED_HISTOGRAM_NAME,
convertSafetyCheckReason(item.safetyCheckWarningReason),
SAFETY_HUB_WARNING_REASON_MAX_SIZE);
if (this.extensions?.length === 1) {
this.numberOfExtensionsChangedByLastReviewPanelAction_ = 1;
}
try {
assert(this.delegate);
await this.delegate.uninstallItem(item.id);
} catch (_) {
// The error was almost certainly the user canceling the dialog.
// Update the number of changed extensions.
this.numberOfExtensionsChangedByLastReviewPanelAction_ = 0;
}
}
protected async onRemoveAllClick_(event: Event): Promise<void> {
chrome.metricsPrivate.recordUserAction(
'SafetyCheck.ReviewPanelRemoveAllClicked');
event.stopPropagation();
this.numberOfExtensionsChangedByLastReviewPanelAction_ =
this.extensions.length;
try {
this.extensions.forEach(extension => {
chrome.metricsPrivate.recordEnumerationValue(
SAFETY_HUB_EXTENSION_REMOVED_HISTOGRAM_NAME,
convertSafetyCheckReason(extension.safetyCheckWarningReason),
SAFETY_HUB_WARNING_REASON_MAX_SIZE);
});
assert(this.delegate);
await this.delegate.deleteItems(
this.extensions.map(extension => extension.id));
} catch (_) {
// The error was almost certainly the user canceling the dialog.
// Reset `numberOfExtensionsChangedByLastReviewPanelAction_`.
this.numberOfExtensionsChangedByLastReviewPanelAction_ = 0;
}
}
}
declare global {
interface HTMLElementTagNameMap {
'extensions-review-panel': ExtensionsReviewPanelElement;
}
}
customElements.define(
ExtensionsReviewPanelElement.is, ExtensionsReviewPanelElement);