| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // 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.m.js'; |
| import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js'; |
| import 'chrome://resources/cr_elements/cr_icons_css.m.js'; |
| import 'chrome://resources/cr_elements/cr_toggle/cr_toggle.m.js'; |
| import 'chrome://resources/cr_elements/hidden_style_css.m.js'; |
| import 'chrome://resources/cr_elements/icons.m.js'; |
| import 'chrome://resources/cr_elements/shared_style_css.m.js'; |
| import 'chrome://resources/cr_elements/shared_vars_css.m.js'; |
| import 'chrome://resources/js/action_link.js'; |
| import 'chrome://resources/cr_elements/action_link_css.m.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/polymer/v3_0/iron-icon/iron-icon.js'; |
| import 'chrome://resources/polymer/v3_0/paper-tooltip/paper-tooltip.js'; |
| |
| import {ChromeEvent} from '/tools/typescript/definitions/chrome_event.js'; |
| import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js'; |
| import {CrToggleElement} from 'chrome://resources/cr_elements/cr_toggle/cr_toggle.m.js'; |
| import {assert, assertNotReached} from 'chrome://resources/js/assert_ts.js'; |
| import {I18nMixin} from 'chrome://resources/js/i18n_mixin.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, getItemSource, getItemSourceString, isEnabled, sortViews, SourceType, userCanChangeEnablement} from './item_util.js'; |
| import {navigation, Page} from './navigation_helper.js'; |
| |
| export interface ItemDelegate { |
| deleteItem(id: string): 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>; |
| |
| // 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, |
| }, |
| |
| // 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; |
| data: chrome.developerPrivate.ExtensionInfo; |
| private showingDetails_: boolean; |
| private firstInspectView_: chrome.developerPrivate.ExtensionView; |
| /** Prevents reloading the same item while it's already being reloaded. */ |
| private isReloading_: boolean = false; |
| |
| private fire_(eventName: string, detail?: any) { |
| this.dispatchEvent( |
| new CustomEvent(eventName, {bubbles: true, composed: true, detail})); |
| } |
| |
| getDetailsButton() { |
| return this.$.detailsButton; |
| } |
| |
| /** @return The "Errors" button, if it exists. */ |
| getErrorsButton(): HTMLElement|null { |
| return this.shadowRoot!.querySelector('#errors-button'); |
| } |
| |
| private observeIdVisibility_() { |
| flush(); |
| const idElement = this.shadowRoot!.querySelector('#extension-id'); |
| if (idElement) { |
| assert(this.data); |
| idElement.innerHTML = 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 onRemoveTap_() { |
| this.delegate.deleteItem(this.data.id); |
| } |
| |
| private onEnableToggleChange_() { |
| this.delegate.setItemEnabled(this.data.id, this.$.enableToggle.checked); |
| this.$.enableToggle.checked = this.isEnabled_(); |
| } |
| |
| private onErrorsTap_() { |
| 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 onDetailsTap_() { |
| navigation.navigateTo({page: Page.DETAILS, extensionId: this.data.id}); |
| } |
| |
| private computeFirstInspectView_(): chrome.developerPrivate.ExtensionView { |
| return sortViews(this.data.views)[0]; |
| } |
| |
| private onInspectTap_() { |
| this.delegate.inspectItemView(this.data.id, this.firstInspectView_); |
| } |
| |
| private onExtraInspectTap_() { |
| navigation.navigateTo({page: Page.DETAILS, extensionId: this.data.id}); |
| } |
| |
| private onReloadTap_() { |
| // Don't reload if in the middle of an update. |
| if (this.isReloading_) { |
| return; |
| } |
| |
| this.isReloading_ = true; |
| |
| const toastManager = getToastManager(); |
| // Keep the toast open indefinitely. |
| toastManager.duration = 0; |
| toastManager.show(this.i18n('itemReloading')); |
| this.delegate.reloadItem(this.data.id) |
| .then( |
| () => { |
| toastManager.hide(); |
| toastManager.duration = 3000; |
| toastManager.show(this.i18n('itemReloaded')); |
| this.isReloading_ = false; |
| }, |
| loadError => { |
| this.fire_('load-error', loadError); |
| toastManager.hide(); |
| this.isReloading_ = false; |
| }); |
| } |
| |
| private onRepairTap_() { |
| 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: |
| 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 { |
| // Only display the reload spinner if the extension is unpacked and |
| // enabled or disabled for reload. If an extension fails to reload (due to |
| // e.g. a parsing error), it will |
| // remain disabled with the "reloading" reason. We show the reload button |
| // when it's disabled for reload to enable developers to reload the fixed |
| // version. (Note that trying to reload an extension that is currently |
| // trying to reload is a no-op.) For other |
| // disableReasons, there's no point in reloading a disabled extension, and |
| // we'll show a crashed reload button if it's terminated. |
| const showIcon = |
| this.data.location === chrome.developerPrivate.Location.UNPACKED && |
| (this.data.state === chrome.developerPrivate.ExtensionState.ENABLED || |
| this.data.disableReasons.reloading); |
| return !showIcon; |
| } |
| |
| private computeExtraInspectLabel_(): string { |
| return this.i18n( |
| 'itemInspectViewsExtra', (this.data.views.length - 1).toString()); |
| } |
| |
| private hasSevereWarnings_(): boolean { |
| return this.data.disableReasons.corruptInstall || |
| this.data.disableReasons.suspiciousInstall || |
| this.data.runtimeWarnings.length > 0 || !!this.data.blacklistText; |
| } |
| |
| private showDescription_(): boolean { |
| return !this.hasSevereWarnings_() && |
| !this.data.showSafeBrowsingAllowlistWarning; |
| } |
| |
| private showAllowlistWarning_(): boolean { |
| // Only show the allowlist warning if there are no other warnings. 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.data.showSafeBrowsingAllowlistWarning && |
| !this.hasSevereWarnings_(); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'extensions-item': ExtensionsItemElement; |
| } |
| } |
| |
| customElements.define(ExtensionsItemElement.is, ExtensionsItemElement); |