| // 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. |
| |
| 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_icons.css.js'; |
| import 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js'; |
| import 'chrome://resources/cr_elements/cr_tooltip/cr_tooltip.js'; |
| import 'chrome://resources/cr_elements/cr_hidden_style.css.js'; |
| import 'chrome://resources/cr_elements/icons_lit.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/js/action_link.js'; |
| import 'chrome://resources/cr_elements/action_link.css.js'; |
| import './icons.html.js'; |
| import './shared_style.css.js'; |
| import './shared_vars.css.js'; |
| import './strings.m.js'; |
| import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js'; |
| import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; |
| |
| import type {ChromeEvent} from '/tools/typescript/definitions/chrome_event.js'; |
| import type {CrToggleElement} from 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js'; |
| import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js'; |
| import {assert, assertNotReached} from 'chrome://resources/js/assert.js'; |
| import {flush, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| import {getTemplate} from './item.html.js'; |
| import {ItemMixin} from './item_mixin.js'; |
| import {computeInspectableViewLabel, EnableControl, getEnableControl, getEnableToggleAriaLabel, getEnableToggleTooltipText, getItemSource, getItemSourceString, isEnabled, sortViews, SourceType, userCanChangeEnablement} from './item_util.js'; |
| import {navigation, Page} from './navigation_helper.js'; |
| |
| export interface ItemDelegate { |
| deleteItem(id: string): void; |
| deleteItems(ids: string[]): Promise<void>; |
| uninstallItem(id: string): Promise<void>; |
| setItemEnabled(id: string, isEnabled: boolean): void; |
| setItemAllowedIncognito(id: string, isAllowedIncognito: boolean): void; |
| setItemAllowedOnFileUrls(id: string, isAllowedOnFileUrls: boolean): void; |
| setItemHostAccess(id: string, hostAccess: chrome.developerPrivate.HostAccess): |
| void; |
| setItemCollectsErrors(id: string, collectsErrors: boolean): void; |
| inspectItemView(id: string, view: chrome.developerPrivate.ExtensionView): |
| void; |
| openUrl(url: string): void; |
| reloadItem(id: string): Promise<void>; |
| repairItem(id: string): void; |
| showItemOptionsPage(extension: chrome.developerPrivate.ExtensionInfo): void; |
| showInFolder(id: string): void; |
| getExtensionSize(id: string): Promise<string>; |
| addRuntimeHostPermission(id: string, host: string): Promise<void>; |
| removeRuntimeHostPermission(id: string, host: string): Promise<void>; |
| setItemSafetyCheckWarningAcknowledged( |
| id: string, |
| reason: chrome.developerPrivate.SafetyCheckWarningReason): void; |
| setShowAccessRequestsInToolbar(id: string, showRequests: boolean): void; |
| setItemPinnedToToolbar(id: string, pinnedToToolbar: boolean): void; |
| |
| // TODO(tjudkins): This function is not specific to items, so should be pulled |
| // out to a more generic place when we need to access it from elsewhere. |
| recordUserAction(metricName: string): void; |
| getItemStateChangedTarget(): |
| ChromeEvent<(data: chrome.developerPrivate.EventData) => void>; |
| } |
| |
| export interface ExtensionsItemElement { |
| $: { |
| a11yAssociation: HTMLElement, |
| detailsButton: HTMLElement, |
| enableToggle: CrToggleElement, |
| name: HTMLElement, |
| removeButton: HTMLElement, |
| }; |
| } |
| |
| const ExtensionsItemElementBase = I18nMixin(ItemMixin(PolymerElement)); |
| |
| export class ExtensionsItemElement extends ExtensionsItemElementBase { |
| static get is() { |
| return 'extensions-item'; |
| } |
| |
| static get template() { |
| return getTemplate(); |
| } |
| |
| static get properties() { |
| return { |
| // The item's delegate, or null. |
| delegate: Object, |
| |
| // Whether or not dev mode is enabled. |
| inDevMode: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| safetyCheckShowing: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| // The underlying ExtensionInfo itself. Public for use in declarative |
| // bindings. |
| data: Object, |
| |
| // Whether or not the expanded view of the item is shown. |
| showingDetails_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| // First inspectable view after sorting. |
| firstInspectView_: { |
| type: Object, |
| computed: 'computeFirstInspectView_(data.views)', |
| }, |
| }; |
| } |
| |
| static get observers() { |
| return ['observeIdVisibility_(inDevMode, showingDetails_, data.id)']; |
| } |
| |
| delegate: ItemDelegate; |
| inDevMode: boolean; |
| safetyCheckShowing: boolean; |
| data: chrome.developerPrivate.ExtensionInfo; |
| private showingDetails_: boolean; |
| private firstInspectView_: chrome.developerPrivate.ExtensionView; |
| |
| private fire_(eventName: string, detail?: any) { |
| this.dispatchEvent( |
| new CustomEvent(eventName, {bubbles: true, composed: true, detail})); |
| } |
| |
| /** @return The "Details" button. */ |
| getDetailsButton(): HTMLElement { |
| return this.$.detailsButton; |
| } |
| |
| /** @return The "Remove" button, if it exists. */ |
| getRemoveButton(): HTMLElement|null { |
| return this.data.mustRemainInstalled ? null : this.$.removeButton; |
| } |
| |
| /** @return The "Errors" button, if it exists. */ |
| getErrorsButton(): HTMLElement|null { |
| return this.shadowRoot!.querySelector('#errors-button'); |
| } |
| |
| private getEnableToggleAriaLabel_(): string { |
| return getEnableToggleAriaLabel( |
| this.isEnabled_(), this.data.type, this.i18n('appEnabled'), |
| this.i18n('extensionEnabled'), this.i18n('itemOff')); |
| } |
| |
| private getEnableToggleTooltipText_(): string { |
| return getEnableToggleTooltipText(this.data); |
| } |
| |
| private observeIdVisibility_() { |
| flush(); |
| const idElement = this.shadowRoot!.querySelector('#extension-id'); |
| if (idElement) { |
| assert(this.data); |
| idElement.textContent = this.i18n('itemId', this.data.id); |
| } |
| } |
| |
| private shouldShowErrorsButton_(): boolean { |
| // When the error console is disabled (happens when |
| // --disable-error-console command line flag is used or when in the |
| // Stable/Beta channel), |installWarnings| is populated. |
| if (this.data.installWarnings && this.data.installWarnings.length > 0) { |
| return true; |
| } |
| |
| // When error console is enabled |installedWarnings| is not populated. |
| // Instead |manifestErrors| and |runtimeErrors| are used. |
| return this.data.manifestErrors.length > 0 || |
| this.data.runtimeErrors.length > 0; |
| } |
| |
| private onRemoveClick_() { |
| if (this.safetyCheckShowing) { |
| const actionToRecord = this.data.safetyCheckText ? |
| 'SafetyCheck.ReviewPanelRemoveClicked' : |
| 'SafetyCheck.NonTriggeringExtensionRemoved'; |
| chrome.metricsPrivate.recordUserAction(actionToRecord); |
| } |
| this.delegate.deleteItem(this.data.id); |
| } |
| |
| private onEnableToggleChange_() { |
| this.delegate.setItemEnabled(this.data.id, this.$.enableToggle.checked); |
| this.$.enableToggle.checked = this.isEnabled_(); |
| } |
| |
| private onErrorsClick_() { |
| if (this.data.installWarnings && this.data.installWarnings.length > 0) { |
| this.fire_('show-install-warnings', this.data.installWarnings); |
| return; |
| } |
| |
| navigation.navigateTo({page: Page.ERRORS, extensionId: this.data.id}); |
| } |
| |
| private onDetailsClick_() { |
| navigation.navigateTo({page: Page.DETAILS, extensionId: this.data.id}); |
| } |
| |
| private computeFirstInspectView_(): chrome.developerPrivate.ExtensionView { |
| return sortViews(this.data.views)[0]; |
| } |
| |
| private onInspectClick_() { |
| this.delegate.inspectItemView(this.data.id, this.firstInspectView_); |
| } |
| |
| private onExtraInspectClick_() { |
| navigation.navigateTo({page: Page.DETAILS, extensionId: this.data.id}); |
| } |
| |
| private onReloadClick_() { |
| this.reloadItem().catch((loadError) => this.fire_('load-error', loadError)); |
| } |
| |
| private onRepairClick_() { |
| this.delegate.repairItem(this.data.id); |
| } |
| |
| private isEnabled_(): boolean { |
| return isEnabled(this.data.state); |
| } |
| |
| private isEnableToggleEnabled_(): boolean { |
| return userCanChangeEnablement(this.data); |
| } |
| |
| /** @return Whether the reload button should be shown. */ |
| private showReloadButton_(): boolean { |
| return getEnableControl(this.data) === EnableControl.RELOAD; |
| } |
| |
| /** @return Whether the repair button should be shown. */ |
| private showRepairButton_(): boolean { |
| return getEnableControl(this.data) === EnableControl.REPAIR; |
| } |
| |
| |
| /** @return Whether the enable toggle should be shown. */ |
| private showEnableToggle_(): boolean { |
| return getEnableControl(this.data) === EnableControl.ENABLE_TOGGLE; |
| } |
| |
| private computeClasses_(): string { |
| let classes = this.isEnabled_() ? 'enabled' : 'disabled'; |
| if (this.inDevMode) { |
| classes += ' dev-mode'; |
| } |
| return classes; |
| } |
| |
| private computeSourceIndicatorIcon_(): string { |
| switch (getItemSource(this.data)) { |
| case SourceType.POLICY: |
| return 'extensions-icons:business'; |
| case SourceType.SIDELOADED: |
| return 'extensions-icons:input'; |
| case SourceType.UNKNOWN: |
| // TODO(dpapad): Ask UX for a better icon for this case. |
| return 'extensions-icons:input'; |
| case SourceType.UNPACKED: |
| return 'extensions-icons:unpacked'; |
| case SourceType.WEBSTORE: |
| case SourceType.INSTALLED_BY_DEFAULT: |
| return ''; |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| private computeSourceIndicatorText_(): string { |
| if (this.data.locationText) { |
| return this.data.locationText; |
| } |
| |
| const sourceType = getItemSource(this.data); |
| return sourceType === SourceType.WEBSTORE ? '' : |
| getItemSourceString(sourceType); |
| } |
| |
| private computeInspectViewsHidden_(): boolean { |
| return !this.data.views || this.data.views.length === 0; |
| } |
| |
| private computeFirstInspectTitle_(): string { |
| // Note: theoretically, this wouldn't be called without any inspectable |
| // views (because it's in a dom-if="!computeInspectViewsHidden_()"). |
| // However, due to the recycling behavior of iron list, it seems that |
| // sometimes it can. Even when it is, the UI behaves properly, but we |
| // need to handle the case gracefully. |
| return this.data.views.length > 0 ? |
| computeInspectableViewLabel(this.firstInspectView_) : |
| ''; |
| } |
| |
| private computeFirstInspectLabel_(): string { |
| const label = this.computeFirstInspectTitle_(); |
| return label && this.data.views.length > 1 ? label + ',' : label; |
| } |
| |
| private computeExtraViewsHidden_(): boolean { |
| return this.data.views.length <= 1; |
| } |
| |
| private computeDevReloadButtonHidden_(): boolean { |
| return !this.canReloadItem(); |
| } |
| |
| private computeExtraInspectLabel_(): string { |
| return this.i18n( |
| 'itemInspectViewsExtra', (this.data.views.length - 1).toString()); |
| } |
| |
| /** |
| * @return Whether the extension has severe warnings. Doesn't determine the |
| * warning's visibility. |
| */ |
| private hasSevereWarnings_(): boolean { |
| return this.data.disableReasons.corruptInstall || |
| this.data.disableReasons.suspiciousInstall || |
| this.data.runtimeWarnings.length > 0 || !!this.data.blocklistText; |
| } |
| |
| /** |
| * @return Whether the extension has an MV2 warning. Doesn't determine the |
| * warning's visibility. |
| */ |
| private hasMv2DeprecationWarning_(): boolean { |
| return this.data.disableReasons.unsupportedManifestVersion; |
| } |
| |
| /** |
| * @return Whether the extension has an allowlist warning. Doesn't determine |
| * the warning's visibility. |
| */ |
| private hasAllowlistWarning_(): boolean { |
| return this.data.showSafeBrowsingAllowlistWarning; |
| } |
| |
| private showDescription_(): boolean { |
| // Description is only visible iff no warnings are visible. |
| return !this.hasSevereWarnings_() && !this.hasMv2DeprecationWarning_() && |
| !this.hasAllowlistWarning_(); |
| } |
| |
| private showSevereWarnings(): boolean { |
| // Severe warning are always visible, if they exist. |
| return this.hasSevereWarnings_(); |
| } |
| |
| private showMv2DeprecationWarning_(): boolean { |
| // MV2 deprecation warning is visible, if existent, if there are no severe |
| // warnings visible. |
| // Note: The item card has a fixed height and the content might get cropped |
| // if too many warnings are displayed. |
| return this.hasMv2DeprecationWarning_() && !this.hasSevereWarnings_(); |
| } |
| |
| private showAllowlistWarning_(): boolean { |
| // Allowlist warning is visible, if existent, if there are no severe |
| // warnings or mv2 deprecation warnings visible. |
| // Note: The item card has a fixed height and the content might get cropped |
| // if too many warnings are displayed. This should be a rare edge case and |
| // the allowlist warning will still be shown in the item detail view. |
| return this.hasAllowlistWarning_() && !this.hasSevereWarnings_() && |
| !this.hasMv2DeprecationWarning_(); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'extensions-item': ExtensionsItemElement; |
| } |
| } |
| |
| customElements.define(ExtensionsItemElement.is, ExtensionsItemElement); |