blob: 4abc1785c6601ce5c914e0183a5589daf641133d [file] [log] [blame]
// 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.m.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_vars_css.m.js';
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link_css.m.js';
import './icons.js';
import './shared_style.js';
import './shared_vars.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 {getInstance} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.m.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.m.js';
import {I18nBehavior} from 'chrome://resources/js/i18n_behavior.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {flush, html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ItemBehavior} from './item_behavior.js';
import {computeInspectableViewLabel, getItemSource, getItemSourceString, isControlled, isEnabled, SourceType, userCanChangeEnablement} from './item_util.js';
import {navigation, Page} from './navigation_helper.js';
/** @interface */
export class ItemDelegate {
/** @param {string} id */
deleteItem(id) {}
/**
* @param {string} id
* @param {boolean} isEnabled
*/
setItemEnabled(id, isEnabled) {}
/**
* @param {string} id
* @param {boolean} isAllowedIncognito
*/
setItemAllowedIncognito(id, isAllowedIncognito) {}
/**
* @param {string} id
* @param {boolean} isAllowedOnFileUrls
*/
setItemAllowedOnFileUrls(id, isAllowedOnFileUrls) {}
/**
* @param {string} id
* @param {!chrome.developerPrivate.HostAccess} hostAccess
*/
setItemHostAccess(id, hostAccess) {}
/**
* @param {string} id
* @param {boolean} collectsErrors
*/
setItemCollectsErrors(id, collectsErrors) {}
/**
* @param {string} id
* @param {chrome.developerPrivate.ExtensionView} view
*/
inspectItemView(id, view) {}
/**
* @param {string} url
*/
openUrl(url) {}
/**
* @param {string} id
* @return {!Promise}
*/
reloadItem(id) {}
/** @param {string} id */
repairItem(id) {}
/** @param {!chrome.developerPrivate.ExtensionInfo} extension */
showItemOptionsPage(extension) {}
/** @param {string} id */
showInFolder(id) {}
/**
* @param {string} id
* @return {!Promise<string>}
*/
getExtensionSize(id) {}
/**
* @param {string} id
* @param {string} host
* @return {!Promise<void>}
*/
addRuntimeHostPermission(id, host) {}
/**
* @param {string} id
* @param {string} host
* @return {!Promise<void>}
*/
removeRuntimeHostPermission(id, host) {}
}
Polymer({
is: 'extensions-item',
_template: html`{__html_template__}`,
behaviors: [I18nBehavior, ItemBehavior],
properties: {
// The item's delegate, or null.
delegate: {
type: Object,
},
// Whether or not dev mode is enabled.
inDevMode: {
type: Boolean,
value: false,
},
// The underlying ExtensionInfo itself. Public for use in declarative
// bindings.
/** @type {chrome.developerPrivate.ExtensionInfo} */
data: {
type: Object,
},
// Whether or not the expanded view of the item is shown.
/** @private */
showingDetails_: {
type: Boolean,
value: false,
},
},
/** Prevents reloading the same item while it's already being reloaded. */
isReloading_: false,
observers: [
'observeIdVisibility_(inDevMode, showingDetails_, data.id)',
],
/** @return {!HTMLElement} The "Details" button. */
getDetailsButton: function() {
return /** @type {!HTMLElement} */ (this.$.detailsButton);
},
/** @return {?HTMLElement} The "Errors" button, if it exists. */
getErrorsButton: function() {
return /** @type {?HTMLElement} */ (this.$$('#errors-button'));
},
/** @private string */
a11yAssociation_: function() {
// Don't use I18nBehavior.i18n because of additional checks it performs.
// Polymer ensures that this string is not stamped into arbitrary HTML.
// |this.data.name| can contain any data including html tags.
// ex: "My <video> download extension!"
return loadTimeData.getStringF('extensionA11yAssociation', this.data.name);
},
/** @private */
observeIdVisibility_: function(inDevMode, showingDetails, id) {
flush();
const idElement = this.$$('#extension-id');
if (idElement) {
assert(this.data);
idElement.innerHTML = this.i18n('itemId', this.data.id);
}
},
/**
* @return {boolean}
* @private
*/
shouldShowErrorsButton_: function() {
// 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_: function() {
this.delegate.deleteItem(this.data.id);
},
/** @private */
onEnableChange_: function() {
this.delegate.setItemEnabled(this.data.id, this.$['enable-toggle'].checked);
},
/** @private */
onErrorsTap_: function() {
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_: function() {
navigation.navigateTo({page: Page.DETAILS, extensionId: this.data.id});
},
/**
* @param {!{model: !{item: !chrome.developerPrivate.ExtensionView}}} e
* @private
*/
onInspectTap_: function(e) {
this.delegate.inspectItemView(this.data.id, this.data.views[0]);
},
/** @private */
onExtraInspectTap_: function() {
navigation.navigateTo({page: Page.DETAILS, extensionId: this.data.id});
},
/** @private */
onReloadTap_: function() {
// Don't reload if in the middle of an update.
if (this.isReloading_) {
return;
}
this.isReloading_ = true;
const toastManager = getInstance();
// Keep the toast open indefinitely.
toastManager.duration = 0;
toastManager.show(this.i18n('itemReloading'), false);
this.delegate.reloadItem(this.data.id)
.then(
() => {
toastManager.hide();
toastManager.duration = 3000;
toastManager.show(this.i18n('itemReloaded'), false);
this.isReloading_ = false;
},
loadError => {
this.fire('load-error', loadError);
toastManager.hide();
this.isReloading_ = false;
});
},
/** @private */
onRepairTap_: function() {
this.delegate.repairItem(this.data.id);
},
/**
* @return {boolean}
* @private
*/
isControlled_: function() {
return isControlled(this.data);
},
/**
* @return {boolean}
* @private
*/
isEnabled_: function() {
return isEnabled(this.data.state);
},
/**
* @return {boolean}
* @private
*/
isEnableToggleEnabled_: function() {
return userCanChangeEnablement(this.data);
},
/**
* Returns true if the enable toggle should be shown.
* @return {boolean}
* @private
*/
showEnableToggle_: function() {
return !this.isTerminated_() && !this.data.disableReasons.corruptInstall;
},
/**
* Returns true if the extension is in the terminated state.
* @return {boolean}
* @private
*/
isTerminated_: function() {
return this.data.state == chrome.developerPrivate.ExtensionState.TERMINATED;
},
/**
* return {string}
* @private
*/
computeClasses_: function() {
let classes = this.isEnabled_() ? 'enabled' : 'disabled';
if (this.inDevMode) {
classes += ' dev-mode';
}
return classes;
},
/**
* @return {string}
* @private
*/
computeSourceIndicatorIcon_: function() {
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 '';
}
assertNotReached();
},
/**
* @return {string}
* @private
*/
computeSourceIndicatorText_: function() {
if (this.data.locationText) {
return this.data.locationText;
}
const sourceType = getItemSource(this.data);
return sourceType == SourceType.WEBSTORE ? '' :
getItemSourceString(sourceType);
},
/**
* @return {boolean}
* @private
*/
computeInspectViewsHidden_: function() {
return !this.data.views || this.data.views.length == 0;
},
/**
* @return {string}
* @private
*/
computeFirstInspectTitle_: function() {
// 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.data.views[0]) :
'';
},
/**
* @return {string}
* @private
*/
computeFirstInspectLabel_: function() {
const label = this.computeFirstInspectTitle_();
return label && this.data.views.length > 1 ? label + ',' : label;
},
/**
* @return {boolean}
* @private
*/
computeExtraViewsHidden_: function() {
return this.data.views.length <= 1;
},
/**
* @return {boolean}
* @private
*/
computeDevReloadButtonHidden_: function() {
// Only display the reload spinner if the extension is unpacked and
// enabled. 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;
return !showIcon;
},
/**
* @return {string}
* @private
*/
computeExtraInspectLabel_: function() {
return this.i18n(
'itemInspectViewsExtra', (this.data.views.length - 1).toString());
},
/**
* @return {boolean}
* @private
*/
hasWarnings_: function() {
return this.data.disableReasons.corruptInstall ||
this.data.disableReasons.suspiciousInstall ||
this.data.runtimeWarnings.length > 0 || !!this.data.blacklistText;
},
/**
* @return {string}
* @private
*/
computeWarningsClasses_: function() {
return this.data.blacklistText ? 'severe' : 'mild';
},
});