| // 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_components/managed_footnote/managed_footnote.js'; |
| import './item.js'; |
| import './mv2_deprecation_panel.js'; |
| import './shared_style.css.js'; |
| import './review_panel.js'; |
| |
| import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js'; |
| import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| import type {ExtensionsItemElement, ItemDelegate} from './item.js'; |
| import {getTemplate} from './item_list.html.js'; |
| import {getMv2ExperimentStage, Mv2ExperimentStage} from './mv2_deprecation_util.js'; |
| |
| type Filter = (info: chrome.developerPrivate.ExtensionInfo) => boolean; |
| |
| const ExtensionsItemListElementBase = I18nMixin(PolymerElement); |
| |
| export class ExtensionsItemListElement extends ExtensionsItemListElementBase { |
| static get is() { |
| return 'extensions-item-list'; |
| } |
| |
| static get template() { |
| return getTemplate(); |
| } |
| |
| static get properties() { |
| return { |
| apps: Array, |
| extensions: Array, |
| delegate: Object, |
| |
| inDevMode: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| isMv2DeprecationNoticeDismissed: { |
| type: Boolean, |
| notify: true, |
| }, |
| |
| filter: { |
| type: String, |
| }, |
| |
| computedFilter_: { |
| type: String, |
| computed: 'computeFilter_(filter)', |
| observer: 'announceSearchResults_', |
| }, |
| |
| maxColumns_: { |
| type: Number, |
| value: 3, |
| }, |
| |
| /** |
| * List of potentially unsafe extensions that should be visible in the |
| * review panel. |
| */ |
| unsafeExtensions_: { |
| type: Array, |
| computed: 'computeUnsafeExtensions_(extensions.*)', |
| }, |
| |
| /** |
| * Current Manifest V2 experiment stage. |
| */ |
| mv2ExperimentStage_: { |
| type: Number, |
| value: () => getMv2ExperimentStage( |
| loadTimeData.getInteger('MV2ExperimentStage')), |
| }, |
| |
| /** |
| * List of extensions that are affected by the mv2 deprecation and should |
| * be visible in the mv2 deprecation panel. |
| */ |
| mv2DeprecatedExtensions_: { |
| type: Array, |
| computed: 'computeMv2DeprecatedExtensions_(extensions.*)', |
| }, |
| |
| shownAppsCount_: { |
| type: Number, |
| value: 0, |
| }, |
| |
| shownExtensionsCount_: { |
| type: Number, |
| value: 0, |
| }, |
| |
| /** |
| * Indicates whether the review panel is shown. |
| */ |
| showSafetyCheckReviewPanel_: { |
| type: Boolean, |
| computed: 'computeShowSafetyCheckReviewPanel_(unsafeExtensions_)', |
| }, |
| |
| /** |
| * Indicates if the review panel has ever been shown. |
| */ |
| reviewPanelShown_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| /* |
| * Indicates whether the mv2 deprecation panel is shown. |
| */ |
| showMv2DeprecationPanel_: { |
| type: Boolean, |
| computed: 'computeShowMv2DeprecationPanel_(' + |
| 'mv2ExperimentStage_, mv2DeprecatedExtensions_, ' + |
| 'isMv2DeprecationNoticeDismissed)', |
| }, |
| |
| hasSafetyCheckTriggeringExtension_: { |
| type: Boolean, |
| computed: 'computeHasSafetyCheckTriggeringExtension_(extensions)', |
| }, |
| }; |
| } |
| |
| apps: chrome.developerPrivate.ExtensionInfo[]; |
| extensions: chrome.developerPrivate.ExtensionInfo[]; |
| delegate: ItemDelegate; |
| inDevMode: boolean; |
| isMv2DeprecationNoticeDismissed: boolean; |
| filter: string; |
| private computedFilter_: string; |
| private maxColumns_: number; |
| private unsafeExtensions_: chrome.developerPrivate.ExtensionInfo[]; |
| private mv2ExperimentStage_: Mv2ExperimentStage; |
| private mv2DeprecatedExtensions_: chrome.developerPrivate.ExtensionInfo[]; |
| private shownAppsCount_: number; |
| private shownExtensionsCount_: number; |
| private showMv2DeprecationPanel_: boolean; |
| private showSafetyCheckReviewPanel_: boolean; |
| private reviewPanelShown_: boolean; |
| private hasSafetyCheckTriggeringExtension_: boolean; |
| |
| getDetailsButton(id: string): HTMLElement|null { |
| const item = |
| this.shadowRoot!.querySelector<ExtensionsItemElement>(`#${id}`); |
| return item && item.getDetailsButton(); |
| } |
| |
| getRemoveButton(id: string): HTMLElement|null { |
| const item = |
| this.shadowRoot!.querySelector<ExtensionsItemElement>(`#${id}`); |
| return item && item.getRemoveButton(); |
| } |
| |
| getErrorsButton(id: string): HTMLElement|null { |
| const item = |
| this.shadowRoot!.querySelector<ExtensionsItemElement>(`#${id}`); |
| return item && item.getErrorsButton(); |
| } |
| |
| /** |
| * Focus the remove button for the item matching `id`. If the remove button is |
| * not visible, focus the details button instead. |
| * return: If an item's button has been focused, see comment below. |
| */ |
| focusItemButton(id: string): boolean { |
| const item = |
| this.shadowRoot!.querySelector<ExtensionsItemElement>(`#${id}`); |
| // This function is called from a setTimeout() inside manager.ts. Rarely, |
| // the list of extensions rendered in this element may not match the list of |
| // extensions stored in manager.ts for a brief moment (not visible to the |
| // user). As a result, `item` here may be null even though `id` points to |
| // an extension inside `manager.ts`. If this happens, do not focus anything. |
| // Observed in crbug.com/1482580. |
| if (!item) { |
| return false; |
| } |
| |
| const buttonToFocus = item.getRemoveButton() || item.getDetailsButton(); |
| buttonToFocus!.focus(); |
| return true; |
| } |
| |
| /** |
| * Computes the filter function to be used for determining which items |
| * should be shown. A |null| value indicates that everything should be |
| * shown. |
| * return {?Function} |
| */ |
| private computeFilter_(): Filter|null { |
| const formattedFilter = this.filter.trim().toLowerCase(); |
| if (!formattedFilter) { |
| return null; |
| } |
| |
| return i => [i.name, i.id].some( |
| s => s.toLowerCase().includes(formattedFilter)); |
| } |
| |
| /** |
| * Computes the extensions that are affected by the manifest v2 deprecation |
| * and should be visible in the MV2 deprecation panel. |
| */ |
| private computeMv2DeprecatedExtensions_(): |
| chrome.developerPrivate.ExtensionInfo[] { |
| return this.extensions.filter((extension) => { |
| switch (this.mv2ExperimentStage_) { |
| case Mv2ExperimentStage.NONE: |
| return false; |
| case Mv2ExperimentStage.WARNING: |
| return extension.isAffectedByMV2Deprecation && |
| !extension.didAcknowledgeMV2DeprecationNotice; |
| case Mv2ExperimentStage.DISABLE_WITH_REENABLE: |
| return extension.isAffectedByMV2Deprecation && |
| extension.disableReasons.unsupportedManifestVersion && |
| !extension.didAcknowledgeMV2DeprecationNotice; |
| } |
| }); |
| } |
| |
| /** |
| * Computes the extensions that are potentially unsafe and should be visible |
| * in the review panel. |
| */ |
| private computeUnsafeExtensions_(): chrome.developerPrivate.ExtensionInfo[] { |
| return this.extensions?.filter( |
| extension => |
| !!(extension.safetyCheckText && |
| extension.safetyCheckText.panelString)); |
| } |
| |
| /** |
| * Returns whether the review deprecation panel should be visible. |
| */ |
| private computeShowSafetyCheckReviewPanel_(): boolean { |
| // Panel is hidden if neither safety feature is on. |
| if (!loadTimeData.getBoolean('safetyCheckShowReviewPanel') && |
| !loadTimeData.getBoolean('safetyHubShowReviewPanel')) { |
| return false; |
| } |
| |
| // If there are any unsafe extensions, they will be shown in the panel. |
| // Store this, so we can show the completion info in the panel when there |
| // are no unsafe extensions left after the user finished reviewing the |
| // extensions. |
| // Note: Unsafe extensions may not be initialized at construction, thus we |
| // check for their existence. |
| if (this.unsafeExtensions_?.length !== 0) { |
| this.reviewPanelShown_ = true; |
| } |
| |
| // Panel is visible if there are any unsafe extensions, or the there are |
| // none left after the user finished reviewing the extensions. |
| // Note: Unsafe extensions may not be initialized at construction, thus we |
| // check for their existence. |
| return this.unsafeExtensions_?.length !== 0 || this.reviewPanelShown_; |
| } |
| |
| private computeHasSafetyCheckTriggeringExtension_(): boolean { |
| if (!this.extensions) { |
| return false; |
| } |
| for (const extension of this.extensions) { |
| if (!!extension.safetyCheckText && |
| !!extension.safetyCheckText.panelString && |
| this.showSafetyCheckReviewPanel_) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns whether the manifest v2 deprecation panel should be visible. |
| */ |
| private computeShowMv2DeprecationPanel_(): boolean { |
| switch (this.mv2ExperimentStage_) { |
| case Mv2ExperimentStage.NONE: |
| return false; |
| case Mv2ExperimentStage.WARNING: |
| case Mv2ExperimentStage.DISABLE_WITH_REENABLE: |
| // Panel is visible when it has not been dismissed and at least one |
| // extension is affected by the MV2 deprecation. |
| return !this.isMv2DeprecationNoticeDismissed && |
| this.mv2DeprecatedExtensions_?.length !== 0; |
| } |
| } |
| |
| private shouldShowEmptyItemsMessage_() { |
| if (!this.apps || !this.extensions) { |
| return; |
| } |
| |
| return this.apps.length === 0 && this.extensions.length === 0; |
| } |
| |
| private shouldShowEmptySearchMessage_() { |
| return !this.shouldShowEmptyItemsMessage_() && this.shownAppsCount_ === 0 && |
| this.shownExtensionsCount_ === 0; |
| } |
| |
| private onNoExtensionsClick_(e: Event) { |
| if ((e.target as HTMLElement).tagName === 'A') { |
| chrome.metricsPrivate.recordUserAction('Options_GetMoreExtensions'); |
| } |
| } |
| |
| private announceSearchResults_() { |
| if (this.computedFilter_) { |
| setTimeout(() => { // Async to allow list to update. |
| const total = this.shownAppsCount_ + this.shownExtensionsCount_; |
| getAnnouncerInstance().announce(this.shouldShowEmptySearchMessage_() ? |
| this.i18n('noSearchResults') : |
| (total === 1 ? |
| this.i18n('searchResultsSingular', this.filter) : |
| this.i18n( |
| 'searchResultsPlural', total.toString(), this.filter))); |
| }, 0); |
| } |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'extensions-item-list': ExtensionsItemListElement; |
| } |
| } |
| |
| customElements.define(ExtensionsItemListElement.is, ExtensionsItemListElement); |