/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint max-len: ["error", 80] */
/* import-globals-from aboutaddonsCommon.js */
/* import-globals-from abuse-reports.js */
/* import-globals-from view-controller.js */
/* global windowRoot */
"use strict";
ChromeUtils.defineESModuleGetters(
this, {
AMBrowserExtensionsImport:
"resource://gre/modules/AddonManager.sys.mjs",
AddonManager:
"resource://gre/modules/AddonManager.sys.mjs",
AddonRepository:
"resource://gre/modules/addons/AddonRepository.sys.mjs",
BuiltInThemes:
"resource:///modules/BuiltInThemes.sys.mjs",
ClientID:
"resource://gre/modules/ClientID.sys.mjs",
DeferredTask:
"resource://gre/modules/DeferredTask.sys.mjs",
E10SUtils:
"resource://gre/modules/E10SUtils.sys.mjs",
ExtensionCommon:
"resource://gre/modules/ExtensionCommon.sys.mjs",
ExtensionParent:
"resource://gre/modules/ExtensionParent.sys.mjs",
ExtensionPermissions:
"resource://gre/modules/ExtensionPermissions.sys.mjs",
PrivateBrowsingUtils:
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"manifestV3enabled",
"extensions.manifestV3.enabled"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"XPINSTALL_ENABLED",
"xpinstall.enabled",
true
);
const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000;
// 2 days (in milliseconds)
XPCOMUtils.defineLazyPreferenceGetter(
this,
"ABUSE_REPORT_ENABLED",
"extensions.abuseReport.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"LIST_RECOMMENDATIONS_ENABLED",
"extensions.htmlaboutaddons.recommendations.enabled",
false
);
const PLUGIN_ICON_URL =
"chrome://global/skin/icons/plugin.svg";
const EXTENSION_ICON_URL =
"chrome://mozapps/skin/extensions/extensionGeneric.svg";
const PERMISSION_MASKS = {
enable: AddonManager.PERM_CAN_ENABLE,
"always-activate": AddonManager.PERM_CAN_ENABLE,
disable: AddonManager.PERM_CAN_DISABLE,
"never-activate": AddonManager.PERM_CAN_DISABLE,
uninstall: AddonManager.PERM_CAN_UNINSTALL,
upgrade: AddonManager.PERM_CAN_UPGRADE,
"change-privatebrowsing": AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS,
};
const PREF_DISCOVERY_API_URL =
"extensions.getAddons.discovery.api_url";
const PREF_THEME_RECOMMENDATION_URL =
"extensions.recommendations.themeRecommendationUrl";
const PREF_RECOMMENDATION_HIDE_NOTICE =
"extensions.recommendations.hideNotice";
const PREF_PRIVACY_POLICY_URL =
"extensions.recommendations.privacyPolicyUrl";
const PREF_RECOMMENDATION_ENABLED =
"browser.discovery.enabled";
const PREF_TELEMETRY_ENABLED =
"datareporting.healthreport.uploadEnabled";
const PRIVATE_BROWSING_PERM_NAME =
"internal:privateBrowsingAllowed";
const PRIVATE_BROWSING_PERMS = {
permissions: [PRIVATE_BROWSING_PERM_NAME],
origins: [],
};
const L10N_ID_MAPPING = {
"theme-disabled-heading":
"theme-disabled-heading2",
};
function getL10nIdMapping(id) {
return L10N_ID_MAPPING[id] || id;
}
function shouldSkipAnimations() {
return (
document.body.hasAttribute(
"skip-animations") ||
window.matchMedia(
"(prefers-reduced-motion: reduce)").matches
);
}
function callListeners(name, args, listeners) {
for (let listener of listeners) {
try {
if (name in listener) {
listener[name](...args);
}
}
catch (e) {
Cu.reportError(e);
}
}
}
function getUpdateInstall(addon) {
return (
// Install object for a pending update.
addon.updateInstall ||
// Install object for a postponed upgrade (only for extensions,
// because is the only addon type that can postpone their own
// updates).
(addon.type ===
"extension" &&
addon.pendingUpgrade &&
addon.pendingUpgrade.install)
);
}
function isManualUpdate(install) {
let isManual =
install.existingAddon &&
!AddonManager.shouldAutoUpdate(install.existingAddon);
let isExtension =
install.existingAddon && install.existingAddon.type ==
"extension";
return (
(isManual && isInState(install,
"available")) ||
(isExtension && isInState(install,
"postponed"))
);
}
const AddonManagerListenerHandler = {
listeners:
new Set(),
addListener(listener) {
this.listeners.add(listener);
},
removeListener(listener) {
this.listeners.
delete(listener);
},
delegateEvent(name, args) {
callListeners(name, args,
this.listeners);
},
startup() {
this._listener =
new Proxy(
{},
{
has: () =>
true,
get:
(_, name) =>
(...args) =>
this.delegateEvent(name, args),
}
);
AddonManager.addAddonListener(
this._listener);
AddonManager.addInstallListener(
this._listener);
AddonManager.addManagerListener(
this._listener);
this._permissionHandler = (type, data) => {
if (type ==
"change-permissions") {
this.delegateEvent(
"onChangePermissions", [data]);
}
};
ExtensionPermissions.addListener(
this._permissionHandler);
},
shutdown() {
AddonManager.removeAddonListener(
this._listener);
AddonManager.removeInstallListener(
this._listener);
AddonManager.removeManagerListener(
this._listener);
ExtensionPermissions.removeListener(
this._permissionHandler);
},
};
/**
* This object wires the AddonManager event listeners into addon-card and
* addon-details elements rather than needing to add/remove listeners all the
* time as the view changes.
*/
const AddonCardListenerHandler =
new Proxy(
{},
{
has: () =>
true,
get(_, name) {
return (...args) => {
let elements = [];
let addonId;
// We expect args[0] to be of type:
// - AddonInstall, on AddonManager install events
// - AddonWrapper, on AddonManager addon events
// - undefined, on AddonManager manage events
if (args[0]) {
addonId =
args[0].addon?.id ||
args[0].existingAddon?.id ||
args[0].extensionId ||
args[0].id;
}
if (addonId) {
let cardSelector = `addon-card[addon-id=
"${addonId}"]`;
elements = document.querySelectorAll(
`${cardSelector}, ${cardSelector} addon-details`
);
}
else if (name ==
"onUpdateModeChanged") {
elements = document.querySelectorAll(
"addon-card");
}
callListeners(name, args, elements);
};
},
}
);
AddonManagerListenerHandler.addListener(AddonCardListenerHandler);
function isAbuseReportSupported(addon) {
return (
ABUSE_REPORT_ENABLED &&
AbuseReporter.isSupportedAddonType(addon.type) &&
!(addon.isBuiltin || addon.isSystem)
);
}
async
function isAllowedInPrivateBrowsing(addon) {
// Use the Promise directly so this function stays sync for the other case.
let perms = await ExtensionPermissions.get(addon.id);
return perms.permissions.includes(PRIVATE_BROWSING_PERM_NAME);
}
function hasPermission(addon, permission) {
return !!(addon.permissions & PERMISSION_MASKS[permission]);
}
function isInState(install, state) {
return install.state == AddonManager[
"STATE_" + state.toUpperCase()];
}
async
function getAddonMessageInfo(
addon,
{ isCardExpanded, isInDisabledSection }
) {
const { name } = addon;
const { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService;
if (addon.blocklistState === STATE_BLOCKED) {
let typeSuffix = addon.type ===
"extension" ?
"extension" :
"other";
return {
linkUrl: await addon.getBlocklistURL(),
linkId:
"details-notification-blocked-link2",
messageId: `details-notification-hard-blocked-${typeSuffix}`,
type:
"error",
};
}
else if (isDisabledUnsigned(addon)) {
return {
linkSumoPage:
"unsigned-addons",
messageId:
"details-notification-unsigned-and-disabled2",
messageArgs: { name },
type:
"error",
};
}
else if (
!addon.isCompatible &&
(AddonManager.checkCompatibility ||
addon.blocklistState !== STATE_SOFTBLOCKED)
) {
return {
// TODO: (Bug 1921870) consider adding a SUMO page.
// NOTE: this messagebar is customized by Thunderbird to include
// a non-SUMO link (see Bug 1921870 comment 0).
messageId:
"details-notification-incompatible2",
messageArgs: { name, version: Services.appinfo.version },
type:
"error",
};
}
else if (!isCorrectlySigned(addon)) {
return {
linkSumoPage:
"unsigned-addons",
messageId:
"details-notification-unsigned2",
messageArgs: { name },
type:
"warning",
};
}
else if (addon.blocklistState === STATE_SOFTBLOCKED) {
const fluentBaseId =
"details-notification-soft-blocked";
let typeSuffix = addon.type ===
"extension" ?
"extension" :
"other";
let stateSuffix;
// If the Addon Card is not expanded, delay changing the messagebar
// string to when the Addon card is refreshed as part of moving
// it between the enabled and disabled sections.
if (isCardExpanded) {
stateSuffix = addon.isActive ?
"enabled" :
"disabled";
}
else {
stateSuffix = !isInDisabledSection ?
"enabled" :
"disabled";
}
let messageId = `${fluentBaseId}-${typeSuffix}-${stateSuffix}`;
return {
linkUrl: await addon.getBlocklistURL(),
linkId:
"details-notification-softblocked-link2",
messageId,
type:
"warning",
};
}
else if (addon.isGMPlugin && !addon.isInstalled && addon.isActive) {
return {
messageId:
"details-notification-gmp-pending2",
messageArgs: { name },
type:
"warning",
};
}
return {};
}
function checkForUpdate(addon) {
return new Promise(resolve => {
let listener = {
onUpdateAvailable(addon, install) {
if (AddonManager.shouldAutoUpdate(addon)) {
// Make sure that an update handler is attached to all the install
// objects when updated xpis are going to be installed automatically.
attachUpdateHandler(install);
let failed = () => {
detachUpdateHandler(install);
install.removeListener(updateListener);
resolve({ installed:
false, pending:
false, found:
true });
};
let updateListener = {
onDownloadFailed: failed,
onInstallCancelled: failed,
onInstallFailed: failed,
onInstallEnded: () => {
detachUpdateHandler(install);
install.removeListener(updateListener);
resolve({ installed:
true, pending:
false, found:
true });
},
onInstallPostponed: () => {
detachUpdateHandler(install);
install.removeListener(updateListener);
resolve({ installed:
false, pending:
true, found:
true });
},
};
install.addListener(updateListener);
install.install();
}
else {
resolve({ installed:
false, pending:
true, found:
true });
}
},
onNoUpdateAvailable() {
resolve({ found:
false });
},
};
addon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
});
}
async
function checkForUpdates() {
let addons = await AddonManager.getAddonsByTypes(
null);
addons = addons.filter(addon => hasPermission(addon,
"upgrade"));
let updates = await Promise.all(addons.map(addon => checkForUpdate(addon)));
gViewController.notifyEMUpdateCheckFinished();
return updates.reduce(
(counts, update) => ({
installed: counts.installed + (update.installed ? 1 : 0),
pending: counts.pending + (update.pending ? 1 : 0),
found: counts.found + (update.found ? 1 : 0),
}),
{ installed: 0, pending: 0, found: 0 }
);
}
// Don't change how we handle this while the page is open.
const INLINE_OPTIONS_ENABLED = Services.prefs.getBoolPref(
"extensions.htmlaboutaddons.inline-options.enabled"
);
const OPTIONS_TYPE_MAP = {
[AddonManager.OPTIONS_TYPE_TAB]:
"tab",
[AddonManager.OPTIONS_TYPE_INLINE_BROWSER]: INLINE_OPTIONS_ENABLED
?
"inline"
:
"tab",
};
// Check if an add-on has the provided options type, accounting for the pref
// to disable inline options.
function getOptionsType(addon) {
return OPTIONS_TYPE_MAP[addon.optionsType];
}
// Check whether the options page can be loaded in the current browser window.
async
function isAddonOptionsUIAllowed(addon) {
if (addon.type !==
"extension" || !getOptionsType(addon)) {
// Themes never have options pages.
// Some plugins have preference pages, and they can always be shown.
// Extensions do not need to be checked if they do not have options pages.
return true;
}
if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
return true;
}
if (addon.incognito ===
"not_allowed") {
return false;
}
// The current page is in a private browsing window, and the add-on does not
// have the permission to access private browsing windows. Block access.
return (
// Note: This function is async because isAllowedInPrivateBrowsing is async.
isAllowedInPrivateBrowsing(addon)
);
}
let _templates = {};
/**
* Import a template from the main document.
*/
function importTemplate(name) {
if (!_templates.hasOwnProperty(name)) {
_templates[name] = document.querySelector(`template[name=
"${name}"]`);
}
let template = _templates[name];
if (template) {
return document.importNode(template.content,
true);
}
throw new Error(`Unknown template: ${name}`);
}
function nl2br(text) {
let frag = document.createDocumentFragment();
let hasAppended =
false;
for (let part of text.split(
"\n")) {
if (hasAppended) {
frag.appendChild(document.createElement(
"br"));
}
frag.appendChild(
new Text(part));
hasAppended =
true;
}
return frag;
}
/**
* Select the screeenshot to display above an add-on card.
*
* @param {AddonWrapper|DiscoAddonWrapper} addon
* @returns {string|null}
* The URL of the best fitting screenshot, if any.
*/
function getScreenshotUrlForAddon(addon) {
if (addon.id ==
"default-theme@mozilla.org") {
return "chrome://mozapps/content/extensions/default-theme/preview.svg";
}
const builtInThemePreview = BuiltInThemes.previewForBuiltInThemeId(addon.id);
if (builtInThemePreview) {
return builtInThemePreview;
}
let { screenshots } = addon;
if (!screenshots || !screenshots.length) {
return null;
}
// The image size is defined at .card-heading-image in aboutaddons.css, and
// is based on the aspect ratio for a 680x92 image. Use the image if possible,
// and otherwise fall back to the first image and hope for the best.
let screenshot = screenshots.find(s => s.width === 680 && s.height === 92);
if (!screenshot) {
console.warn(`Did not find screenshot with desired size
for ${addon.id}.`);
screenshot = screenshots[0];
}
return screenshot.url;
}
/**
* Adds UTM parameters to a given URL, if it is an AMO URL.
*
* @param {string} contentAttribute
* Identifies the part of the UI with which the link is associated.
* @param {string} url
* @returns {string}
* The url with UTM parameters if it is an AMO URL.
* Otherwise the url in unmodified form.
*/
function formatUTMParams(contentAttribute, url) {
let parsedUrl =
new URL(url);
let domain = `.${parsedUrl.hostname}`;
if (
!domain.endsWith(
".mozilla.org") &&
// For testing: addons-dev.allizom.org and addons.allizom.org
!domain.endsWith(
".allizom.org")
) {
return url;
}
parsedUrl.searchParams.set(
"utm_source",
"firefox-browser");
parsedUrl.searchParams.set(
"utm_medium",
"firefox-browser");
parsedUrl.searchParams.set(
"utm_content", contentAttribute);
return parsedUrl.href;
}
// A wrapper around an item from the "results" array from AMO's discovery API.
// See https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
class DiscoAddonWrapper {
/**
* @param {object} details
* An item in the "results" array from AMO's discovery API.
*/
constructor(details) {
// Reuse AddonRepository._parseAddon to have the AMO response parsing logic
// in one place.
let repositoryAddon = AddonRepository._parseAddon(details.addon);
// Note: Any property used by RecommendedAddonCard should appear here.
// The property names and values should have the same semantics as
// AddonWrapper, to ease the reuse of helper functions in this file.
this.id = repositoryAddon.id;
this.type = repositoryAddon.type;
this.name = repositoryAddon.name;
this.screenshots = repositoryAddon.screenshots;
this.sourceURI = repositoryAddon.sourceURI;
this.creator = repositoryAddon.creator;
this.averageRating = repositoryAddon.averageRating;
this.dailyUsers = details.addon.average_daily_users;
this.editorialDescription = details.description_text;
this.iconURL = details.addon.icon_url;
this.amoListingUrl = details.addon.url;
this.taarRecommended = details.is_recommendation;
}
}
/**
* A helper to retrieve the list of recommended add-ons via AMO's discovery API.
*/
var DiscoveryAPI = {
// Map<boolean, Promise> Promises from fetching the API results with or
// without a client ID. The `false` (no client ID) case could actually
// have been fetched with a client ID. See getResults() for more info.
_resultPromises:
new Map(),
/**
* Fetch the list of recommended add-ons. The results are cached.
*
* Pending requests are coalesced, so there is only one request at any given
* time. If a request fails, the pending promises are rejected, but a new
* call will result in a new request. A succesful response is cached for the
* lifetime of the document.
*
* @param {boolean} preferClientId
* A boolean indicating a preference for using a client ID.
* This will not overwrite the user preference but will
* avoid sending a client ID if no request has been made yet.
* @returns {Promise<DiscoAddonWrapper[]>}
*/
async getResults(preferClientId =
true) {
// Allow a caller to set preferClientId to false, but not true if discovery
// is disabled.
preferClientId = preferClientId &&
this.clientIdDiscoveryEnabled;
// Reuse a request for this preference first.
let resultPromise =
this._resultPromises.get(preferClientId) ||
// If the client ID isn't preferred, we can still reuse a request with the
// client ID.
(!preferClientId &&
this._resultPromises.get(
true));
if (resultPromise) {
return resultPromise;
}
// Nothing is prepared for this preference, make a new request.
resultPromise =
this._fetchRecommendedAddons(preferClientId).
catch(e => {
// Delete the pending promise, so _fetchRecommendedAddons can be
// called again at the next property access.
this._resultPromises.
delete(preferClientId);
Cu.reportError(e);
throw e;
});
// Store the new result for the preference.
this._resultPromises.set(preferClientId, resultPromise);
return resultPromise;
},
get clientIdDiscoveryEnabled() {
// These prefs match Discovery.sys.mjs for enabling clientId cookies.
return (
Services.prefs.getBoolPref(PREF_RECOMMENDATION_ENABLED,
false) &&
Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED,
false) &&
!PrivateBrowsingUtils.isContentWindowPrivate(window)
);
},
async _fetchRecommendedAddons(useClientId) {
let discoveryApiUrl =
new URL(
Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL)
);
if (useClientId) {
let clientId = await ClientID.getClientIdHash();
discoveryApiUrl.searchParams.set(
"telemetry-client-id", clientId);
}
let res = await fetch(discoveryApiUrl.href, {
credentials:
"omit",
});
if (!res.ok) {
throw new Error(`Failed to fetch recommended add-ons, ${res.status}`);
}
let { results } = await res.json();
return results.map(details =>
new DiscoAddonWrapper(details));
},
};
class SearchAddons
extends HTMLElement {
connectedCallback() {
if (
this.childElementCount === 0) {
this.input = document.createXULElement(
"search-textbox");
this.input.setAttribute(
"searchbutton",
true);
this.input.setAttribute(
"maxlength", 100);
this.input.setAttribute(
"data-l10n-attrs",
"placeholder");
document.l10n.setAttributes(
this.input,
"addons-heading-search-input");
this.append(
this.input);
}
this.input.addEventListener(
"command",
this);
}
disconnectedCallback() {
this.input.removeEventListener(
"command",
this);
}
handleEvent(e) {
if (e.type ===
"command") {
this.searchAddons(
this.value);
}
}
get value() {
return this.input.value;
}
searchAddons(query) {
if (query.length === 0) {
return;
}
let url = formatUTMParams(
"addons-manager-search",
AddonRepository.getSearchURL(query)
);
let browser = getBrowserElement();
let chromewin = browser.ownerGlobal;
chromewin.openWebLinkIn(url,
"tab");
}
}
customElements.define(
"search-addons", SearchAddons);
class MessageBarStackElement
extends HTMLElement {
constructor() {
super();
this._observer =
null;
const shadowRoot =
this.attachShadow({ mode:
"open" });
shadowRoot.append(
this.constructor.template.content.cloneNode(
true));
}
connectedCallback() {
// Close any message bar that should be allowed based on the
// maximum number of message bars.
this.closeMessageBars();
// Observe mutations to close older bars when new ones have been
// added.
this._observer =
new MutationObserver(() => {
this._observer.disconnect();
this.closeMessageBars();
this._observer.observe(
this, { childList:
true });
});
this._observer.observe(
this, { childList:
true });
}
disconnectedCallback() {
this._observer.disconnect();
this._observer =
null;
}
closeMessageBars() {
const { maxMessageBarCount } =
this;
if (maxMessageBarCount > 1) {
// Remove the older message bars if the stack reached the
// maximum number of message bars allowed.
while (
this.childElementCount > maxMessageBarCount) {
this.firstElementChild.remove();
}
}
}
get maxMessageBarCount() {
return parseInt(
this.getAttribute(
"max-message-bar-count"), 10);
}
static get template() {
const template = document.createElement(
"template");
const style = document.createElement(
"style");
// Render the stack in the reverse order if the stack has the
// reverse attribute set.
style.textContent = `
:host {
display: block;
}
:host([reverse]) > slot {
display: flex;
flex-direction: column-reverse;
}
`;
template.content.append(style);
template.content.append(document.createElement(
"slot"));
Object.defineProperty(
this,
"template", {
value: template,
});
return template;
}
}
customElements.define(
"message-bar-stack", MessageBarStackElement);
class GlobalWarnings
extends MessageBarStackElement {
constructor() {
super();
// This won't change at runtime, but we'll want to fake it in tests.
this.inSafeMode = Services.appinfo.inSafeMode;
this.globalWarning =
null;
}
connectedCallback() {
this.refresh();
this.addEventListener(
"click",
this);
AddonManagerListenerHandler.addListener(
this);
}
disconnectedCallback() {
this.removeEventListener(
"click",
this);
AddonManagerListenerHandler.removeListener(
this);
}
refresh() {
if (
this.inSafeMode) {
this.setWarning(
"safe-mode");
}
else if (
AddonManager.checkUpdateSecurityDefault &&
!AddonManager.checkUpdateSecurity
) {
this.setWarning(
"update-security", { action:
true });
}
else if (!AddonManager.checkCompatibility) {
this.setWarning(
"check-compatibility", { action:
true });
}
else if (AMBrowserExtensionsImport.canCompleteOrCancelInstalls) {
this.setWarning(
"imported-addons", { action:
true });
}
else {
this.removeWarning();
}
}
setWarning(type, opts) {
if (
this.globalWarning &&
this.globalWarning.getAttribute(
"warning-type") !== type
) {
this.removeWarning();
}
if (!
this.globalWarning) {
this.globalWarning = document.createElement(
"moz-message-bar");
this.globalWarning.setAttribute(
"warning-type", type);
let { messageId, buttonId } =
this.getGlobalWarningL10nIds(type);
document.l10n.setAttributes(
this.globalWarning, messageId);
this.globalWarning.setAttribute(
"data-l10n-attrs",
"message");
if (opts && opts.action) {
let button = document.createElement(
"button");
document.l10n.setAttributes(button, buttonId);
button.setAttribute(
"action", type);
button.setAttribute(
"slot",
"actions");
this.globalWarning.appendChild(button);
}
this.appendChild(
this.globalWarning);
}
}
getGlobalWarningL10nIds(type) {
const WARNING_TYPE_TO_L10NID_MAPPING = {
"safe-mode": {
messageId:
"extensions-warning-safe-mode2",
},
"update-security": {
messageId:
"extensions-warning-update-security2",
buttonId:
"extensions-warning-update-security-button",
},
"check-compatibility": {
messageId:
"extensions-warning-check-compatibility2",
buttonId:
"extensions-warning-check-compatibility-button",
},
"imported-addons": {
messageId:
"extensions-warning-imported-addons2",
buttonId:
"extensions-warning-imported-addons-button",
},
};
return WARNING_TYPE_TO_L10NID_MAPPING[type];
}
removeWarning() {
if (
this.globalWarning) {
this.globalWarning.remove();
this.globalWarning =
null;
}
}
handleEvent(e) {
if (e.type ===
"click") {
switch (e.target.getAttribute(
"action")) {
case "update-security":
AddonManager.checkUpdateSecurity =
true;
break;
case "check-compatibility":
AddonManager.checkCompatibility =
true;
break;
case "imported-addons":
AMBrowserExtensionsImport.completeInstalls();
break;
}
}
}
/**
* AddonManager listener events.
*/
onCompatibilityModeChanged() {
this.refresh();
}
onCheckUpdateSecurityChanged() {
this.refresh();
}
onBrowserExtensionsImportChanged() {
this.refresh();
}
}
customElements.define(
"global-warnings", GlobalWarnings);
class AddonPageHeader
extends HTMLElement {
connectedCallback() {
if (
this.childElementCount === 0) {
this.appendChild(importTemplate(
"addon-page-header"));
this.heading =
this.querySelector(
".header-name");
this.backButton =
this.querySelector(
".back-button");
this.pageOptionsMenuButton =
this.querySelector(
'[action="page-options"]'
);
// The addon-page-options element is outside of this element since this is
// position: sticky and that would break the positioning of the menu.
this.pageOptionsMenu = document.getElementById(
this.getAttribute(
"page-options-id")
);
}
document.addEventListener(
"view-selected",
this);
this.addEventListener(
"click",
this);
this.addEventListener(
"mousedown",
this);
// Use capture since the event is actually triggered on the internal
// panel-list and it doesn't bubble.
this.pageOptionsMenu.addEventListener(
"shown",
this,
true);
this.pageOptionsMenu.addEventListener(
"hidden",
this,
true);
}
disconnectedCallback() {
document.removeEventListener(
"view-selected",
this);
this.removeEventListener(
"click",
this);
this.removeEventListener(
"mousedown",
this);
this.pageOptionsMenu.removeEventListener(
"shown",
this,
true);
this.pageOptionsMenu.removeEventListener(
"hidden",
this,
true);
}
setViewInfo({ type, param }) {
this.setAttribute(
"current-view", type);
this.setAttribute(
"current-param", param);
let viewType = type ===
"list" ? param : type;
this.setAttribute(
"type", viewType);
this.heading.hidden = viewType ===
"detail";
this.backButton.hidden = viewType !==
"detail" && viewType !==
"shortcuts";
this.backButton.disabled = !history.state?.previousView;
if (viewType !==
"detail") {
document.l10n.setAttributes(
this.heading, `${viewType}-heading`);
}
}
handleEvent(e) {
let { backButton, pageOptionsMenu, pageOptionsMenuButton } =
this;
if (e.type ===
"click") {
switch (e.target) {
case backButton:
window.history.back();
break;
case pageOptionsMenuButton:
if (e.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
this.pageOptionsMenu.toggle(e);
}
break;
}
}
else if (
e.type ==
"mousedown" &&
e.target == pageOptionsMenuButton &&
e.button == 0
) {
this.pageOptionsMenu.toggle(e);
}
else if (
e.target == pageOptionsMenu.panel &&
(e.type ==
"shown" || e.type ==
"hidden")
) {
this.pageOptionsMenuButton.setAttribute(
"aria-expanded",
this.pageOptionsMenu.open
);
}
else if (e.target == document && e.type ==
"view-selected") {
const { type, param } = e.detail;
this.setViewInfo({ type, param });
}
}
}
customElements.define(
"addon-page-header", AddonPageHeader);
class AddonUpdatesMessage
extends HTMLElement {
static get observedAttributes() {
return [
"state"];
}
constructor() {
super();
this.attachShadow({ mode:
"open" });
let style = document.createElement(
"style");
style.textContent = `
@
import "chrome://global/skin/in-content/common.css";
button {
margin: 0;
}
`;
this.message = document.createElement(
"span");
this.message.hidden =
true;
this.button = document.createElement(
"button");
this.button.addEventListener(
"click", e => {
if (e.button === 0) {
gViewController.loadView(
"updates/available");
}
});
this.button.hidden =
true;
this.shadowRoot.append(style,
this.message,
this.button);
}
connectedCallback() {
document.l10n.connectRoot(
this.shadowRoot);
document.l10n.translateFragment(
this.shadowRoot);
}
disconnectedCallback() {
document.l10n.disconnectRoot(
this.shadowRoot);
}
attributeChangedCallback(name, oldVal, newVal) {
if (name ===
"state" && oldVal !== newVal) {
let l10nId = `addon-updates-${newVal}`;
switch (newVal) {
case "updating":
case "installed":
case "none-found":
this.button.hidden =
true;
this.message.hidden =
false;
document.l10n.setAttributes(
this.message, l10nId);
break;
case "manual-updates-found":
this.message.hidden =
true;
this.button.hidden =
false;
document.l10n.setAttributes(
this.button, l10nId);
break;
}
}
}
set state(val) {
this.setAttribute(
"state", val);
}
}
customElements.define(
"addon-updates-message", AddonUpdatesMessage);
class AddonPageOptions
extends HTMLElement {
connectedCallback() {
if (
this.childElementCount === 0) {
this.render();
}
this.addEventListener(
"click",
this);
this.panel.addEventListener(
"showing",
this);
AddonManagerListenerHandler.addListener(
this);
}
disconnectedCallback() {
this.removeEventListener(
"click",
this);
this.panel.removeEventListener(
"showing",
this);
AddonManagerListenerHandler.removeListener(
this);
}
toggle(...args) {
return this.panel.toggle(...args);
}
get open() {
return this.panel.open;
}
render() {
this.appendChild(importTemplate(
"addon-page-options"));
this.panel =
this.querySelector(
"panel-list");
this.installFromFile =
this.querySelector(
'[action="install-from-file"]');
this.toggleUpdatesEl =
this.querySelector(
'[action="set-update-automatically"]'
);
this.resetUpdatesEl =
this.querySelector(
'[action="reset-update-states"]');
this.onUpdateModeChanged();
}
async handleEvent(e) {
if (e.type ===
"click") {
e.target.disabled =
true;
try {
await
this.onClick(e);
}
finally {
e.target.disabled =
false;
}
}
else if (e.type ===
"showing") {
this.installFromFile.hidden = !XPINSTALL_ENABLED;
}
}
async onClick(e) {
switch (e.target.getAttribute(
"action")) {
case "check-for-updates":
await
this.checkForUpdates();
break;
case "view-recent-updates":
gViewController.loadView(
"updates/recent");
break;
case "install-from-file":
if (XPINSTALL_ENABLED) {
installAddonsFromFilePicker();
}
break;
case "debug-addons":
this.openAboutDebugging();
break;
case "set-update-automatically":
await
this.toggleAutomaticUpdates();
break;
case "reset-update-states":
await
this.resetAutomaticUpdates();
break;
case "manage-shortcuts":
gViewController.loadView(
"shortcuts/shortcuts");
break;
}
}
async checkForUpdates() {
let message = document.getElementById(
"updates-message");
message.state =
"updating";
message.hidden =
false;
let { installed, pending } = await checkForUpdates();
if (pending > 0) {
message.state =
"manual-updates-found";
}
else if (installed > 0) {
message.state =
"installed";
}
else {
message.state =
"none-found";
}
}
openAboutDebugging() {
let mainWindow = window.windowRoot.ownerGlobal;
if (
"switchToTabHavingURI" in mainWindow) {
let principal = Services.scriptSecurityManager.getSystemPrincipal();
mainWindow.switchToTabHavingURI(
`about:debugging#/runtime/this-firefox`,
true,
{
ignoreFragment:
"whenComparing",
triggeringPrincipal: principal,
}
);
}
}
automaticUpdatesEnabled() {
return AddonManager.updateEnabled && AddonManager.autoUpdateDefault;
}
toggleAutomaticUpdates() {
if (!
this.automaticUpdatesEnabled()) {
// One or both of the prefs is false, i.e. the checkbox is not
// checked. Now toggle both to true. If the user wants us to
// auto-update add-ons, we also need to auto-check for updates.
AddonManager.updateEnabled =
true;
AddonManager.autoUpdateDefault =
true;
}
else {
// Both prefs are true, i.e. the checkbox is checked.
// Toggle the auto pref to false, but don't touch the enabled check.
AddonManager.autoUpdateDefault =
false;
}
}
async resetAutomaticUpdates() {
let addons = await AddonManager.getAllAddons();
for (let addon of addons) {
if (
"applyBackgroundUpdates" in addon) {
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
}
}
}
/**
* AddonManager listener events.
*/
onUpdateModeChanged() {
let updatesEnabled =
this.automaticUpdatesEnabled();
this.toggleUpdatesEl.checked = updatesEnabled;
let resetType = updatesEnabled ?
"automatic" :
"manual";
let resetStringId = `addon-updates-reset-updates-to-${resetType}`;
document.l10n.setAttributes(
this.resetUpdatesEl, resetStringId);
}
}
customElements.define(
"addon-page-options", AddonPageOptions);
class CategoryButton
extends HTMLButtonElement {
connectedCallback() {
if (
this.childElementCount != 0) {
return;
}
// Make sure the aria-selected attribute is set correctly.
this.selected =
this.hasAttribute(
"selected");
document.l10n.setAttributes(
this, `addon-category-${
this.name}-title`);
let text = document.createElement(
"span");
text.classList.add(
"category-name");
document.l10n.setAttributes(text, `addon-category-${
this.name}`);
this.append(text);
}
load() {
gViewController.loadView(
this.viewId);
}
get isVisible() {
// Make a category button visible only if the related addon type is
// supported by the AddonManager Providers actually registered to
// the AddonManager.
return AddonManager.hasAddonType(
this.name);
}
get badgeCount() {
return parseInt(
this.getAttribute(
"badge-count"), 10) || 0;
}
set badgeCount(val) {
let count = parseInt(val, 10);
if (count) {
this.setAttribute(
"badge-count", count);
}
else {
this.removeAttribute(
"badge-count");
}
}
get selected() {
return this.hasAttribute(
"selected");
}
set selected(val) {
this.toggleAttribute(
"selected", !!val);
this.setAttribute(
"aria-selected", !!val);
}
get name() {
return this.getAttribute(
"name");
}
get viewId() {
return this.getAttribute(
"viewid");
}
// Just setting the hidden attribute isn't enough in case the category gets
// hidden while about:addons is closed since it could be the last active view
// which will unhide the button when it gets selected.
get defaultHidden() {
return this.hasAttribute(
"default-hidden");
}
}
customElements.define(
"category-button", CategoryButton, {
extends:
"button" });
class DiscoverButton
extends CategoryButton {
get isVisible() {
return isDiscoverEnabled();
}
}
customElements.define(
"discover-button", DiscoverButton, {
extends:
"button" });
// Create the button-group element so it gets loaded.
document.createElement(
"button-group");
class CategoriesBox
extends customElements.get(
"button-group") {
constructor() {
super();
// This will resolve when the initial category states have been set from
// our cached prefs. This is intended for use in testing to verify that we
// are caching the previous state.
this.promiseRendered =
new Promise(resolve => {
this._resolveRendered = resolve;
});
}
handleEvent(e) {
if (e.target == document && e.type ==
"view-selected") {
const { type, param } = e.detail;
this.select(`addons:
//${type}/${param}`);
return;
}
if (e.target ==
this && e.type ==
"button-group:key-selected") {
this.activeChild.load();
return;
}
if (e.type ==
"click") {
const button = e.target.closest(
"[viewid]");
if (button) {
button.load();
return;
}
}
// Forward the unhandled events to the button-group custom element.
super.handleEvent(e);
}
disconnectedCallback() {
document.removeEventListener(
"view-selected",
this);
this.removeEventListener(
"button-group:key-selected",
this);
this.removeEventListener(
"click",
this);
AddonManagerListenerHandler.removeListener(
this);
super.disconnectedCallback();
}
async initialize() {
let hiddenTypes =
new Set([]);
for (let button of
this.children) {
let { defaultHidden, name } = button;
button.hidden =
!button.isVisible || (defaultHidden &&
this.shouldHideCategory(name));
if (defaultHidden && AddonManager.hasAddonType(name)) {
hiddenTypes.add(name);
}
}
let hiddenUpdated;
if (hiddenTypes.size) {
hiddenUpdated =
this.updateHiddenCategories(Array.from(hiddenTypes));
}
this.updateAvailableCount();
document.addEventListener(
"view-selected",
this);
this.addEventListener(
"button-group:key-selected",
this);
this.addEventListener(
"click",
this);
AddonManagerListenerHandler.addListener(
this);
this._resolveRendered();
await hiddenUpdated;
}
shouldHideCategory(name) {
return Services.prefs.getBoolPref(`extensions.ui.${name}.hidden`,
true);
}
setShouldHideCategory(name, hide) {
Services.prefs.setBoolPref(`extensions.ui.${name}.hidden`, hide);
}
getButtonByName(name) {
return this.querySelector(`[name=
"${name}"]`);
}
get selectedChild() {
return this._selectedChild;
}
set selectedChild(node) {
if (node &&
this.contains(node)) {
if (
this._selectedChild) {
this._selectedChild.selected =
false;
}
this._selectedChild = node;
this._selectedChild.selected =
true;
}
}
select(viewId) {
let button =
this.querySelector(`[viewid=
"${viewId}"]`);
if (button) {
this.activeChild = button;
this.selectedChild = button;
button.hidden =
false;
Services.prefs.setStringPref(PREF_UI_LASTCATEGORY, viewId);
}
}
selectType(type) {
this.select(`addons:
//list/${type}`);
}
onInstalled(addon) {
let button =
this.getButtonByName(addon.type);
if (button) {
button.hidden =
false;
this.setShouldHideCategory(addon.type,
false);
}
this.updateAvailableCount();
}
onInstallStarted(install) {
this.onInstalled(install);
}
onNewInstall() {
this.updateAvailableCount();
}
onInstallPostponed() {
this.updateAvailableCount();
}
onInstallCancelled() {
this.updateAvailableCount();
}
async updateAvailableCount() {
let installs = await AddonManager.getAllInstalls();
var count = installs.filter(install => {
return isManualUpdate(install) && !install.installed;
}).length;
let availableButton =
this.getButtonByName(
"available-updates");
availableButton.hidden = !availableButton.selected && count == 0;
availableButton.badgeCount = count;
}
async updateHiddenCategories(types) {
let hiddenTypes =
new Set(types);
let getAddons = AddonManager.getAddonsByTypes(types);
let getInstalls = AddonManager.getInstallsByTypes(types);
for (let addon of await getAddons) {
if (addon.hidden) {
continue;
}
this.onInstalled(addon);
hiddenTypes.
delete(addon.type);
if (!hiddenTypes.size) {
return;
}
}
for (let install of await getInstalls) {
if (
install.existingAddon ||
install.state == AddonManager.STATE_AVAILABLE
) {
continue;
}
this.onInstalled(install);
hiddenTypes.
delete(install.type);
if (!hiddenTypes.size) {
return;
}
}
for (let type of hiddenTypes) {
let button =
this.getButtonByName(type);
if (button.selected) {
// Cancel the load if this view should be hidden.
gViewController.resetState();
}
this.setShouldHideCategory(type,
true);
button.hidden =
true;
}
}
}
customElements.define(
"categories-box", CategoriesBox);
class SidebarFooter
extends HTMLElement {
connectedCallback() {
let list = document.createElement(
"ul");
list.classList.add(
"sidebar-footer-list");
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
let prefsItem =
this.createItem({
icon:
"chrome://global/skin/icons/settings.svg",
createLinkElement: () => {
let link = document.createElement(
"a");
link.href =
"about:preferences";
link.id =
"preferencesButton";
return link;
},
titleL10nId:
"sidebar-settings-button-title",
labelL10nId:
"addons-settings-button",
onClick: e => {
e.preventDefault();
let hasAboutSettings = windowRoot.ownerGlobal.switchToTabHavingURI(
"about:settings",
false,
{
ignoreFragment:
"whenComparing",
}
);
if (!hasAboutSettings) {
windowRoot.ownerGlobal.switchToTabHavingURI(
"about:preferences",
true,
{
ignoreFragment:
"whenComparing",
triggeringPrincipal: systemPrincipal,
}
);
}
},
});
let supportItem =
this.createItem({
icon:
"chrome://global/skin/icons/help.svg",
createLinkElement: () => {
let link = document.createElement(
"a", { is:
"moz-support-link" });
link.setAttribute(
"support-page",
"addons-help");
link.id =
"help-button";
return link;
},
titleL10nId:
"sidebar-help-button-title",
labelL10nId:
"help-button",
});
list.append(prefsItem, supportItem);
this.append(list);
}
createItem({ onClick, titleL10nId, labelL10nId, icon, createLinkElement }) {
let listItem = document.createElement(
"li");
let link = createLinkElement();
link.classList.add(
"sidebar-footer-link");
link.addEventListener(
"click", onClick);
document.l10n.setAttributes(link, titleL10nId);
let img = document.createElement(
"img");
img.src = icon;
img.className =
"sidebar-footer-icon";
let label = document.createElement(
"span");
label.className =
"sidebar-footer-label";
document.l10n.setAttributes(label, labelL10nId);
link.append(img, label);
listItem.append(link);
return listItem;
}
}
customElements.define(
"sidebar-footer", SidebarFooter, {
extends:
"footer" });
class AddonOptions
extends HTMLElement {
connectedCallback() {
if (!
this.children.length) {
this.render();
}
}
get panel() {
return this.querySelector(
"panel-list");
}
updateSeparatorsVisibility() {
let lastSeparator;
let elWasVisible =
false;
// Collect the panel-list children that are not already hidden.
const children = Array.from(
this.panel.children).filter(el => !el.hidden);
for (let child of children) {
if (child.localName ==
"hr") {
child.hidden = !elWasVisible;
if (!child.hidden) {
lastSeparator = child;
}
elWasVisible =
false;
}
else {
elWasVisible =
true;
}
}
if (!elWasVisible && lastSeparator) {
lastSeparator.hidden =
true;
}
}
get template() {
return "addon-options";
}
render() {
this.appendChild(importTemplate(
this.template));
}
setElementState(el, card, addon, updateInstall) {
switch (el.getAttribute(
"action")) {
case "remove":
if (hasPermission(addon,
"uninstall")) {
// Regular add-on that can be uninstalled.
el.disabled =
false;
el.hidden =
false;
document.l10n.setAttributes(el,
"remove-addon-button");
}
else if (addon.isBuiltin) {
// Likely the built-in themes, can't be removed, that's fine.
el.hidden =
true;
}
else {
// Likely sideloaded, mention that it can't be removed with a link.
el.hidden =
false;
el.disabled =
true;
if (!el.querySelector(
'[slot="support-link"]')) {
let link = document.createElement(
"a", { is:
"moz-support-link" });
link.setAttribute(
"data-l10n-name",
"link");
link.setAttribute(
"support-page",
"cant-remove-addon");
link.setAttribute(
"slot",
"support-link");
el.appendChild(link);
document.l10n.setAttributes(el,
"remove-addon-disabled-button");
}
}
break;
case "report":
el.hidden = !isAbuseReportSupported(addon);
break;
case "install-update":
el.hidden = !updateInstall;
break;
case "expand":
el.hidden = card.expanded;
break;
case "preferences":
el.hidden =
getOptionsType(addon) !==
"tab" &&
(getOptionsType(addon) !==
"inline" || card.expanded);
if (!el.hidden) {
isAddonOptionsUIAllowed(addon).then(allowed => {
el.hidden = !allowed;
});
}
break;
}
}
update(card, addon, updateInstall) {
for (let el of
this.items) {
this.setElementState(el, card, addon, updateInstall);
}
// Update the separators visibility based on the updated visibility
// of the actions in the panel-list.
this.updateSeparatorsVisibility();
}
get items() {
return this.querySelectorAll(
"panel-item");
}
get visibleItems() {
return Array.from(
this.items).filter(item => !item.hidden);
}
}
customElements.define(
"addon-options", AddonOptions);
class PluginOptions
extends AddonOptions {
get template() {
return "plugin-options";
}
setElementState(el, card, addon) {
const userDisabledStates = {
"always-activate":
false,
"never-activate":
true,
};
const action = el.getAttribute(
"action");
if (action in userDisabledStates) {
let userDisabled = userDisabledStates[action];
el.checked = addon.userDisabled === userDisabled;
el.disabled = !(el.checked || hasPermission(addon, action));
}
else {
super.setElementState(el, card, addon);
}
}
}
customElements.define(
"plugin-options", PluginOptions);
class ProxyContextMenu
extends HTMLElement {
openPopupAtScreen(...args) {
// prettier-ignore
const parentContextMenuPopup =
windowRoot.ownerGlobal.document.getElementById(
"contentAreaContextMenu");
return parentContextMenuPopup.openPopupAtScreen(...args);
}
}
customElements.define(
"proxy-context-menu", ProxyContextMenu);
class InlineOptionsBrowser
extends HTMLElement {
constructor() {
super();
// Force the options_ui remote browser to recompute window.mozInnerScreenX
// and window.mozInnerScreenY when the "addon details" page has been
// scrolled (See Bug 1390445 for rationale).
// Also force a repaint to fix an issue where the click location was
// getting out of sync (see bug 1548687).
this.updatePositionTask =
new DeferredTask(() => {
if (
this.browser &&
this.browser.isRemoteBrowser) {
// Select boxes can appear in the wrong spot after scrolling, this will
// clear that up. Bug 1390445.
this.browser.frameLoader.requestUpdatePosition();
}
}, 100);
this._embedderElement =
null;
this._promiseDisconnected =
new Promise(
resolve => (
this._resolveDisconnected = resolve)
);
}
connectedCallback() {
window.addEventListener(
"scroll",
this,
true);
const { embedderElement } = top.browsingContext;
this._embedderElement = embedderElement;
embedderElement.addEventListener(
"FullZoomChange",
this);
embedderElement.addEventListener(
"TextZoomChange",
this);
}
disconnectedCallback() {
this._resolveDisconnected();
window.removeEventListener(
"scroll",
this,
true);
this._embedderElement?.removeEventListener(
"FullZoomChange",
this);
this._embedderElement?.removeEventListener(
"TextZoomChange",
this);
this._embedderElement =
null;
}
handleEvent(e) {
switch (e.type) {
case "scroll":
return this.updatePositionTask.arm();
case "FullZoomChange":
case "TextZoomChange":
return this.maybeUpdateZoom();
}
return undefined;
}
maybeUpdateZoom() {
let bc =
this.browser?.browsingContext;
let topBc = top.browsingContext;
if (!bc || !topBc) {
return;
}
// Use the same full-zoom as our top window.
bc.fullZoom = topBc.fullZoom;
bc.textZoom = topBc.textZoom;
}
setAddon(addon) {
this.addon = addon;
}
destroyBrowser() {
this.textContent =
"";
}
ensureBrowserCreated() {
if (
this.childElementCount === 0) {
this.render();
}
}
async render() {
let { addon } =
this;
if (!addon) {
throw new Error(
"addon required to create inline options");
}
let browser = document.createXULElement(
"browser");
browser.setAttribute(
"type",
"content");
browser.setAttribute(
"disableglobalhistory",
"true");
browser.setAttribute(
"messagemanagergroup",
"webext-browsers");
browser.setAttribute(
"id",
"addon-inline-options");
browser.setAttribute(
"class",
"addon-inline-options");
browser.setAttribute(
"transparent",
"true");
browser.setAttribute(
"forcemessagemanager",
"true");
browser.setAttribute(
"autocompletepopup",
"PopupAutoComplete");
let { optionsURL, optionsBrowserStyle } = addon;
if (addon.isWebExtension) {
let policy = ExtensionParent.WebExtensionPolicy.getByID(addon.id);
browser.setAttribute(
"initialBrowsingContextGroupId",
policy.browsingContextGroupId
);
}
let readyPromise;
let remoteSubframes = window.docShell.QueryInterface(
Ci.nsILoadContext
).useRemoteSubframes;
// For now originAttributes have no effect, which will change if the
// optionsURL becomes anything but moz-extension* or we start considering
// OA for extensions.
var oa = E10SUtils.predictOriginAttributes({ browser });
let loadRemote = E10SUtils.canLoadURIInRemoteType(
optionsURL,
remoteSubframes,
E10SUtils.EXTENSION_REMOTE_TYPE,
oa
);
if (loadRemote) {
browser.setAttribute(
"remote",
"true");
browser.setAttribute(
"remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
readyPromise = promiseEvent(
"XULFrameLoaderCreated", browser);
}
else {
readyPromise = promiseEvent(
"load", browser,
true);
}
this.appendChild(browser);
this.browser = browser;
// Force bindings to apply synchronously.
browser.clientTop;
await readyPromise;
this.maybeUpdateZoom();
if (!browser.messageManager) {
// If the browser.messageManager is undefined, the browser element has
// been removed from the document in the meantime (e.g. due to a rapid
// sequence of addon reload), return null.
return;
}
ExtensionParent.apiManager.emit(
"extension-browser-inserted", browser);
await
new Promise(resolve => {
let messageListener = {
receiveMessage({ name, data }) {
if (name ===
"Extension:BrowserResized") {
browser.style.height = `${data.height}px`;
}
else if (name ===
"Extension:BrowserContentLoaded") {
resolve();
}
},
};
let mm = browser.messageManager;
if (!mm) {
// If the browser.messageManager is undefined, the browser element has
// been removed from the document in the meantime (e.g. due to a rapid
// sequence of addon reload), return null.
resolve();
return;
}
mm.loadFrameScript(
"chrome://extensions/content/ext-browser-content.js",
false,
true
);
mm.addMessageListener(
"Extension:BrowserContentLoaded", messageListener);
mm.addMessageListener(
"Extension:BrowserResized", messageListener);
let browserOptions = {
fixedWidth:
true,
isInline:
true,
};
if (optionsBrowserStyle) {
// aboutaddons.js is not used on Android. extension.css is included in
// Firefox desktop and Thunderbird.
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
browserOptions.stylesheets = [
"chrome://browser/content/extension.css"];
}
mm.sendAsyncMessage(
"Extension:InitBrowser", browserOptions);
if (browser.isConnectedAndReady) {
this.fixupAndLoadURIString(optionsURL);
}
else {
// browser custom element does opt-in the delayConnectedCallback
// behavior (see connectedCallback in the custom element definition
// from browser-custom-element.js) and so calling browser.loadURI
// would fail if the about:addons document is not yet fully loaded.
Promise.race([
promiseEvent(
"DOMContentLoaded", document),
this._promiseDisconnected,
]).then(() => {
this.fixupAndLoadURIString(optionsURL);
});
}
});
}
fixupAndLoadURIString(uriString) {
if (!
this.browser || !
this.browser.isConnectedAndReady) {
throw new Error(
"Fail to loadURI");
}
this.browser.fixupAndLoadURIString(uriString, {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
}
}
customElements.define(
"inline-options-browser", InlineOptionsBrowser);
class UpdateReleaseNotes
extends HTMLElement {
connectedCallback() {
this.addEventListener(
"click",
this);
}
disconnectedCallback() {
this.removeEventListener(
"click",
this);
}
handleEvent(e) {
// We used to strip links, but ParserUtils.parseFragment() leaves them in,
// so just make sure we open them using the null principal in a new tab.
if (e.type ==
"click" && e.target.localName ==
"a" && e.target.href) {
e.preventDefault();
e.stopPropagation();
windowRoot.ownerGlobal.openWebLinkIn(e.target.href,
"tab");
}
}
async loadForUri(uri) {
// Can't load the release notes without a URL to load.
if (!uri || !uri.spec) {
this.setErrorMessage();
this.dispatchEvent(
new CustomEvent(
"release-notes-error"));
return;
}
// Don't try to load for the same update a second time.
if (
this.url == uri.spec) {
this.dispatchEvent(
new CustomEvent(
"release-notes-cached"));
return;
}
// Store the URL to skip the network if loaded again.
this.url = uri.spec;
// Set the loading message before hitting the network.
this.setLoadingMessage();
this.dispatchEvent(
new CustomEvent(
"release-notes-loading"));
try {
// loadReleaseNotes will fetch and sanitize the release notes.
let fragment = await loadReleaseNotes(uri);
this.textContent =
"";
this.appendChild(fragment);
this.dispatchEvent(
new CustomEvent(
"release-notes-loaded"));
}
catch (e) {
this.setErrorMessage();
this.dispatchEvent(
new CustomEvent(
"release-notes-error"));
}
}
setMessage(id) {
this.textContent =
"";
let message = document.createElement(
"p");
document.l10n.setAttributes(message, id);
this.appendChild(message);
}
setLoadingMessage() {
this.setMessage(
"release-notes-loading");
}
setErrorMessage() {
this.setMessage(
"release-notes-error");
}
}
customElements.define(
"update-release-notes", UpdateReleaseNotes);
class AddonPermissionsList
extends HTMLElement {
setAddon(addon) {
this.addon = addon;
this.render();
}
async render() {
let empty = { origins: [], permissions: [] };
let requiredPerms = { ...(
this.addon.userPermissions ?? empty) };
let optionalPerms = { ...(
this.addon.optionalPermissions ?? empty) };
let grantedPerms = await ExtensionPermissions.get(
this.addon.id);
if (manifestV3enabled) {
// If optional permissions include <all_urls>, extension can request and
// be granted permission for individual sites not listed in the manifest.
// Include them as well in the optional origins list.
let origins = [
...(
this.addon.optionalOriginsNormalized ?? []),
...grantedPerms.origins.filter(o => !requiredPerms.origins.includes(o)),
];
optionalPerms.origins = [...
new Set(origins)];
}
let permissions = Extension.formatPermissionStrings(
{
permissions: requiredPerms,
optionalPermissions: optionalPerms,
},
{ buildOptionalOrigins: manifestV3enabled }
);
let optionalEntries = [
...Object.entries(permissions.optionalPermissions),
...Object.entries(permissions.optionalOrigins),
];
this.textContent =
"";
let frag = importTemplate(
"addon-permissions-list");
if (permissions.msgs.length) {
let section = frag.querySelector(
".addon-permissions-required");
section.hidden =
false;
let list = section.querySelector(
".addon-permissions-list");
for (let msg of permissions.msgs) {
let item = document.createElement(
"li");
item.classList.add(
"permission-info",
"permission-checked");
item.appendChild(document.createTextNode(msg));
list.appendChild(item);
}
}
if (optionalEntries.length) {
let section = frag.querySelector(
".addon-permissions-optional");
section.hidden =
false;
let list = section.querySelector(
".addon-permissions-list");
for (let id = 0; id < optionalEntries.length; id++) {
let [perm, msg] = optionalEntries[id];
let type =
"permission";
if (permissions.optionalOrigins[perm]) {
type =
"origin";
}
let item = document.createElement(
"li");
item.classList.add(
"permission-info");
let toggle = document.createElement(
"moz-toggle");
toggle.setAttribute(
"label", msg);
toggle.id = `permission-${id}`;
toggle.setAttribute(
"permission-type", type);
let checked =
grantedPerms.permissions.includes(perm) ||
grantedPerms.origins.includes(perm);
// If this is one of the "all sites" permissions
if (Extension.isAllSitesPermission(perm)) {
// mark it as checked if ANY of the "all sites" permission is granted.
checked = await AddonCard.optionalAllSitesGranted(
this.addon.id);
toggle.toggleAttribute(
"permission-all-sites",
true);
}
toggle.pressed = checked;
item.classList.toggle(
"permission-checked", checked);
toggle.setAttribute(
"permission-key", perm);
toggle.setAttribute(
"action",
"toggle-permission");
if (perm ===
"userScripts") {
let mb = document.createElement(
"moz-message-bar");
mb.setAttribute(
"type",
"warning");
mb.messageL10nId =
"webext-perms-extra-warning-userScripts-long";
mb.slot =
"nested";
toggle.append(mb);
}
item.appendChild(toggle);
list.appendChild(item);
}
}
if (!permissions.msgs.length && !optionalEntries.length) {
let row = frag.querySelector(
".addon-permissions-empty");
row.hidden =
false;
}
this.appendChild(frag);
}
}
customElements.define(
"addon-permissions-list", AddonPermissionsList);
class AddonSitePermissionsList
extends HTMLElement {
setAddon(addon) {
this.addon = addon;
this.render();
}
async render() {
let permissions = Extension.formatPermissionStrings({
sitePermissions:
this.addon.sitePermissions,
siteOrigin:
this.addon.siteOrigin,
});
this.textContent =
"";
let frag = importTemplate(
"addon-sitepermissions-list");
if (permissions.msgs.length) {
let section = frag.querySelector(
".addon-permissions-required");
section.hidden =
false;
let list = section.querySelector(
".addon-permissions-list");
let header = section.querySelector(
".permission-header");
document.l10n.setAttributes(header,
"addon-sitepermissions-required", {
hostname:
new URL(
this.addon.siteOrigin).hostname,
});
for (let msg of permissions.msgs) {
let item = document.createElement(
"li");
item.classList.add(
"permission-info",
"permission-checked");
item.appendChild(document.createTextNode(msg));
list.appendChild(item);
}
}
this.appendChild(frag);
}
}
customElements.define(
"addon-sitepermissions-list", AddonSitePermissionsList);
class AddonDetails
extends HTMLElement {
connectedCallback() {
if (!
this.children.length) {
this.render();
}
this.deck.addEventListener(
"view-changed",
this);
this.descriptionShowMoreButton.addEventListener(
"click",
this);
}
disconnectedCallback() {
this.inlineOptions.destroyBrowser();
this.deck.removeEventListener(
"view-changed",
this);
this.descriptionShowMoreButton.removeEventListener(
"click",
this);
}
handleEvent(e) {
if (e.type ==
"view-changed" && e.target ==
this.deck) {
switch (
this.deck.selectedViewName) {
case "release-notes":
let releaseNotes =
this.querySelector(
"update-release-notes");
let uri =
this.releaseNotesUri;
if (uri) {
releaseNotes.loadForUri(uri);
}
break;
case "preferences":
if (getOptionsType(
this.addon) ==
"inline") {
this.inlineOptions.ensureBrowserCreated();
}
break;
}
// When a details view is rendered again, the default details view is
// unconditionally shown. So if any other tab is selected, do not save
// the current scroll offset, but start at the top of the page instead.
ScrollOffsets.canRestore =
this.deck.selectedViewName ===
"details";
}
else if (
e.type ==
"click" &&
e.target ==
this.descriptionShowMoreButton
) {
this.toggleDescription();
}
}
onInstalled() {
let policy = WebExtensionPolicy.getByID(
this.addon.id);
let extension = policy && policy.extension;
if (extension && extension.startupReason ===
"ADDON_UPGRADE") {
// Ensure the options browser is recreated when a new version starts.
this.extensionShutdown();
this.extensionStartup();
}
}
onDisabled() {
this.extensionShutdown();
}
onEnabled() {
this.extensionStartup();
}
extensionShutdown() {
this.inlineOptions.destroyBrowser();
}
extensionStartup() {
if (
this.deck.selectedViewName ===
"preferences") {
this.inlineOptions.ensureBrowserCreated();
}
}
toggleDescription() {
this.descriptionCollapsed = !
this.descriptionCollapsed;
this.descriptionWrapper.classList.toggle(
"addon-detail-description-collapse",
this.descriptionCollapsed
);
this.descriptionShowMoreButton.hidden =
false;
document.l10n.setAttributes(
this.descriptionShowMoreButton,
this.descriptionCollapsed
?
"addon-detail-description-expand"
:
"addon-detail-description-collapse"
);
}
get releaseNotesUri() {
--> --------------------
--> maximum size reached
--> --------------------