summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions')
-rw-r--r--toolkit/components/extensions/Extension.sys.mjs93
-rw-r--r--toolkit/components/extensions/ExtensionContent.sys.mjs18
-rw-r--r--toolkit/components/extensions/ExtensionDNR.sys.mjs30
-rw-r--r--toolkit/components/extensions/ExtensionDNRLimits.sys.mjs129
-rw-r--r--toolkit/components/extensions/ExtensionParent.sys.mjs69
-rw-r--r--toolkit/components/extensions/ExtensionShortcuts.sys.mjs32
-rw-r--r--toolkit/components/extensions/ExtensionTelemetry.sys.mjs4
-rw-r--r--toolkit/components/extensions/ExtensionTestCommon.sys.mjs128
-rw-r--r--toolkit/components/extensions/Schemas.sys.mjs21
-rw-r--r--toolkit/components/extensions/WebNavigation.sys.mjs4
-rw-r--r--toolkit/components/extensions/child/ext-storage.js28
-rw-r--r--toolkit/components/extensions/metrics.yaml80
-rw-r--r--toolkit/components/extensions/parent/ext-declarativeNetRequest.js12
-rw-r--r--toolkit/components/extensions/parent/ext-management.js2
-rw-r--r--toolkit/components/extensions/parent/ext-runtime.js92
-rw-r--r--toolkit/components/extensions/schemas/declarative_net_request.json24
-rw-r--r--toolkit/components/extensions/schemas/geckoProfiler.json3
-rw-r--r--toolkit/components/extensions/schemas/management.json4
-rw-r--r--toolkit/components/extensions/schemas/manifest.json4
-rw-r--r--toolkit/components/extensions/schemas/runtime.json127
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js4
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js8
-rw-r--r--toolkit/components/extensions/test/marionette/manifest-serviceworker.toml1
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest-common.toml10
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_all_apis.js2
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html5
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_extension_getViews.html108
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_getContexts.html126
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html5
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html67
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js32
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js1
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js118
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js33
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_regexFilter_limits.js44
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_session_rules.js33
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js22
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_urlencoded.js2
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js39
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js2
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_permissions.js144
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_getContexts.js180
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_service_worker_messaging.js128
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js45
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_webRequest_eventPage_StreamFilter.js6
-rw-r--r--toolkit/components/extensions/test/xpcshell/xpcshell-common.toml2
-rw-r--r--toolkit/components/extensions/test/xpcshell/xpcshell-serviceworker.toml2
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp3
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionEventListener.h6
49 files changed, 1735 insertions, 347 deletions
diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs
index 8ab3c30234..56f1320d6c 100644
--- a/toolkit/components/extensions/Extension.sys.mjs
+++ b/toolkit/components/extensions/Extension.sys.mjs
@@ -161,6 +161,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
30 * 1000
);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "installIncludesOrigins",
+ "extensions.originControls.grantByDefault",
+ false
+);
+
var {
GlobalManager,
IconDetails,
@@ -509,14 +516,36 @@ var ExtensionAddonObserver = {
},
onUninstalled(addon) {
+ this.clearOnUninstall(addon.id);
+ },
+
+ /**
+ * Clears persistent state from the add-on post install.
+ *
+ * @param {string} addonId The ID of the addon that has been uninstalled.
+ */
+ clearOnUninstall(addonId) {
+ const tasks = [];
+ function addShutdownBlocker(name, promise) {
+ lazy.AsyncShutdown.profileChangeTeardown.addBlocker(name, promise);
+ tasks.push({ name, promise });
+ }
+ function notifyUninstallTaskObservers() {
+ Management.emit("cleanupAfterUninstall", addonId, tasks);
+ }
+
// Cleanup anything that is used by non-extension addon types
// since only extensions have uuid's.
- lazy.ExtensionPermissions.removeAll(addon.id);
+ addShutdownBlocker(
+ `Clear ExtensionPermissions for ${addonId}`,
+ lazy.ExtensionPermissions.removeAll(addonId)
+ );
- lazy.QuarantinedDomains.clearUserPref(addon.id);
+ lazy.QuarantinedDomains.clearUserPref(addonId);
- let uuid = UUIDMap.get(addon.id, false);
+ let uuid = UUIDMap.get(addonId, false);
if (!uuid) {
+ notifyUninstallTaskObservers();
return;
}
@@ -527,8 +556,8 @@ var ExtensionAddonObserver = {
);
// Clear all cached resources (e.g. CSS and images);
- lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear cache for ${addon.id}`,
+ addShutdownBlocker(
+ `Clear cache for ${addonId}`,
clearCacheForExtensionPrincipal(principal, /* clearAll */ true)
);
@@ -549,38 +578,38 @@ var ExtensionAddonObserver = {
// down because is being uninstalled) and then cleared from
// the persisted serviceworker registration on the next
// startup.
- lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear ServiceWorkers for ${addon.id}`,
+ addShutdownBlocker(
+ `Clear ServiceWorkers for ${addonId}`,
lazy.ServiceWorkerCleanUp.removeFromPrincipal(principal)
);
// Clear the persisted dynamic content scripts created with the scripting
// API (if any).
- lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear scripting store for ${addon.id}`,
- lazy.ExtensionScriptingStore.clearOnUninstall(addon.id)
+ addShutdownBlocker(
+ `Clear scripting store for ${addonId}`,
+ lazy.ExtensionScriptingStore.clearOnUninstall(addonId)
);
// Clear the DNR API's rules data persisted on disk (if any).
- lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear declarativeNetRequest store for ${addon.id}`,
+ addShutdownBlocker(
+ `Clear declarativeNetRequest store for ${addonId}`,
lazy.ExtensionDNRStore.clearOnUninstall(uuid)
);
if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
// Clear browser.storage.local backends.
- lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear Extension Storage ${addon.id} (File Backend)`,
- lazy.ExtensionStorage.clear(addon.id, { shouldNotifyListeners: false })
+ addShutdownBlocker(
+ `Clear Extension Storage ${addonId} (File Backend)`,
+ lazy.ExtensionStorage.clear(addonId, { shouldNotifyListeners: false })
);
// Clear browser.storage.sync rust-based backend.
// (storage.sync clearOnUninstall will resolve and log an error on the
// browser console in case of unexpected failures).
if (!lazy.storageSyncOldKintoBackend) {
- lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
- `Clear Extension StorageSync ${addon.id}`,
- lazy.extensionStorageSync.clearOnUninstall(addon.id)
+ addShutdownBlocker(
+ `Clear Extension StorageSync ${addonId}`,
+ lazy.extensionStorageSync.clearOnUninstall(addonId)
);
}
@@ -595,7 +624,7 @@ var ExtensionAddonObserver = {
});
Services.qms.clearStoragesForPrincipal(storagePrincipal);
- lazy.ExtensionStorageIDB.clearMigratedExtensionPref(addon.id);
+ lazy.ExtensionStorageIDB.clearMigratedExtensionPref(addonId);
// If LSNG is not enabled, we need to clear localStorage explicitly using
// the old API.
@@ -632,8 +661,10 @@ var ExtensionAddonObserver = {
if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
// Clear the entry in the UUID map
- UUIDMap.remove(addon.id);
+ UUIDMap.remove(addonId);
}
+
+ notifyUninstallTaskObservers();
},
onPropertyChanged(addon, properties) {
@@ -1172,8 +1203,10 @@ export class ExtensionData {
* includes the contents of the "permissions" property as well as other
* capabilities that are derived from manifest fields that users should
* be informed of (e.g., origins where content scripts are injected).
+ *
+ * For MV3 extensions with origin controls, this does not include origins.
*/
- get manifestPermissions() {
+ getRequiredPermissions() {
if (this.type !== "extension") {
return null;
}
@@ -1217,6 +1250,20 @@ export class ExtensionData {
}
/**
+ * Returns additional permissions that extensions is requesting based on its
+ * manifest. For now, this is host_permissions (and content scripts) in mv3.
+ */
+ getRequestedPermissions() {
+ if (this.type !== "extension") {
+ return null;
+ }
+ if (this.originControls && lazy.installIncludesOrigins) {
+ return { permissions: [], origins: this.getManifestOrigins() };
+ }
+ return { permissions: [], origins: [] };
+ }
+
+ /**
* Returns optional permissions from the manifest, including host permissions
* if originControls is true.
*/
@@ -3705,8 +3752,8 @@ export class Extension extends ExtensionData {
if (
this.originControls &&
- this.manifest.granted_host_permissions &&
- this.startupReason === "ADDON_INSTALL"
+ this.startupReason === "ADDON_INSTALL" &&
+ (this.manifest.granted_host_permissions || lazy.installIncludesOrigins)
) {
let origins = this.getManifestOrigins();
lazy.ExtensionPermissions.add(this.id, { permissions: [], origins });
diff --git a/toolkit/components/extensions/ExtensionContent.sys.mjs b/toolkit/components/extensions/ExtensionContent.sys.mjs
index a2fce282ee..83d17ca84e 100644
--- a/toolkit/components/extensions/ExtensionContent.sys.mjs
+++ b/toolkit/components/extensions/ExtensionContent.sys.mjs
@@ -475,6 +475,10 @@ class Script {
* execution is complete.
*/
async inject(context, reportExceptions = true) {
+ // NOTE: Avoid unnecessary use of "await" in this function, because doing
+ // so can delay script execution beyond the scheduled point. In particular,
+ // document_start scripts should run "immediately" in most cases.
+
DocumentManager.lazyInit();
if (this.requiresCleanup) {
context.addScript(this);
@@ -552,11 +556,19 @@ class Script {
let scripts = this.getCompiledScripts(context);
if (scripts instanceof Promise) {
+ // Note: in theory, the following async await could result in script
+ // execution being scheduled too late. That would be an issue for
+ // document_start scripts. In practice, this is not a problem because the
+ // compiled script is cached in the process, and preloading to compile
+ // starts as soon as the network request for the document has been
+ // received (see ExtensionPolicyService::CheckRequest).
scripts = await scripts;
}
- // Make sure we've injected any related CSS before we run content scripts.
- await cssPromise;
+ if (cssPromise) {
+ // Make sure we've injected any related CSS before we run content scripts.
+ await cssPromise;
+ }
let result;
@@ -623,7 +635,7 @@ class Script {
p.catch(error => {
Services.console.logMessage(
new ScriptError(
- `${error.name}: ${error.message}`,
+ error.toString(),
error.fileName,
null,
error.lineNumber,
diff --git a/toolkit/components/extensions/ExtensionDNR.sys.mjs b/toolkit/components/extensions/ExtensionDNR.sys.mjs
index afc7d30751..6fec9f7639 100644
--- a/toolkit/components/extensions/ExtensionDNR.sys.mjs
+++ b/toolkit/components/extensions/ExtensionDNR.sys.mjs
@@ -2175,12 +2175,34 @@ class RuleManager {
this.#updateAllowAllRequestRules();
}
- getSessionRules() {
- return this.sessionRules.rules;
+ /**
+ * Get the session scoped rules.
+ *
+ * @param {Array<integer>|null} ruleIds
+ Optional array of rule IDs to return. By default, all the session
+ scoped rules are returned.
+ */
+ getSessionRules(ruleIds = null) {
+ if (!ruleIds) {
+ return this.sessionRules.rules;
+ }
+
+ return this.sessionRules.rules.filter(rule => ruleIds.includes(rule.id));
}
- getDynamicRules() {
- return this.dynamicRules.rules;
+ /**
+ * Get the dynamic rules.
+ *
+ * @param {Array<integer>|null} ruleIds
+ Optional array of rule IDs to return. By default, all the dynamic
+ rules are returned.
+ */
+ getDynamicRules(ruleIds = null) {
+ if (!ruleIds) {
+ return this.dynamicRules.rules;
+ }
+
+ return this.dynamicRules.rules.filter(rule => ruleIds.includes(rule.id));
}
getRulesCount() {
diff --git a/toolkit/components/extensions/ExtensionDNRLimits.sys.mjs b/toolkit/components/extensions/ExtensionDNRLimits.sys.mjs
index ac4cd79c44..12919e4ee1 100644
--- a/toolkit/components/extensions/ExtensionDNRLimits.sys.mjs
+++ b/toolkit/components/extensions/ExtensionDNRLimits.sys.mjs
@@ -2,58 +2,99 @@
* 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/. */
-// TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs).
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-/**
- * The minimum number of static rules guaranteed to an extension across its
- * enabled static rulesets. Any rules above this limit will count towards the
- * global static rule limit.
- */
-const GUARANTEED_MINIMUM_STATIC_RULES = 30000;
+// TODO(Bug 1803370): allow extension to exceed the GUARANTEED_MINIMUM_STATIC_RULES limit.
+//
+// The maximum number of static rules exceeding the per-extension
+// GUARANTEED_MINIMUM_STATIC_RULES across every extensions.
+//
+// const MAX_GLOBAL_NUMBER_OF_STATIC_RULES = 300000;
-/**
- * The maximum number of static Rulesets an extension can specify as part of
- * the "rule_resources" manifest key.
- *
- * NOTE: this limit may be increased in the future, see https://github.com/w3c/webextensions/issues/318
- */
-const MAX_NUMBER_OF_STATIC_RULESETS = 50;
+const lazy = {};
-/**
- * The maximum number of static Rulesets an extension can enable at any one time.
- *
- * NOTE: this limit may be increased in the future, see https://github.com/w3c/webextensions/issues/318
- */
-const MAX_NUMBER_OF_ENABLED_STATIC_RULESETS = 10;
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "GUARANTEED_MINIMUM_STATIC_RULES",
+ "extensions.dnr.guaranteed_minimum_static_rules",
+ 30000
+);
-/**
- * The maximum number of dynamic and session rules an extension can add.
- * NOTE: in the Firefox we are enforcing this limit to the session and dynamic rules count separately,
- * instead of enforcing it to the rules count for both combined as the Chrome implementation does.
- *
- * NOTE: this limit may be increased in the future, see https://github.com/w3c/webextensions/issues/319
- */
-const MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES = 5000;
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "MAX_NUMBER_OF_STATIC_RULESETS",
+ "extensions.dnr.max_number_of_static_rulesets",
+ 100
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "MAX_NUMBER_OF_ENABLED_STATIC_RULESETS",
+ "extensions.dnr.max_number_of_enabled_static_rulesets",
+ 20
+);
/**
- * The maximum number of regular expression rules that an extension can add.
- * Session, dynamic and static rules have their own quota.
- *
- * TODO bug 1821033: Bump limit after optimizing regexFilter.
+ * NOTE: this limit may be increased in the future, see
+ * https://github.com/w3c/webextensions/issues/319
*/
-const MAX_NUMBER_OF_REGEX_RULES = 1000;
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES",
+ "extensions.dnr.max_number_of_dynamic_and_session_rules",
+ 5000
+);
-// TODO(Bug 1803370): allow extension to exceed the GUARANTEED_MINIMUM_STATIC_RULES limit.
-//
-// The maximum number of static rules exceeding the per-extension
-// GUARANTEED_MINIMUM_STATIC_RULES across every extensions.
-//
-// const MAX_GLOBAL_NUMBER_OF_STATIC_RULES = 300000;
+// TODO bug 1821033: Bump limit after optimizing regexFilter.
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "MAX_NUMBER_OF_REGEX_RULES",
+ "extensions.dnr.max_number_of_regex_rules",
+ 1000
+);
export const ExtensionDNRLimits = {
- GUARANTEED_MINIMUM_STATIC_RULES,
- MAX_NUMBER_OF_STATIC_RULESETS,
- MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
- MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES,
- MAX_NUMBER_OF_REGEX_RULES,
+ /**
+ * The minimum number of static rules guaranteed to an extension across its
+ * enabled static rulesets. Any rules above this limit will count towards the
+ * global static rule limit.
+ */
+ get GUARANTEED_MINIMUM_STATIC_RULES() {
+ return lazy.GUARANTEED_MINIMUM_STATIC_RULES;
+ },
+
+ /**
+ * The maximum number of static Rulesets an extension can specify as part of
+ * the "rule_resources" manifest key.
+ */
+ get MAX_NUMBER_OF_STATIC_RULESETS() {
+ return lazy.MAX_NUMBER_OF_STATIC_RULESETS;
+ },
+
+ /**
+ * The maximum number of static Rulesets an extension can enable at any one
+ * time.
+ */
+ get MAX_NUMBER_OF_ENABLED_STATIC_RULESETS() {
+ return lazy.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS;
+ },
+
+ /**
+ * The maximum number of dynamic and session rules an extension can add.
+ *
+ * NOTE: in the Firefox we are enforcing this limit to the session and
+ * dynamic rules count separately, instead of enforcing it to the rules count
+ * for both combined as the Chrome implementation does.
+ */
+ get MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES() {
+ return lazy.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES;
+ },
+
+ /**
+ * The maximum number of regular expression rules that an extension can add.
+ * Session, dynamic and static rules have their own quota.
+ */
+ get MAX_NUMBER_OF_REGEX_RULES() {
+ return lazy.MAX_NUMBER_OF_REGEX_RULES;
+ },
};
diff --git a/toolkit/components/extensions/ExtensionParent.sys.mjs b/toolkit/components/extensions/ExtensionParent.sys.mjs
index f951433713..bbc82092b0 100644
--- a/toolkit/components/extensions/ExtensionParent.sys.mjs
+++ b/toolkit/components/extensions/ExtensionParent.sys.mjs
@@ -31,6 +31,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
Schemas: "resource://gre/modules/Schemas.sys.mjs",
getErrorNameForTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
+ WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetters(lazy, {
@@ -508,6 +509,7 @@ class ProxyContextParent extends BaseContext {
this.childId = params.childId;
this.uri = Services.io.newURI(params.url);
+ this.browsingContext = browsingContext;
this.incognito = params.incognito;
@@ -548,6 +550,39 @@ class ProxyContextParent extends BaseContext {
return true;
}
+ get frameId() {
+ if (!this.browsingContext) {
+ return -1;
+ }
+
+ return lazy.WebNavigationFrames.getFrameId(this.browsingContext);
+ }
+
+ get contextType() {
+ switch (this.viewType) {
+ case "background_worker": // intentionally fall-through
+ case "background":
+ return "BACKGROUND";
+ case "popup":
+ return "POPUP";
+ case "sidebar":
+ return "SIDE_PANEL";
+ case "tab":
+ return "TAB";
+ default:
+ throw new Error(
+ `Unexpected missing contextType mapping for viewType "${this.viewType}"`
+ );
+ }
+ }
+
+ toExtensionContext() {
+ // NOTE: implemented in subclasses that should be listed in runtime.getContexts results
+ // when they match the ContextFilter, whereas instances from subclasses that don't
+ // implement it will always be filtered out.
+ return undefined;
+ }
+
trackRunListenerPromise(runListenerPromise) {
if (
// The extension was already shutdown.
@@ -746,6 +781,34 @@ class ExtensionPageContextParent extends ProxyContextParent {
return undefined;
}
+ toExtensionContext() {
+ const { tabTracker } = apiManager.global;
+ const { tabId, windowId } = tabTracker.getBrowserDataForContext(this);
+ const windowContext = this.browsingContext?.currentWindowContext;
+ return {
+ // NOTE: the contextId property in the final set of properties returned to
+ // extensions code is filled in on the ext-runtime.js and it is not to be
+ // confused with the internal property called contextId.
+ contextId: undefined,
+ // NOTE: contextType is a getter that maps the viewType property used
+ // internally with the value expected for the runtime.ExtensionContext
+ // contextType property (which should be one of the values part of the
+ // runtime.ContextType enum).
+ contextType: this.contextType,
+ // TODO(Bug 1891478): add documentId.
+ // TODO(Bug 1890739): consider switching this to use webExposedOriginSerialization when available
+ // Using nsIPrincipal.originNoSuffix to avoid including the
+ // private browsing (or contextual identity ones)
+ documentOrigin: windowContext?.documentPrincipal.originNoSuffix,
+ documentUrl: windowContext?.documentURI.spec,
+ incognito: this.incognito,
+ frameId: this.frameId,
+ tabId,
+ windowId,
+ // TODO: File followup to also add a Firefox-only userContextId?
+ };
+ }
+
unload() {
super.unload();
this.extension.views.delete(this);
@@ -775,6 +838,12 @@ class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
this._onResourceAvailable = this._onResourceAvailable.bind(this);
}
+ toExtensionContext() {
+ // NOTE: devtools extension contexts are currently omitted in getContexts
+ // results.
+ return undefined;
+ }
+
set devToolsToolbox(toolbox) {
if (this._devToolsToolbox) {
throw new Error("Cannot set the context DevTools toolbox twice");
diff --git a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs
index 17cff67eb9..e800fcc22d 100644
--- a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs
+++ b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs
@@ -261,7 +261,24 @@ export class ExtensionShortcuts {
if (storedCommand && storedCommand.value) {
commands.set(name, { ...manifestCommands.get(name) });
+
lazy.ExtensionSettingsStore.removeSetting(extension.id, "commands", name);
+ if (
+ name === "_execute_action" &&
+ extension.manifestVersion > 2 &&
+ lazy.ExtensionSettingsStore.hasSetting(
+ extension.id,
+ "commands",
+ "_execute_browser_action"
+ )
+ ) {
+ lazy.ExtensionSettingsStore.removeSetting(
+ extension.id,
+ "commands",
+ "_execute_browser_action"
+ );
+ }
+
this.registerKeys(commands);
}
}
@@ -285,6 +302,19 @@ export class ExtensionShortcuts {
let savedCommands = await this.loadCommandsFromStorage(extension.id);
savedCommands.forEach((update, name) => {
let command = commands.get(name);
+ if (
+ name === "_execute_browser_action" &&
+ extension.manifestVersion > 2
+ ) {
+ // Ignore the old _execute_browser_action if there is data stored for
+ // the new _execute_action command. Otherwise use the stored data for
+ // `_execute_action` (since we renamed `_execute_browser_action` to
+ // `_execute_action` in MV3).
+ command = savedCommands.has("_execute_action")
+ ? null
+ : commands.get("_execute_action");
+ }
+
if (command) {
// We will only update commands, not add them.
Object.assign(command, update);
@@ -419,7 +449,7 @@ export class ExtensionShortcuts {
}
doc.documentElement.appendChild(keyset);
if (sidebarKey) {
- window.SidebarUI.updateShortcut({ keyId: sidebarKey.id });
+ window.SidebarController.updateShortcut({ keyId: sidebarKey.id });
}
this.keysetsMap.set(window, keyset);
}
diff --git a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs
index 95b71ce007..58368c01f2 100644
--- a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs
+++ b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs
@@ -18,8 +18,6 @@ const HISTOGRAMS_IDS = {
eventPageIdleResult: "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT",
extensionStartup: "WEBEXT_EXTENSION_STARTUP_MS",
pageActionPopupOpen: "WEBEXT_PAGEACTION_POPUP_OPEN_MS",
- storageLocalGetJson: "WEBEXT_STORAGE_LOCAL_GET_MS",
- storageLocalSetJson: "WEBEXT_STORAGE_LOCAL_SET_MS",
storageLocalGetIdb: "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
storageLocalSetIdb: "WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
};
@@ -33,8 +31,6 @@ const GLEAN_METRICS_TYPES = {
eventPageIdleResult: "labeled_counter",
extensionStartup: "timing_distribution",
pageActionPopupOpen: "timing_distribution",
- storageLocalGetJson: "timing_distribution",
- storageLocalSetJson: "timing_distribution",
storageLocalGetIdb: "timing_distribution",
storageLocalSetIdb: "timing_distribution",
};
diff --git a/toolkit/components/extensions/ExtensionTestCommon.sys.mjs b/toolkit/components/extensions/ExtensionTestCommon.sys.mjs
index 701a85d97b..6a3b068dd2 100644
--- a/toolkit/components/extensions/ExtensionTestCommon.sys.mjs
+++ b/toolkit/components/extensions/ExtensionTestCommon.sys.mjs
@@ -17,10 +17,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
Assert: "resource://testing-common/Assert.sys.mjs",
Extension: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionAddonObserver: "resource://gre/modules/Extension.sys.mjs",
ExtensionData: "resource://gre/modules/Extension.sys.mjs",
ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
});
ChromeUtils.defineLazyGetter(
@@ -36,6 +39,110 @@ const { flushJarCache } = ExtensionUtils;
const { instanceOf } = ExtensionCommon;
+// The tasks received here have already been registered as shutdown blockers
+// (with AsyncShutdown). That means that in reality, if these tasks take
+// long, that they affect Firefox's ability to quit, with a warning after 10
+// seconds and a forced quit without waiting for shutdown after 60 seconds
+// (or whatever is in the toolkit.asyncshutdown.crash_timeout pref).
+//
+// To help with detecting unreasonable slowness in tests, we log when these
+// tasks are taking too long at shutdown.
+const MS_SLOW_TASK_DURATION = 2000;
+
+/**
+ * ExtensionUninstallTracker should be instantiated before extension shutdown,
+ * and can be used to await the completion of the uninstall and post-uninstall
+ * cleanup logic. Log messages are printed to aid debugging if the cleanup is
+ * observed to be slow (i.e. taking longer than MS_SLOW_TASK_DURATION).
+ *
+ * // Usage:
+ * let uninstallTracker = new ExtensionUninstallTracker(extension.id);
+ * await extension.shutdown();
+ * await uninstallTracker.waitForUninstallCleanupDone();
+ */
+class ExtensionUninstallTracker {
+ #resolveOnCleanupDone;
+ constructor(addonId) {
+ this.id = addonId;
+
+ this.remainingTasks = new Set();
+ // The uninstall/cleanup observer needs to be registered early, to not miss
+ // the notifications.
+ this._uninstallPromise = this.#promiseUninstallComplete();
+ this._cleanupPromise = this.#promiseCleanupAfterUninstall();
+ }
+
+ async waitForUninstallCleanupDone() {
+ // Call #addTask() now instead of in the constructor, so that we only track
+ // the time after extension.shutdown() has completed.
+ this.#addTask("Awaiting uninstall-complete", this._uninstallPromise);
+ this.#addTask("Awaiting cleanupAfterUninstall", this._cleanupPromise);
+ // For debugging purposes, if shutdown is slow, regularly print a message
+ // with the remaining tasks.
+ let timer = lazy.setInterval(
+ () => this.#checkRemainingTasks(),
+ 2 * MS_SLOW_TASK_DURATION
+ );
+ await new Promise(resolve => {
+ this.#resolveOnCleanupDone = resolve;
+ this.#checkRemainingTasks();
+ });
+ lazy.clearInterval(timer);
+ }
+
+ #addTask(name, promise) {
+ const task = { name, promise, timeStart: Date.now() };
+ this.remainingTasks.add(task);
+ promise.finally(() => {
+ this.remainingTasks.delete(task);
+ this.#checkRemainingTasks();
+ });
+ }
+
+ #checkRemainingTasks() {
+ for (let task of this.remainingTasks) {
+ const timeSinceStart = Date.now() - task.timeStart;
+ if (timeSinceStart > MS_SLOW_TASK_DURATION) {
+ dump(
+ `WARNING: Detected slow post-uninstall task: ${timeSinceStart}ms for extension ${this.id}: ${task.name}\n`
+ );
+ }
+ }
+ if (this.remainingTasks.size === 0) {
+ this.#resolveOnCleanupDone?.();
+ }
+ }
+
+ #promiseUninstallComplete() {
+ return new Promise(resolve => {
+ const onUninstallComplete = (eventName, { id }) => {
+ if (id === this.id) {
+ lazy.apiManager.off("uninstall-complete", onUninstallComplete);
+ resolve();
+ }
+ };
+ lazy.apiManager.on("uninstall-complete", onUninstallComplete);
+ });
+ }
+
+ #promiseCleanupAfterUninstall() {
+ return new Promise(resolve => {
+ const onCleanupAfterUninstall = (eventName, id, tasks) => {
+ if (id === this.id) {
+ lazy.apiManager.off("cleanupAfterUninstall", onCleanupAfterUninstall);
+ for (const task of tasks) {
+ if (task.promise) {
+ this.#addTask(task.name, task.promise);
+ }
+ }
+ resolve();
+ }
+ };
+ lazy.apiManager.on("cleanupAfterUninstall", onCleanupAfterUninstall);
+ });
+ }
+}
+
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
@@ -75,7 +182,6 @@ export class MockExtension {
this._extension = null;
this._extensionPromise = promiseEvent("startup");
this._readyPromise = promiseEvent("ready");
- this._uninstallPromise = promiseEvent("uninstall-complete");
}
maybeSetID(uri, id) {
@@ -674,4 +780,24 @@ export var ExtensionTestCommon = class ExtensionTestCommon {
data.startupReason ?? "ADDON_INSTALL"
);
}
+
+ /**
+ * Unload an extension and await completion of post-uninstall cleanup tasks.
+ *
+ * @param {Extension|MockExtension} extension
+ */
+ static async unloadTestExtension(extension) {
+ const { id } = extension;
+ const uninstallTracker = new ExtensionUninstallTracker(id);
+ await extension.shutdown();
+ if (extension instanceof lazy.Extension) {
+ // AddonManager-managed add-ons run additional (cleanup) tasks after
+ // shutting down an extension. Do the same even without useAddonManager.
+ lazy.Extension.getBootstrapScope().uninstall({ id });
+
+ // Data removal by ExtensionAddonObserver.onUninstalled:
+ lazy.ExtensionAddonObserver.clearOnUninstall(id);
+ }
+ await uninstallTracker.waitForUninstallCleanupDone();
+ }
};
diff --git a/toolkit/components/extensions/Schemas.sys.mjs b/toolkit/components/extensions/Schemas.sys.mjs
index b107036355..e95cbd6bb5 100644
--- a/toolkit/components/extensions/Schemas.sys.mjs
+++ b/toolkit/components/extensions/Schemas.sys.mjs
@@ -316,6 +316,27 @@ const POSTPROCESSORS = {
}
return value;
},
+
+ incognitoSplitUnsupportedAndFallback(value, context) {
+ if (value === "split") {
+ // incognito:split has not been implemented (bug 1380812). There are two
+ // alternatives: "spanning" and "not_allowed".
+ //
+ // "incognito":"split" is required by Chrome when extensions want to load
+ // any extension page in a tab in Chrome. In Firefox that is not required,
+ // so extensions could replace "split" with "spanning".
+ // Another (poorly documented) effect of "incognito":"split" is separation
+ // of some state between some extension APIs. Because this can in theory
+ // result in unwanted mixing of state between private and non-private
+ // browsing, we fall back to "not_allowed", which prevents the user from
+ // enabling the extension in private browsing windows.
+ value = "not_allowed";
+ context.logWarning(
+ `incognito "split" is unsupported. Falling back to incognito "${value}".`
+ );
+ }
+ return value;
+ },
};
// Parses a regular expression, with support for the Python extended
diff --git a/toolkit/components/extensions/WebNavigation.sys.mjs b/toolkit/components/extensions/WebNavigation.sys.mjs
index c33a45db81..98d537d85c 100644
--- a/toolkit/components/extensions/WebNavigation.sys.mjs
+++ b/toolkit/components/extensions/WebNavigation.sys.mjs
@@ -8,6 +8,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
ClickHandlerParent: "resource:///actors/ClickHandlerParent.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@@ -249,8 +250,7 @@ export var WebNavigationManager = {
onContentClick(target, data) {
// We are interested only on clicks to links which are not "add to bookmark" commands
if (data.href && !data.bookmark) {
- let ownerWin = target.ownerGlobal;
- let where = ownerWin.whereToOpenLink(data);
+ let where = lazy.BrowserUtils.whereToOpenLink(data);
if (where == "current") {
this.setRecentTabTransitionData({ link: true });
}
diff --git a/toolkit/components/extensions/child/ext-storage.js b/toolkit/components/extensions/child/ext-storage.js
index 3d71a1cd60..c6c67a8736 100644
--- a/toolkit/components/extensions/child/ext-storage.js
+++ b/toolkit/components/extensions/child/ext-storage.js
@@ -28,28 +28,16 @@ this.storage = class extends ExtensionAPI {
getLocalFileBackend(context, { deserialize, serialize }) {
return {
get(keys) {
- return measureOp(
- ExtensionTelemetry.storageLocalGetJson,
- context.extension,
- () => {
- return context.childManager
- .callParentAsyncFunction("storage.local.JSONFileBackend.get", [
- serialize(keys),
- ])
- .then(deserialize);
- }
- );
+ return context.childManager
+ .callParentAsyncFunction("storage.local.JSONFileBackend.get", [
+ serialize(keys),
+ ])
+ .then(deserialize);
},
set(items) {
- return measureOp(
- ExtensionTelemetry.storageLocalSetJson,
- context.extension,
- () => {
- return context.childManager.callParentAsyncFunction(
- "storage.local.JSONFileBackend.set",
- [serialize(items)]
- );
- }
+ return context.childManager.callParentAsyncFunction(
+ "storage.local.JSONFileBackend.set",
+ [serialize(items)]
);
},
remove(keys) {
diff --git a/toolkit/components/extensions/metrics.yaml b/toolkit/components/extensions/metrics.yaml
index 6d36dae614..7993832a64 100644
--- a/toolkit/components/extensions/metrics.yaml
+++ b/toolkit/components/extensions/metrics.yaml
@@ -58,6 +58,8 @@ extensions:
data_sensitivity:
- technical
telemetry_mirror: EXTENSIONS_STARTUPCACHE_LOAD_TIME
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
startup_cache_read_errors:
type: labeled_counter
@@ -75,6 +77,8 @@ extensions:
data_sensitivity:
- technical
telemetry_mirror: EXTENSIONS_STARTUPCACHE_READ_ERRORS
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
startup_cache_write_bytelength:
type: quantity
@@ -92,6 +96,8 @@ extensions:
data_sensitivity:
- technical
telemetry_mirror: EXTENSIONS_STARTUPCACHE_WRITE_BYTELENGTH
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
process_event:
type: labeled_counter
@@ -130,7 +136,6 @@ extensions.apis.dnr:
expires: 138
description: |
Amount of data read from the DNR startup cache file.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -149,7 +154,6 @@ extensions.apis.dnr:
expires: 138
description: |
Amount of time it takes to read data into the DNR startup cache file.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -168,7 +172,6 @@ extensions.apis.dnr:
expires: 138
description: |
Amount of data written to the DNR startup cache file.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -187,7 +190,6 @@ extensions.apis.dnr:
expires: 138
description: |
Amount of time it takes to write data into the DNR startup cache file.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -221,6 +223,8 @@ extensions.apis.dnr:
- hit
- miss
telemetry_mirror: EXTENSIONS_APIS_DNR_STARTUP_CACHE_ENTRIES
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
validate_rules_time:
type: timing_distribution
@@ -229,7 +233,6 @@ extensions.apis.dnr:
description: |
Amount of time it takes to validate DNR rules of individual ruleset
when dynamic or static rulesets have been loaded from disk.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -248,7 +251,6 @@ extensions.apis.dnr:
expires: 138
description: |
Amount of time it takes to evaluate DNR rules for one network request.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -378,6 +380,8 @@ extensions.quarantined_domains:
data_sensitivity:
- technical
telemetry_mirror: EXTENSIONS_QUARANTINEDDOMAINS_LISTSIZE
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
listhash:
type: string
@@ -396,6 +400,8 @@ extensions.quarantined_domains:
data_sensitivity:
- technical
telemetry_mirror: EXTENSIONS_QUARANTINEDDOMAINS_LISTHASH
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
remotehash:
type: string
@@ -417,6 +423,8 @@ extensions.quarantined_domains:
data_sensitivity:
- technical
telemetry_mirror: EXTENSIONS_QUARANTINEDDOMAINS_REMOTEHASH
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
extensions.counters:
@@ -488,7 +496,6 @@ extensions.timing:
description: |
Amount of time it takes to load a WebExtensions background page, from when the
build function is called to when the page has finished processing the onload event.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -506,7 +513,6 @@ extensions.timing:
expires: never
description: |
Amount of time it takes for a BrowserAction popup to open.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -532,7 +538,6 @@ extensions.timing:
expires: never
description: |
Amount of time it takes for content scripts from a WebExtension to be injected into a window.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -555,7 +560,6 @@ extensions.timing:
description: |
Amount of time (keyed by addon id) that an event page has been running before being suspended,
or the entire addon shutdown.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -573,7 +577,6 @@ extensions.timing:
description: |
Amount of time it takes for a WebExtension to start up, from when the
startup function is called to when the startup promise resolves.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -591,7 +594,6 @@ extensions.timing:
expires: never
description: |
Amount of time it takes for a PageAction popup to open.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -611,65 +613,12 @@ extensions.timing:
data_sensitivity:
- technical
- storage_local_get_json:
- type: timing_distribution
- time_unit: millisecond
- expires: 128
- description: |
- Amount of time it takes to perform a get via storage.local using the JSONFile backend.
- lifetime: application
- notification_emails:
- - addons-dev-internal@mozilla.com
- bugs:
- - https://bugzilla.mozilla.org/1371398
- - https://bugzilla.mozilla.org/1513556
- - https://bugzilla.mozilla.org/1578225
- - https://bugzilla.mozilla.org/1623315
- - https://bugzilla.mozilla.org/1666980
- - https://bugzilla.mozilla.org/1706839
- - https://bugzilla.mozilla.org/1745271
- - https://bugzilla.mozilla.org/1777402
- - https://bugzilla.mozilla.org/1811155
- - https://bugzilla.mozilla.org/1861303
- - https://bugzilla.mozilla.org/1820158
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1820158#c8
- data_sensitivity:
- - technical
-
- storage_local_set_json:
- type: timing_distribution
- time_unit: millisecond
- expires: 128
- description: |
- Amount of time it takes to perform a set via storage.local using the JSONFile backend.
- lifetime: application
- notification_emails:
- - addons-dev-internal@mozilla.com
- bugs:
- - https://bugzilla.mozilla.org/1371398
- - https://bugzilla.mozilla.org/1513556
- - https://bugzilla.mozilla.org/1578225
- - https://bugzilla.mozilla.org/1623315
- - https://bugzilla.mozilla.org/1666980
- - https://bugzilla.mozilla.org/1706839
- - https://bugzilla.mozilla.org/1745271
- - https://bugzilla.mozilla.org/1777402
- - https://bugzilla.mozilla.org/1811155
- - https://bugzilla.mozilla.org/1861303
- - https://bugzilla.mozilla.org/1820158
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1820158#c8
- data_sensitivity:
- - technical
-
storage_local_get_idb:
type: timing_distribution
time_unit: millisecond
expires: never
description: |
Amount of time it takes to perform a get via storage.local using the IndexedDB backend.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
@@ -695,7 +644,6 @@ extensions.timing:
expires: never
description: |
Amount of time it takes to perform a set via storage.local using the Indexed backend.
- lifetime: application
notification_emails:
- addons-dev-internal@mozilla.com
bugs:
diff --git a/toolkit/components/extensions/parent/ext-declarativeNetRequest.js b/toolkit/components/extensions/parent/ext-declarativeNetRequest.js
index fab1d941a4..2ca66cd1d4 100644
--- a/toolkit/components/extensions/parent/ext-declarativeNetRequest.js
+++ b/toolkit/components/extensions/parent/ext-declarativeNetRequest.js
@@ -92,17 +92,21 @@ this.declarativeNetRequest = class extends ExtensionAPI {
});
},
- async getDynamicRules() {
+ async getDynamicRules(details) {
await ExtensionDNR.ensureInitialized(extension);
- return ExtensionDNR.getRuleManager(extension).getDynamicRules();
+ return ExtensionDNR.getRuleManager(extension).getDynamicRules(
+ details?.ruleIds
+ );
},
- getSessionRules() {
+ getSessionRules(details) {
// ruleManager.getSessionRules() returns an array of Rule instances.
// When these are structurally cloned (to send them to the child),
// the enumerable public fields of the class instances are copied to
// plain objects, as desired.
- return ExtensionDNR.getRuleManager(extension).getSessionRules();
+ return ExtensionDNR.getRuleManager(extension).getSessionRules(
+ details?.ruleIds
+ );
},
isRegexSupported(regexOptions) {
diff --git a/toolkit/components/extensions/parent/ext-management.js b/toolkit/components/extensions/parent/ext-management.js
index fb28eca92c..67c254de99 100644
--- a/toolkit/components/extensions/parent/ext-management.js
+++ b/toolkit/components/extensions/parent/ext-management.js
@@ -42,6 +42,8 @@ const installType = addon => {
return "sideload";
} else if (addon.isSystem) {
return "other";
+ } else if (addon.isInstalledByEnterprisePolicy) {
+ return "admin";
}
return "normal";
};
diff --git a/toolkit/components/extensions/parent/ext-runtime.js b/toolkit/components/extensions/parent/ext-runtime.js
index 3f9c0f8857..ff7aae6e5f 100644
--- a/toolkit/components/extensions/parent/ext-runtime.js
+++ b/toolkit/components/extensions/parent/ext-runtime.js
@@ -12,6 +12,8 @@ var { ExtensionParent } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionParent.sys.mjs"
);
+var { DefaultWeakMap } = ExtensionUtils;
+
ChromeUtils.defineESModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
@@ -126,10 +128,100 @@ this.runtime = class extends ExtensionAPIPersistent {
},
};
+ // Although we have an internal context.contextId field, we generate a new one here because
+ // the internal type is an integer and the public field a (UUID) string.
+ // TODO: Move the implementation elsewhere when contextId is used anywhere other than
+ // runtime.getContexts. See https://bugzilla.mozilla.org/show_bug.cgi?id=1628178#c5
+ //
+ // Map<ProxyContextParent, string>
+ #contextUUIDMap = new DefaultWeakMap(_context =>
+ String(Services.uuid.generateUUID()).slice(1, -1)
+ );
+
+ getContexts(filter) {
+ const { extension } = this;
+ const { proxyContexts } = ExtensionParent.ParentAPIManager;
+ const results = [];
+ for (const proxyContext of proxyContexts.values()) {
+ if (proxyContext.extension !== extension) {
+ continue;
+ }
+ let ctx;
+ try {
+ ctx = proxyContext.toExtensionContext();
+ } catch (err) {
+ // toExtensionContext may throw if the contextType getter
+ // raised an exception due to an internal viewType has
+ // not be mapped with a contextType value.
+ //
+ // When running in DEBUG builds we reject the getContexts
+ // call, while in non DEBUG build we just omit the result
+ // and log a warning in the Browser Console.
+ if (AppConstants.DEBUG) {
+ throw err;
+ } else {
+ Cu.reportError(err);
+ }
+ }
+
+ if (this.matchContextFilter(filter, ctx)) {
+ results.push({
+ ...ctx,
+ contextId: this.#contextUUIDMap.get(proxyContext),
+ });
+ }
+ }
+ return results;
+ }
+
+ matchContextFilter(filter, ctx) {
+ if (!ctx) {
+ // Filter out subclasses that do not return any ExtensionContext details
+ // from their toExtensionContext method.
+ return false;
+ }
+ if (filter.contextIds && !filter.contextIds.includes(ctx.contextId)) {
+ return false;
+ }
+ if (filter.contextTypes && !filter.contextTypes.includes(ctx.contextType)) {
+ return false;
+ }
+ if (filter.documentIds && !filter.documentIds.includes(ctx.documentId)) {
+ return false;
+ }
+ if (
+ filter.documentOrigins &&
+ !filter.documentOrigins.includes(ctx.documentOrigin)
+ ) {
+ return false;
+ }
+ if (filter.documentUrls && !filter.documentUrls.includes(ctx.documentUrl)) {
+ return false;
+ }
+ if (filter.frameIds && !filter.frameIds.includes(ctx.frameId)) {
+ return false;
+ }
+ if (filter.tabIds && !filter.tabIds.includes(ctx.tabId)) {
+ return false;
+ }
+ if (filter.windowIds && !filter.windowIds.includes(ctx.windowId)) {
+ return false;
+ }
+ if (
+ typeof filter.incognito === "boolean" &&
+ ctx.incognito !== filter.incognito
+ ) {
+ return false;
+ }
+ return true;
+ }
+
getAPI(context) {
let { extension } = context;
return {
runtime: {
+ getContexts: filter => this.getContexts(filter),
+
// onStartup is special-cased in ext-backgroundPages to cause
// an immediate startup. We do not prime onStartup.
onStartup: new EventManager({
diff --git a/toolkit/components/extensions/schemas/declarative_net_request.json b/toolkit/components/extensions/schemas/declarative_net_request.json
index e7bdc02041..5c91fc1e35 100644
--- a/toolkit/components/extensions/schemas/declarative_net_request.json
+++ b/toolkit/components/extensions/schemas/declarative_net_request.json
@@ -439,6 +439,18 @@
}
}
}
+ },
+ {
+ "id": "GetRulesFilter",
+ "type": "object",
+ "properties": {
+ "ruleIds": {
+ "type": "array",
+ "optional": true,
+ "description": "If specified, only rules with matching IDs are included.",
+ "items": { "type": "integer" }
+ }
+ }
}
],
"functions": [
@@ -589,6 +601,12 @@
"async": "callback",
"parameters": [
{
+ "name": "filter",
+ "$ref": "GetRulesFilter",
+ "optional": true,
+ "description": "An object to filter the set of dynamic rules for the extension."
+ },
+ {
"name": "callback",
"type": "function",
"parameters": [
@@ -610,6 +628,12 @@
"async": "callback",
"parameters": [
{
+ "name": "filter",
+ "$ref": "GetRulesFilter",
+ "optional": true,
+ "description": "An object to filter the set of session scoped rules for the extension."
+ },
+ {
"name": "callback",
"type": "function",
"parameters": [
diff --git a/toolkit/components/extensions/schemas/geckoProfiler.json b/toolkit/components/extensions/schemas/geckoProfiler.json
index f2c6bec4cd..6da180fd49 100644
--- a/toolkit/components/extensions/schemas/geckoProfiler.json
+++ b/toolkit/components/extensions/schemas/geckoProfiler.json
@@ -47,7 +47,8 @@
"power",
"responsiveness",
"cpufreq",
- "bandwidth"
+ "bandwidth",
+ "memory"
]
},
{
diff --git a/toolkit/components/extensions/schemas/management.json b/toolkit/components/extensions/schemas/management.json
index b75c0e2b4c..069f3b030a 100644
--- a/toolkit/components/extensions/schemas/management.json
+++ b/toolkit/components/extensions/schemas/management.json
@@ -46,9 +46,9 @@
},
{
"id": "ExtensionInstallType",
- "description": "How the extension was installed. One of<br><var>development</var>: The extension was loaded unpacked in developer mode,<br><var>normal</var>: The extension was installed normally via an .xpi file,<br><var>sideload</var>: The extension was installed by other software on the machine,<br><var>other</var>: The extension was installed by other means.",
+ "description": "How the extension was installed. One of<br><var>development</var>: The extension was loaded unpacked in developer mode,<br><var>normal</var>: The extension was installed normally via an .xpi file,<br><var>sideload</var>: The extension was installed by other software on the machine,<br><var>admin</var>: The extension was installed by policy,<br><var>other</var>: The extension was installed by other means.",
"type": "string",
- "enum": ["development", "normal", "sideload", "other"]
+ "enum": ["development", "normal", "sideload", "admin", "other"]
},
{
"id": "ExtensionInfo",
diff --git a/toolkit/components/extensions/schemas/manifest.json b/toolkit/components/extensions/schemas/manifest.json
index 384b168e39..9701eda25a 100644
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -121,7 +121,9 @@
"incognito": {
"type": "string",
- "enum": ["not_allowed", "spanning"],
+ "description": "The 'split' value is not supported.",
+ "enum": ["not_allowed", "spanning", "split"],
+ "postprocess": "incognitoSplitUnsupportedAndFallback",
"default": "spanning",
"optional": true
},
diff --git a/toolkit/components/extensions/schemas/runtime.json b/toolkit/components/extensions/schemas/runtime.json
index 75ff341393..d2e21706e4 100644
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -19,6 +19,108 @@
"description": "Use the <code>browser.runtime</code> API to retrieve the background page, return details about the manifest, and listen for and respond to events in the app or extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.",
"types": [
{
+ "id": "ContextFilter",
+ "min_manifest_version": 3,
+ "type": "object",
+ "description": "A filter to match against existing extension context. Matching contexts must match all specified filters.",
+ "properties": {
+ "contextIds": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "contextTypes": {
+ "optional": true,
+ "type": "array",
+ "items": { "$ref": "ContextType" }
+ },
+ "documentIds": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "documentOrigins": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "documentUrls": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "frameIds": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "integer" }
+ },
+ "tabIds": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "integer" }
+ },
+ "windowIds": {
+ "optional": true,
+ "type": "array",
+ "items": { "type": "integer" }
+ },
+ "incognito": { "optional": true, "type": "boolean" }
+ }
+ },
+ {
+ "id": "ContextType",
+ "type": "string",
+ "enum": ["BACKGROUND", "POPUP", "SIDE_PANEL", "TAB"],
+ "description": "The type of extension view."
+ },
+ {
+ "id": "ExtensionContext",
+ "type": "object",
+ "description": "A context hosting extension content",
+ "properties": {
+ "contextId": {
+ "type": "string",
+ "description": "An unique identifier associated to this context"
+ },
+ "contextType": {
+ "$ref": "ContextType",
+ "description": "The type of the context"
+ },
+ "documentId": {
+ "type": "string",
+ "unsupported": true,
+ "optional": true,
+ "description": "An UUID for the document associated with this context, or undefined if it is not hosted in a document"
+ },
+ "documentOrigin": {
+ "type": "string",
+ "optional": true,
+ "description": "The origin of the document associated with this context, or undefined if it is not hosted in a document"
+ },
+ "documentUrl": {
+ "type": "string",
+ "optional": true,
+ "description": "The URL of the document associated with this context, or undefined if it is not hosted in a document"
+ },
+ "incognito": {
+ "type": "boolean",
+ "description": "Whether the context is associated with an private browsing context."
+ },
+ "frameId": {
+ "type": "integer",
+ "description": "The frame ID for this context, or -1 if it is not hosted in a frame."
+ },
+ "tabId": {
+ "type": "integer",
+ "description": "The tab ID for this context, or -1 if it is not hosted in a tab."
+ },
+ "windowId": {
+ "type": "integer",
+ "description": "The window ID for this context, or -1 if it is not hosted in a window."
+ }
+ }
+ },
+ {
"id": "Port",
"type": "object",
"allowedContexts": ["content", "devtools"],
@@ -219,6 +321,31 @@
]
},
{
+ "name": "getContexts",
+ "type": "function",
+ "description": "Fetches information about active contexts associated with this extension",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "filter",
+ "$ref": "ContextFilter",
+ "description": "A filter to find matching context."
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "contexts",
+ "type": "array",
+ "items": { "$ref": "ExtensionContext" },
+ "description": "The matching contexts, if any."
+ }
+ ]
+ }
+ ]
+ },
+ {
"name": "openOptionsPage",
"type": "function",
"description": "<p>Open your Extension's options page, if possible.</p><p>The precise behavior may depend on your manifest's <code>$(topic:optionsV2)[options_ui]</code> or <code>$(topic:options)[options_page]</code> key, or what the browser happens to support at the time.</p><p>If your Extension does not declare an options page, or the browser failed to create one for some other reason, the callback will set $(ref:lastError).</p>",
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js b/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js
index 587c5d4efe..86d794669d 100644
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js
@@ -139,7 +139,9 @@ add_task(async function test_getcurrent_privateBrowsing() {
"resource://gre/modules/ExtensionCommon.sys.mjs"
);
const { makeWidgetId } = ExtensionCommon;
- privateWin.SidebarUI.show(`${makeWidgetId(extension.id)}-sidebar-action`);
+ privateWin.SidebarController.show(
+ `${makeWidgetId(extension.id)}-sidebar-action`
+ );
let imageLoaded = extension.awaitMessage("theme-image");
Assert.deepEqual(await imageLoaded, { success: true }, "theme image loaded");
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
index 0d2e69716d..492fe3a263 100644
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
@@ -17,7 +17,7 @@ async function test_sidebar_theme(theme, isBrightText) {
const sidebarBox = document.getElementById("sidebar-box");
const browserRoot = document.documentElement;
- const content = SidebarUI.browser.contentWindow;
+ const content = SidebarController.browser.contentWindow;
const root = content.document.documentElement;
ok(
@@ -184,7 +184,7 @@ add_task(async function test_support_sidebar_colors() {
for (let command of ["viewBookmarksSidebar", "viewHistorySidebar"]) {
info("Executing command: " + command);
- await SidebarUI.show(command);
+ await SidebarController.show(command);
await test_sidebar_theme(
{
@@ -263,7 +263,7 @@ add_task(async function test_support_sidebar_border_color() {
"Sidebar splitter should be colored properly"
);
- SidebarUI.reversePosition();
+ SidebarController.reversePosition();
is(
sidebarSplitterCS.borderInlineStartColor,
@@ -271,7 +271,7 @@ add_task(async function test_support_sidebar_border_color() {
"Sidebar splitter should be colored properly after switching sides"
);
- SidebarUI.reversePosition();
+ SidebarController.reversePosition();
}
await extension.unload();
diff --git a/toolkit/components/extensions/test/marionette/manifest-serviceworker.toml b/toolkit/components/extensions/test/marionette/manifest-serviceworker.toml
index c8035f80c2..04bdc4e17e 100644
--- a/toolkit/components/extensions/test/marionette/manifest-serviceworker.toml
+++ b/toolkit/components/extensions/test/marionette/manifest-serviceworker.toml
@@ -1,4 +1,5 @@
[DEFAULT]
+skip-if = ["!nightly_build"] # MOZ_WEBEXT_WEBIDL_ENABLED is only defined on nightly
["test_extension_serviceworkers_purged_on_pref_disabled.py"]
["test_temporary_extension_serviceworkers_not_persisted.py"]
diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.toml b/toolkit/components/extensions/test/mochitest/mochitest-common.toml
index 782069a79c..f49fb131c5 100644
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.toml
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.toml
@@ -268,6 +268,8 @@ skip-if = [
"http2",
]
+["test_ext_extension_getViews.html"]
+
["test_ext_extension_iframe_messaging.html"]
skip-if = [
"http3",
@@ -350,6 +352,8 @@ skip-if = [
"http2",
]
+["test_ext_runtime_getContexts.html"]
+
["test_ext_script_filenames.html"]
["test_ext_scripting_contentScripts.html"]
@@ -443,12 +447,6 @@ skip-if = [
]
["test_ext_subframes_privileges.html"]
-skip-if = [
- "os == 'android'", # Bug 1845918
- "verify", # Bug 1489771
- "http3",
- "http2",
-]
["test_ext_tabs_captureTab.html"]
skip-if = [
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
index 95ac9af50d..8dec0c6ae5 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -89,8 +89,10 @@ let expectedBackgroundApis = [
"permissions.remove",
"permissions.onAdded",
"permissions.onRemoved",
+ "runtime.ContextType",
"runtime.getBackgroundPage",
"runtime.getBrowserInfo",
+ "runtime.getContexts",
"runtime.getPlatformInfo",
"runtime.onConnectExternal",
"runtime.onInstalled",
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html
index 076c177dfa..28bbe3b253 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html
@@ -14,7 +14,10 @@
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
- set: [["extensions.manifestV3.enabled", true]],
+ set: [
+ ["extensions.manifestV3.enabled", true],
+ ["extensions.originControls.grantByDefault", false],
+ ],
});
});
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_extension_getViews.html b/toolkit/components/extensions/test/mochitest/test_ext_extension_getViews.html
new file mode 100644
index 0000000000..9309d45cdf
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_extension_getViews.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>extension.getViews Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function genericChecker() {
+ const params = new URLSearchParams(window.location.search);
+ const kind = params.get("kind");
+ const createdTabIds = [];
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == `${kind}-test-get-views`) {
+ try {
+ let result = browser.extension.getViews({});
+ browser.test.assertEq(3, result.length, "Expect 3 extension views to be found");
+
+ result = browser.extension.getViews({ type: "tab" });
+ browser.test.assertEq(1, result.length, "Expect 1 tab extension view to be found");
+
+ result = browser.extension.getViews({ type: "popup" });
+ browser.test.assertEq(1, result.length, "Expect 1 popup extension view to be found");
+
+ browser.test.sendMessage(`${msg}:done`);
+ } catch (err) {
+ browser.test.fail(`browser.extension.getViews exception: ${err}`);
+ browser.test.sendMessage(`${msg}:done`);
+ }
+ } else if (msg == `${kind}-open-tab`) {
+ const tab = await browser.tabs.create({ url: "/page.html?kind=tab" });
+ createdTabIds.push(tab.id);
+ } else if (msg == `${kind}-close-tabs`) {
+ await browser.tabs.remove(createdTabIds);
+ browser.test.sendMessage(`${msg}:done`);
+ }
+ });
+
+ browser.test.log(`${kind} extension page loaded`);
+ browser.test.sendMessage(`${kind}-loaded`);
+}
+
+add_task(async function test_runtime_getContexts() {
+ const EXT_ID = "runtime-getContexts@mochitest";
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ incognitoOverride: "spanning",
+ manifest: {
+ manifest_version: 3,
+ browser_specific_settings: { gecko: { id: EXT_ID } },
+
+ action: {
+ default_popup: "page.html?kind=action",
+ default_area: "navbar",
+ },
+
+ background: {
+ page: "page.html?kind=background",
+ },
+ },
+
+ files: {
+ "page.html": `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"><\/head>
+ <body>
+ <script src="page.js"><\/script>
+ <\/body>
+ <\/html>
+ `,
+
+ "page.js": genericChecker,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-loaded");
+
+ extension.sendMessage("background-open-tab");
+ await extension.awaitMessage("tab-loaded");
+
+ await AppTestDelegate.clickBrowserAction(window, extension);
+ await extension.awaitMessage("action-loaded");
+
+ extension.sendMessage("background-test-get-views");
+ await extension.awaitMessage("background-test-get-views:done");
+
+ // Close popup window.
+ await AppTestDelegate.closeBrowserAction(window, extension);
+
+ // Close extension page tabs.
+ extension.sendMessage("background-close-tabs");
+ await extension.awaitMessage("background-close-tabs:done");
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_getContexts.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_getContexts.html
new file mode 100644
index 0000000000..fa3b7385da
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_getContexts.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>runtime.getContexts Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function genericChecker() {
+ const params = new URLSearchParams(window.location.search);
+ const kind = params.get("kind");
+ const createdTabIds = [];
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == `${kind}-test-get-contexts`) {
+ try {
+ const result = await browser.runtime.getContexts({});
+ browser.test.assertEq(3, result.length, "Expect 3 extension contexts to be found");
+
+ const bgContext = result.find(it => it.contextType === browser.runtime.ContextType.BACKGROUND);
+ const tabContext = result.find(it => it.documentUrl.endsWith("kind=tab"));
+ const popupContext = result.find(it => it.documentUrl.endsWith("kind=action"));
+
+ browser.test.assertEq(window.location.href, bgContext.documentUrl, "background context has the expected documentUrl");
+ browser.test.assertEq(-1, bgContext.windowId, "background context has the expected windowId");
+ browser.test.assertEq(-1, bgContext.tabId, "background context has the expected tabId");
+ browser.test.assertEq("TAB", tabContext.contextType, "Got expected tab context type");
+ browser.test.assertTrue(
+ typeof tabContext.windowId === "number" && tabContext.windowId > 0,
+ "Got expected windowId on tab context"
+ );
+ browser.test.assertTrue(
+ typeof tabContext.windowId === "number" && tabContext.tabId > 0,
+ "Got expected tabId on tab context"
+ );
+ browser.test.assertEq(
+ tabContext.windowId,
+ popupContext.windowId,
+ "Poup and tab expected to have the same windowId"
+ );
+ browser.test.assertEq(-1, popupContext.tabId, "popup context has the expected tabId");
+ browser.test.assertEq("POPUP", popupContext.contextType, "Got expected popup context type");
+
+ browser.test.sendMessage(`${msg}:done`);
+ } catch (err) {
+ browser.test.fail(`broser.runtime.getContexts call rejected: ${err}`);
+ browser.test.sendMessage(`${msg}:done`);
+ }
+ } else if (msg == `${kind}-open-tab`) {
+ const tab = await browser.tabs.create({ url: "/page.html?kind=tab" });
+ createdTabIds.push(tab.id);
+ } else if (msg == `${kind}-close-tabs`) {
+ await browser.tabs.remove(createdTabIds);
+ browser.test.sendMessage(`${msg}:done`);
+ }
+ });
+
+ browser.test.log(`${kind} extension page loaded`);
+ browser.test.sendMessage(`${kind}-loaded`);
+}
+
+add_task(async function test_runtime_getContexts() {
+ const EXT_ID = "runtime-getContexts@mochitest";
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ incognitoOverride: "spanning",
+ manifest: {
+ manifest_version: 3,
+ browser_specific_settings: { gecko: { id: EXT_ID } },
+
+ action: {
+ default_popup: "page.html?kind=action",
+ default_area: "navbar",
+ },
+
+ background: {
+ page: "page.html?kind=background",
+ },
+ },
+
+ files: {
+ "page.html": `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"><\/head>
+ <body>
+ <script src="page.js"><\/script>
+ <\/body>
+ <\/html>
+ `,
+
+ "page.js": genericChecker,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-loaded");
+
+ extension.sendMessage("background-open-tab");
+ await extension.awaitMessage("tab-loaded");
+
+ await AppTestDelegate.clickBrowserAction(window, extension);
+ await extension.awaitMessage("action-loaded");
+
+ extension.sendMessage("background-test-get-contexts");
+ await extension.awaitMessage("background-test-get-contexts:done");
+
+ // Close popup window.
+ await AppTestDelegate.closeBrowserAction(window, extension);
+
+ // Close extension page tabs.
+ extension.sendMessage("background-close-tabs");
+ await extension.awaitMessage("background-close-tabs:done");
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html
index 5eb2193409..a0ceed72d5 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_executeScript_activeTab.html
@@ -27,7 +27,10 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
- set: [["extensions.manifestV3.enabled", true]],
+ set: [
+ ["extensions.manifestV3.enabled", true],
+ ["extensions.originControls.grantByDefault", false],
+ ],
});
});
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
index f791d08602..0586275808 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
@@ -14,6 +14,32 @@
"use strict";
/* eslint-disable mozilla/balanced-listeners */
+const {
+ WebExtensionPolicy,
+} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services);
+
+
+// Some tests load non-moz-extension:-URLs in their extension document. When
+// extensions run in-process (extensions.webextensions.remote set to false),
+// that fails.
+// For details, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1724099 and
+// the same function in toolkit/components/extensions/test/xpcshell/head.js
+async function allow_unsafe_parent_loads_when_extensions_not_remote() {
+ if (!WebExtensionPolicy.useRemoteWebExtensions) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_unsafe_parent_loads", true]],
+ });
+ }
+}
+
+async function revert_allow_unsafe_parent_loads_when_extensions_not_remote() {
+ if (!WebExtensionPolicy.useRemoteWebExtensions) {
+ // Assume that the previous call to pushPrefEnv was from
+ // allow_unsafe_parent_loads_when_extensions_not_remote.
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
add_task(async function test_webext_tab_subframe_privileges() {
function background() {
browser.runtime.onMessage.addListener(async ({msg, success, tabId, error}) => {
@@ -198,19 +224,25 @@ add_task(async function test_webext_contentscript_iframe_subframe_privileges() {
});
add_task(async function test_webext_background_remote_subframe_privileges() {
- function backgroundSubframeScript() {
+ // file_remote_frame.html is opened at the same origin as this test page.
+ document.cookie = "cookie=monster";
+
+ function backgroundScript() {
window.addEventListener("message", evt => {
- browser.test.assertEq("http://mochi.test:8888", evt.origin, "postmessage origin ok");
+ browser.test.assertTrue(
+ evt.origin === "http://mochi.test:8888" ||
+ evt.origin === "https://mochi.test:8888", // using https-first, http2/http3 server.
+ `postmessage origin ok: ${evt.origin}`
+ );
browser.test.assertFalse(evt.data.tabs, "remote frame cannot access webextension APIs");
browser.test.assertEq("cookie=monster", evt.data.cookie, "Expected cookie value");
browser.test.notifyPass("webext-background-subframe-privileges");
}, {once: true});
- browser.cookies.set({url: "http://mochi.test:8888", name: "cookie", "value": "monster"});
}
let extensionData = {
manifest: {
- permissions: ["cookies", "*://mochi.test/*", "tabs"],
+ permissions: ["*://mochi.test/*", "tabs"],
background: {
page: "background.html",
},
@@ -219,32 +251,32 @@ add_task(async function test_webext_background_remote_subframe_privileges() {
"background.html": `<!DOCTYPE>
<head>
<meta charset="utf-8">
- <script src="background-subframe.js"><\/script>
+ <script src="background.js"><\/script>
</head>
<body>
<iframe src='${SimpleTest.getTestFileURL("file_remote_frame.html")}'></iframe>
</body>
</html>`,
- "background-subframe.js": backgroundSubframeScript,
+ "background.js": backgroundScript,
},
};
// Need remote webextensions to be able to load remote content from a background page.
- if (!SpecialPowers.getBoolPref("extensions.webextensions.remote", true)) {
- return;
- }
+ await allow_unsafe_parent_loads_when_extensions_not_remote();
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
await extension.awaitFinish("webext-background-subframe-privileges");
await extension.unload();
+ await revert_allow_unsafe_parent_loads_when_extensions_not_remote();
});
// Test a moz-extension:// iframe inside a content iframe in an extension page.
add_task(async function test_sub_subframe_conduit_verified_env() {
let manifest = {
content_scripts: [{
- matches: ["http://mochi.test/*/file_sample.html"],
+ // Note: no :8888 because of bug 1468162.
+ matches: ["*://mochi.test/*/file_sample.html"],
all_frames: true,
js: ["cs.js"],
}],
@@ -311,19 +343,20 @@ add_task(async function test_sub_subframe_conduit_verified_env() {
is(err, "Unknown sender or wrong actor for recvCreateProxyContext");
}
- let remote = SpecialPowers.getBoolPref("extensions.webextensions.remote");
-
let badProcess = { message: /Bad {[\w-]+} process: web/ };
let badPrincipal = { message: /Bad {[\w-]+} principal: http/ };
- consoleMonitor.start(remote ? [badPrincipal, badProcess] : [badProcess]);
+ consoleMonitor.start([badPrincipal, badProcess]);
let extension = ExtensionTestUtils.loadExtension({ manifest, files });
+
+
+ // Need OOP to spoof from a web iframe inside background page.
+ await allow_unsafe_parent_loads_when_extensions_not_remote();
await extension.startup();
- if (remote) {
- info("Need OOP to spoof from a web iframe inside background page.");
- await expectErrors(extension);
- }
+ info("Spoof from a web iframe inside background page.");
+ await expectErrors(extension);
+ await revert_allow_unsafe_parent_loads_when_extensions_not_remote();
info("Try spoofing from the web process.");
let win = window.open("./file_sample.html");
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js
index 6fae3b838a..c4f504c219 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_errors.js
@@ -18,6 +18,11 @@ add_task(async function test_cached_contentscript_on_document_start() {
// Use distinct content scripts as some will throw and would prevent executing the next script
{
matches: ["http://*/*/file_content_script_errors.html"],
+ js: ["script_does_not_exist.js"],
+ run_at: "document_start",
+ },
+ {
+ matches: ["http://*/*/file_content_script_errors.html"],
js: ["script1.js"],
run_at: "document_start",
},
@@ -41,6 +46,16 @@ add_task(async function test_cached_contentscript_on_document_start() {
js: ["script5.js"],
run_at: "document_start",
},
+ {
+ matches: ["http://*/*/file_content_script_errors.html"],
+ js: ["script6.js"],
+ run_at: "document_start",
+ },
+ {
+ matches: ["http://*/*/file_content_script_errors.html"],
+ js: ["script7.js"],
+ run_at: "document_start",
+ },
],
},
@@ -58,6 +73,12 @@ add_task(async function test_cached_contentscript_on_document_start() {
)
`,
"script5.js": `
+ throw null;
+ `,
+ "script6.js": `
+ throw Symbol("MySymbol");
+ `,
+ "script7.js": `
Promise.reject("rejected promise");
(async () => {
@@ -78,19 +99,22 @@ add_task(async function test_cached_contentscript_on_document_start() {
},
});
+ await extension.startup();
+
// Error messages, in roughly the order they appear above.
let expectedMessages = [
+ `Unable to load script: moz-extension://${extension.uuid}/script_does_not_exist.js`,
"Error: Object exception",
"uncaught exception: String exception",
"ReferenceError: undefinedSymbol is not defined",
"SyntaxError: expected expression, got ')'",
+ "uncaught exception: null",
+ "uncaught exception: Symbol(MySymbol)",
"uncaught exception: rejected promise",
"Error: async function exception",
"ReferenceError: asyncUndefinedSymbol is not defined",
];
- await extension.startup();
-
// Load a first page in order to be able to register a console listener in the content process.
// This has to be done in the same domain of the second page to stay in the same process.
let contentPage = await ExtensionTestUtils.loadContentPage(TEST_URL_1);
@@ -110,7 +134,7 @@ add_task(async function test_cached_contentscript_on_document_start() {
innerWindowID: error.innerWindowID,
message: error.errorMessage,
});
- if (this.collectedErrors.length == 7) {
+ if (this.collectedErrors.length == 10) {
Services.console.unregisterListener(this);
resolve(this.collectedErrors);
}
@@ -128,7 +152,7 @@ add_task(async function test_cached_contentscript_on_document_start() {
await extension.awaitMessage("content-script-loaded");
- equal(errors.length, 7);
+ equal(errors.length, 10);
let messages = [];
for (const { innerWindowID, message } of errors) {
equal(
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js
index 842994858e..dbaa01fa86 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_permissions_change.js
@@ -1,6 +1,7 @@
"use strict";
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+Services.prefs.setBoolPref("extensions.originControls.grantByDefault", false);
const server = createHttpServer({ hosts: ["example.com", "example.net"] });
server.registerDirectory("/data/", do_get_file("data"));
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js b/toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js
index e7dd1e99c6..b8fa021a1c 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js
@@ -61,17 +61,34 @@ function assertCookiesForHost(url, cookiesCount, message) {
// Test that the indexedDB and localStorage are allowed in an extension page
// and that the indexedDB is allowed in a extension worker.
add_task(async function test_ext_page_allowed_storage() {
- function testWebStorages() {
+ async function testWebStorages() {
const url = window.location.href;
try {
// In a webpage accessing indexedDB throws on cookiesBehavior reject,
// here we verify that doesn't happen for an extension page.
+
+ await new Promise((resolve, reject) => {
+ const begin = indexedDB.open("door");
+ begin.onsuccess = resolve;
+ begin.onerror = err => reject(err.target);
+ });
+
+ const dbs = await indexedDB.databases();
browser.test.assertTrue(
- indexedDB,
- "IndexedDB global should be accessible"
+ dbs.some(elem => elem.name === "door"),
+ "Just created database should be found"
);
+ await new Promise((resolve, reject) => {
+ const end = indexedDB.deleteDatabase("door");
+ end.onsuccess = () => {
+ browser.test.log(`IndexedDB is accessible`);
+ resolve();
+ };
+ end.onerror = reject;
+ });
+
// In a webpage localStorage is undefined on cookiesBehavior reject,
// here we verify that doesn't happen for an extension page.
browser.test.assertTrue(
@@ -97,9 +114,23 @@ add_task(async function test_ext_page_allowed_storage() {
}
function testWorker() {
- this.onmessage = () => {
+ this.onmessage = async () => {
try {
void indexedDB;
+ await new Promise((resolve, reject) => {
+ const begin = indexedDB.open("door");
+ begin.onerror = err => reject(err.target);
+ begin.onsuccess = () => {
+ indexedDB
+ .databases()
+ .then(() => {
+ const end = indexedDB.deleteDatabase("door");
+ end.onerror = err => reject(err.target);
+ end.onsuccess = resolve;
+ })
+ .catch(reject);
+ };
+ });
postMessage({ pass: true });
} catch (err) {
postMessage({ pass: false });
@@ -448,9 +479,43 @@ add_task(
let extFrame = this.content.document.querySelector("iframe#ext");
let webFrame = this.content.document.querySelector("iframe#web");
- function testIDB(win) {
+ async function testIDB(win) {
try {
- void win.indexedDB;
+ if (!win.indexedDB) {
+ Assert.ok(false, "IndexedDB global should be accessible");
+ return;
+ }
+
+ await new Promise((resolve, reject) => {
+ const req = win.indexedDB.open("door");
+ req.onerror = err => {
+ reject(err.target);
+ Assert.ok(
+ false,
+ "IDB open should be accessible: " + err.target.message
+ );
+ };
+ req.onsuccess = resolve;
+ });
+
+ const dbs = await win.indexedDB.databases();
+ Assert.ok(
+ dbs.some(elem => elem.name === "door"),
+ "Just created database should be found"
+ );
+
+ await new Promise((resolve, reject) => {
+ const req = win.indexedDB.deleteDatabase("door");
+ req.onerror = err => {
+ reject(err.target);
+ Assert.ok(
+ false,
+ "IDB deleteDatabase should be accessible: " + err.target.message
+ );
+ };
+ req.onsuccess = resolve;
+ });
+
return { success: true };
} catch (err) {
return { error: `${err}` };
@@ -467,10 +532,10 @@ add_task(
}
return {
- extTopLevel: testIDB(this.content),
+ extTopLevel: await testIDB(this.content),
// TODO bug 1762638: Execute the following in their own tasks.
- extSubFrame: testIDB(extFrame.contentWindow),
- webSubFrame: testIDB(webFrame.contentWindow),
+ extSubFrame: await testIDB(extFrame.contentWindow),
+ webSubFrame: await testIDB(webFrame.contentWindow),
webServiceWorker: await testServiceWorker(webFrame.contentWindow),
};
});
@@ -485,9 +550,30 @@ add_task(
return new Promise(resolve => {
let frame = this.content.document.createElement("iframe");
frame.setAttribute("src", `moz-extension://${uuid}/subframe.html`);
- frame.onload = () => {
+ frame.onload = async () => {
try {
- void frame.contentWindow.indexedDB;
+ if (!frame.contentWindow.indexedDB) {
+ throw Error("IndexedDB global should be accessible");
+ }
+ const indexedDB = frame.contentWindow.indexedDB;
+
+ await new Promise((success, failure) => {
+ const begin = indexedDB.open("door");
+ begin.onsuccess = success;
+ begin.onerror = err => failure(err.target);
+ });
+
+ const dbs = await indexedDB.databases();
+ if (!dbs.some(elem => elem.name === "door")) {
+ throw Error("Just created database should be found");
+ }
+
+ await new Promise((success, failure) => {
+ const end = indexedDB.deleteDatabase("door");
+ end.onsuccess = success;
+ end.onerror = err => failure(err.target);
+ });
+
resolve({ success: true });
} catch (err) {
resolve({ error: `${err}` });
@@ -512,7 +598,7 @@ add_task(
Assert.deepEqual(
results.webSubFrame,
- { error: "SecurityError: The operation is insecure." },
+ { error: "SecurityError: IDBFactory.open: The operation is insecure" },
"IndexedDB not allowed in a subframe webpage with a top level extension page"
);
Assert.deepEqual(
@@ -544,8 +630,14 @@ add_task(async function test_content_script_on_cookieBehaviorReject() {
function contentScript() {
// Ensure that when the current cookieBehavior doesn't allow a webpage to use indexedDB
// or localStorage, then a WebExtension content script is not allowed to use it as well.
+ browser.test.assertTrue(indexedDB, "IndexedDB handle should be accessible");
+
browser.test.assertThrows(
- () => indexedDB,
+ () => {
+ indexedDB.open("door").onsuccess = () => {
+ browser.test.fail(`Unreached function`);
+ };
+ },
/The operation is insecure/,
"a content script can't use indexedDB from a page where it is disallowed"
);
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js
index 4ba120852f..ff87da1dde 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js
@@ -414,10 +414,10 @@ add_task(async function test_save_and_load_dynamic_rules() {
browser.test.onMessage.addListener(async (msg, ...args) => {
switch (msg) {
case "assertGetDynamicRules": {
- const [{ expectedRules }] = args;
+ const [{ expectedRules, filter }] = args;
browser.test.assertDeepEq(
expectedRules,
- await dnr.getDynamicRules(),
+ await dnr.getDynamicRules(filter),
"getDynamicRules() resolves to the expected dynamic rules"
);
break;
@@ -503,6 +503,35 @@ add_task(async function test_save_and_load_dynamic_rules() {
expectedRules: getSchemaNormalizedRules(extension, rules),
});
+ info("Verify getDynamicRules with some filters");
+ // An empty list of rule IDs should return no rule.
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: [],
+ filter: { ruleIds: [] },
+ });
+ // Non-existent rule ID.
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: [],
+ filter: { ruleIds: [456] },
+ });
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, [rules[0]]),
+ filter: { ruleIds: [rules[0].id] },
+ });
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, [rules[1]]),
+ filter: { ruleIds: [rules[1].id] },
+ });
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, rules),
+ filter: { ruleIds: rules.map(rule => rule.id) },
+ });
+ // When `ruleIds` isn't defined, we return all the rules.
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, rules),
+ filter: {},
+ });
+
const extUUID = extension.uuid;
const dnrStore = ExtensionDNRStore._getStoreForTesting();
await dnrStore._savePromises.get(extUUID);
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_regexFilter_limits.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_regexFilter_limits.js
index 443f69c2d1..69635e4c80 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_regexFilter_limits.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_regexFilter_limits.js
@@ -26,33 +26,6 @@ add_setup(async () => {
await ExtensionTestUtils.startAddonManager();
});
-const _origDescs = {};
-function restoreDefaultDnrLimit(key) {
- info(`Restoring original value of ExtensionDNRLimits.${key}`);
- Object.defineProperty(ExtensionDNRLimits, key, _origDescs[key]);
-}
-function overrideDefaultDnrLimit(key, value) {
- // Until DNR limits can be customized through prefs (bug 1803370), we need to
- // overwrite the internals here in the parent process. That is sufficient to
- // control the limits. Notably, this does NOT affect the values of the
- // constants exposed through the declarativeNetRequest keyspace, because
- // their values are directly read from the extension (child) process.
- if (!_origDescs[key]) {
- _origDescs[key] = Object.getOwnPropertyDescriptor(ExtensionDNRLimits, key);
- registerCleanupFunction(() => restoreDefaultDnrLimit(key));
- }
- Assert.ok(
- typeof value === "number" && Number.isInteger(value),
- `Setting ExtensionDNRLimits.${key} = ${value} (was: ${ExtensionDNRLimits[key]})`
- );
- Object.defineProperty(ExtensionDNRLimits, key, {
- configurable: true,
- writable: true,
- enumerable: true,
- value,
- });
-}
-
// Create an extension composed of the given test cases, and start or reload
// the extension before each test case.
//
@@ -302,7 +275,10 @@ add_task(async function session_and_dynamic_regexFilter_limit() {
setup() {
// Artificially decrease the max number of allowed regexFilter rules,
// so that whatever that was stored on disk is no longer within quota.
- overrideDefaultDnrLimit("MAX_NUMBER_OF_REGEX_RULES", 1);
+ Services.prefs.setIntPref(
+ "extensions.dnr.max_number_of_regex_rules",
+ 1
+ );
},
backgroundFn: testPart3_too_many_regexFilters_stored_after_lowering_quota,
checkConsoleMessages: expectError,
@@ -311,7 +287,9 @@ add_task(async function session_and_dynamic_regexFilter_limit() {
name: "testPart4_reload_after_quota_back",
setup() {
// Restore the original quota after it was lowered in testPart3.
- restoreDefaultDnrLimit("MAX_NUMBER_OF_REGEX_RULES");
+ Services.prefs.clearUserPref(
+ "extensions.dnr.max_number_of_regex_rules"
+ );
},
backgroundFn: testPart4_reload_after_quota_back,
checkConsoleMessages: noErrors,
@@ -525,8 +503,8 @@ add_task(async function static_regexFilter_limit() {
{
name: "testPart5_after_doubling_quota",
setup() {
- overrideDefaultDnrLimit(
- "MAX_NUMBER_OF_REGEX_RULES",
+ Services.prefs.setIntPref(
+ "extensions.dnr.max_number_of_regex_rules",
2 * MAX_NUMBER_OF_REGEX_RULES
);
},
@@ -537,7 +515,9 @@ add_task(async function static_regexFilter_limit() {
name: "testPart6_after_restoring_original_quota_half",
setup() {
// Restore the original quota after it was raised in testPart5.
- restoreDefaultDnrLimit("MAX_NUMBER_OF_REGEX_RULES");
+ Services.prefs.clearUserPref(
+ "extensions.dnr.max_number_of_regex_rules"
+ );
},
backgroundFn: testPart6_after_restoring_original_quota_half,
checkConsoleMessages: (n, m) =>
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_session_rules.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_session_rules.js
index 5f0b0d72a2..6ac80a4ee7 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_session_rules.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_session_rules.js
@@ -218,6 +218,39 @@ add_task(async function register_and_retrieve_session_rules() {
"getSessionRules() returns all registered session rules"
);
+ browser.test.assertDeepEq(
+ [],
+ await dnr.getSessionRules({ ruleIds: [] }),
+ "getSessionRules() returns no rule because ruleIds is an empty array"
+ );
+ browser.test.assertDeepEq(
+ [],
+ await dnr.getSessionRules({ ruleIds: [1234567890] }),
+ "getSessionRules() returns no rule because rule ID is non-existent"
+ );
+ browser.test.assertDeepEq(
+ [RULE_4321_OUT],
+ await dnr.getSessionRules({ ruleIds: [4321] }),
+ "getSessionRules() returns a rule"
+ );
+ browser.test.assertDeepEq(
+ [RULE_1234_OUT],
+ await dnr.getSessionRules({ ruleIds: [1234] }),
+ "getSessionRules() returns a rule"
+ );
+ // The order is the same as the original input above, not the order of
+ // the IDs in `ruleIds`.
+ browser.test.assertDeepEq(
+ [RULE_4321_OUT, RULE_1234_OUT],
+ await dnr.getSessionRules({ ruleIds: [1234, 4321] }),
+ "getSessionRules() returns two rules"
+ );
+ browser.test.assertDeepEq(
+ [RULE_4321_OUT, RULE_1234_OUT],
+ await dnr.getSessionRules({}),
+ "getSessionRules() returns all the rules because there is no ruleIds"
+ );
+
await browser.test.assertRejects(
dnr.updateSessionRules({
addRules: [RULE_9001_IN, RULE_1234_IN],
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js
index 4d20bd330e..7d5befef2e 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js
@@ -1269,13 +1269,13 @@ add_task(async function test_static_rulesets_limits() {
},
{
message:
- /declarative_net_request: Enabled static rulesets are exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS limit .* "ruleset_10"/,
+ /declarative_net_request: Enabled static rulesets are exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS limit .* "ruleset_20"/,
},
// Error reported on the browser console as part of loading enabled rulesets)
// on enabled rulesets being ignored because exceeding the limit.
{
message:
- /Ignoring enabled static ruleset exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS .* "ruleset_10"/,
+ /Ignoring enabled static ruleset exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS .* "ruleset_20"/,
},
],
});
@@ -1285,7 +1285,7 @@ add_task(async function test_static_rulesets_limits() {
);
extension.sendMessage("updateEnabledRulesets", {
disableRulesetIds: ["ruleset_0"],
- enableRulesetIds: ["ruleset_10", "ruleset_11"],
+ enableRulesetIds: ["ruleset_20", "ruleset_21"],
});
await Assert.rejects(
@@ -1313,19 +1313,19 @@ add_task(async function test_static_rulesets_limits() {
"updateEnabledRulesets",
{
disableRulesetIds: ["ruleset_0"],
- enableRulesetIds: ["ruleset_10"],
+ enableRulesetIds: ["ruleset_20"],
},
{
- disableRulesetIds: ["ruleset_10"],
- enableRulesetIds: ["ruleset_11"],
+ disableRulesetIds: ["ruleset_20"],
+ enableRulesetIds: ["ruleset_21"],
}
);
await extension.awaitMessage("updateEnabledRulesets:done");
- // Expect ruleset_0 disabled, ruleset_10 to be enabled but then disabled by the
- // second update queued after the first one, and ruleset_11 to be enabled.
+ // Expect ruleset_0 disabled, ruleset_20 to be enabled but then disabled by the
+ // second update queued after the first one, and ruleset_21 to be enabled.
delete expectedEnabledRulesets.ruleset_0;
- expectedEnabledRulesets.ruleset_11 = getSchemaNormalizedRules(
+ expectedEnabledRulesets.ruleset_21 = getSchemaNormalizedRules(
extension,
rules
);
@@ -1356,10 +1356,10 @@ add_task(async function test_static_rulesets_limits() {
await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
extension.sendMessage("updateEnabledRulesets", {
- disableRulesetIds: ["ruleset_11"],
+ disableRulesetIds: ["ruleset_21"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
- delete expectedEnabledRulesets.ruleset_11;
+ delete expectedEnabledRulesets.ruleset_21;
await assertDNRGetEnabledRulesets(
extension,
Array.from(Object.keys(expectedEnabledRulesets))
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_urlencoded.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_urlencoded.js
index ae40faf909..8532ce4a99 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_urlencoded.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_urlencoded.js
@@ -83,7 +83,7 @@ add_task(async function test_decoded_filename_download() {
const FILE_NAME_DECODED_2 = "file\u{0001F6B2}encoded.txt";
const FILE_NAME_ENCODED_URL_2 = BASE + "/" + FILE_NAME_ENCODED_2;
const FILE_NAME_ENCODED_3 = "file%X%20encode.txt";
- const FILE_NAME_DECODED_3 = "file%X encode.txt";
+ const FILE_NAME_DECODED_3 = "file_X encode.txt";
const FILE_NAME_ENCODED_URL_3 = BASE + "/" + FILE_NAME_ENCODED_3;
const FILE_NAME_ENCODED_4 = "file%E3%80%82encode.txt";
const FILE_NAME_DECODED_4 = "file\u3002encode.txt";
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
index 4330e1b681..6c2c0f65f9 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
@@ -27,19 +27,48 @@ add_task(async function test_manifest_incognito() {
"Should have the expected incognito string"
);
+ ExtensionTestUtils.failOnSchemaWarnings(false);
normalized = await ExtensionTestUtils.normalizeManifest({
- incognito: "split",
+ incognito: "whatisthis",
});
+ ExtensionTestUtils.failOnSchemaWarnings(true);
equal(
normalized.error,
- 'Error processing incognito: Invalid enumeration value "split"',
+ 'Error processing incognito: Invalid enumeration value "whatisthis"',
"Should have an error"
);
Assert.deepEqual(normalized.errors, [], "Should not have a warning");
+
+ // Sanity check: Default value of "incognito" is "spanning".
+ normalized = await ExtensionTestUtils.normalizeManifest({});
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ equal(
+ normalized.value.incognito,
+ "spanning",
+ "Should have the expected default value for incognito when unspecified"
+ );
+});
+
+add_task(async function test_manifest_incognito_split_fallback_not_allowed() {
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ incognito: "split",
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ equal(normalized.error, undefined, "Should not have an error");
+ Assert.deepEqual(
+ normalized.errors,
+ [
+ `Warning processing incognito: incognito "split" is unsupported. Falling back to incognito "not_allowed".`,
+ ],
+ "Should have a warning"
+ );
equal(
- normalized.value,
- undefined,
- "Invalid incognito string should be undefined"
+ normalized.value.incognito,
+ "not_allowed",
+ "incognito:split should fall back to not_allowed by default"
);
});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js
index ab3f20f12e..925f8f9dfe 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js
@@ -41,7 +41,7 @@ async function getManifestPermissions(extensionData) {
ExtensionTestUtils.failOnSchemaWarnings(false);
await extension.loadManifest();
ExtensionTestUtils.failOnSchemaWarnings(true);
- let result = extension.manifestPermissions;
+ let result = extension.getRequiredPermissions();
if (extension.manifest.manifest_version >= 3) {
// In MV3, host permissions are optional by default.
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
index ae6ce3d27e..a0bebc3f77 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -10,6 +10,11 @@ const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
+const WITH_INSTALL_PROMPT = [
+ ["extensions.originControls.grantByDefault", true],
+];
+const NO_INSTALL_PROMPT = [["extensions.originControls.grantByDefault", false]];
+
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
// ExtensionParent.sys.mjs is being imported lazily because when it is imported Services.appinfo will be
@@ -427,12 +432,24 @@ add_task(function test_normal_mv2() {
});
});
+add_task(function test_normal_mv3_noInstallPrompt() {
+ return runWithPrefs(NO_INSTALL_PROMPT, () =>
+ test_permissions({
+ manifest_version: 3,
+ useAddonManager: "permanent",
+ expectAllGranted: false,
+ })
+ );
+});
+
add_task(function test_normal_mv3() {
- return test_permissions({
- manifest_version: 3,
- useAddonManager: "permanent",
- expectAllGranted: false,
- });
+ return runWithPrefs(WITH_INSTALL_PROMPT, () =>
+ test_permissions({
+ manifest_version: 3,
+ useAddonManager: "permanent",
+ expectAllGranted: true,
+ })
+ );
});
add_task(function test_granted_for_temporary_mv3() {
@@ -444,28 +461,30 @@ add_task(function test_granted_for_temporary_mv3() {
});
});
-add_task(async function test_granted_only_for_privileged_mv3() {
- try {
- // For permanent non-privileged, granted_host_permissions does nothing.
- await test_permissions({
- manifest_version: 3,
- granted_host_permissions: true,
- useAddonManager: "permanent",
- expectAllGranted: false,
- });
+add_task(function test_granted_only_for_privileged_mv3() {
+ return runWithPrefs(NO_INSTALL_PROMPT, async () => {
+ try {
+ // For permanent non-privileged, granted_host_permissions does nothing.
+ await test_permissions({
+ manifest_version: 3,
+ granted_host_permissions: true,
+ useAddonManager: "permanent",
+ expectAllGranted: false,
+ });
- // Make extensions loaded with addon manager privileged.
- AddonTestUtils.usePrivilegedSignatures = true;
+ // Make extensions loaded with addon manager privileged.
+ AddonTestUtils.usePrivilegedSignatures = true;
- await test_permissions({
- manifest_version: 3,
- granted_host_permissions: true,
- useAddonManager: "permanent",
- expectAllGranted: true,
- });
- } finally {
- AddonTestUtils.usePrivilegedSignatures = false;
- }
+ await test_permissions({
+ manifest_version: 3,
+ granted_host_permissions: true,
+ useAddonManager: "permanent",
+ expectAllGranted: true,
+ });
+ } finally {
+ AddonTestUtils.usePrivilegedSignatures = false;
+ }
+ });
});
add_task(async function test_startup() {
@@ -540,7 +559,7 @@ add_task(async function test_startup() {
});
// Test that we don't prompt for permissions an extension already has.
-async function test_alreadyGranted(manifest_version) {
+async function test_alreadyGranted({ manifest_version }) {
const REQUIRED_PERMISSIONS = ["geolocation"];
const REQUIRED_ORIGINS = [
"*://required-host.com/",
@@ -671,10 +690,17 @@ async function test_alreadyGranted(manifest_version) {
await extension.unload();
}
add_task(async function test_alreadyGranted_mv2() {
- return test_alreadyGranted(2);
+ return test_alreadyGranted({ manifest_version: 2 });
});
-add_task(async function test_alreadyGranted_mv3() {
- return test_alreadyGranted(3);
+add_task(function test_alreadyGranted_mv3_noInstallPrompt() {
+ return runWithPrefs(NO_INSTALL_PROMPT, () =>
+ test_alreadyGranted({ manifest_version: 3 })
+ );
+});
+add_task(function test_alreadyGranted_mv3() {
+ return runWithPrefs(WITH_INSTALL_PROMPT, () =>
+ test_alreadyGranted({ manifest_version: 3 })
+ );
});
// IMPORTANT: Do not change this list without review from a Web Extensions peer!
@@ -779,7 +805,10 @@ add_task(async function test_optional_all_urls() {
});
// Check when content_script match patterns are treated as optional origins.
-async function test_content_script_is_optional(manifest_version) {
+async function test_content_script_is_optional({
+ manifest_version,
+ expectGranted,
+}) {
function background() {
browser.test.onMessage.addListener(async (msg, arg) => {
if (msg == "request") {
@@ -816,7 +845,11 @@ async function test_content_script_is_optional(manifest_version) {
extension.sendMessage("getAll");
let initial = await extension.awaitMessage("granted");
- deepEqual(initial.origins, [], "Nothing granted on install.");
+ if (manifest_version < 3 || !expectGranted) {
+ deepEqual(initial.origins, [], "Nothing granted on install.");
+ } else {
+ deepEqual(initial.origins, [CS_ORIGIN], "CS origin granted on install.");
+ }
await withHandlingUserInput(extension, async () => {
extension.sendMessage("request", {
@@ -845,11 +878,32 @@ async function test_content_script_is_optional(manifest_version) {
await extension.unload();
}
-add_task(() => test_content_script_is_optional(2));
-add_task(() => test_content_script_is_optional(3));
+
+add_task(async function test_content_script_is_optional_mv2() {
+ await test_content_script_is_optional({ manifest_version: 2 });
+});
+add_task(function test_content_script_is_optional_mv3_noInstallPrompt() {
+ return runWithPrefs(NO_INSTALL_PROMPT, () =>
+ test_content_script_is_optional({
+ manifest_version: 3,
+ expectGranted: false,
+ })
+ );
+});
+add_task(function test_content_script_is_optional_mv3() {
+ return runWithPrefs(WITH_INSTALL_PROMPT, () =>
+ test_content_script_is_optional({
+ manifest_version: 3,
+ expectGranted: true,
+ })
+ );
+});
// Check that optional permissions are not included in update prompts
-async function test_permissions_prompt(manifest_version) {
+async function test_permissions_prompt({
+ manifest_version,
+ expectInitialGranted,
+}) {
function background() {
browser.test.onMessage.addListener(async (msg, arg) => {
if (msg == "request") {
@@ -896,7 +950,7 @@ async function test_permissions_prompt(manifest_version) {
equal(result, true, "request() for optional permissions succeeded");
});
- if (manifest_version >= 3) {
+ if (!expectInitialGranted) {
await withHandlingUserInput(extension, async () => {
extension.sendMessage("request", {
origins: ["https://test1.example.com/*"],
@@ -964,10 +1018,26 @@ async function test_permissions_prompt(manifest_version) {
await extension.unload();
}
add_task(async function test_permissions_prompt_mv2() {
- return test_permissions_prompt(2);
+ return test_permissions_prompt({
+ manifest_version: 2,
+ expectInitialGranted: true,
+ });
+});
+add_task(function test_permissions_prompt_mv3_noInstallPrompt() {
+ return runWithPrefs(NO_INSTALL_PROMPT, () =>
+ test_permissions_prompt({
+ manifest_version: 3,
+ expectInitialGranted: false,
+ })
+ );
});
add_task(async function test_permissions_prompt_mv3() {
- return test_permissions_prompt(3);
+ return runWithPrefs(WITH_INSTALL_PROMPT, () =>
+ test_permissions_prompt({
+ manifest_version: 3,
+ expectInitialGranted: true,
+ })
+ );
});
// Check that internal permissions can not be set and are not returned by the API.
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getContexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getContexts.js
new file mode 100644
index 0000000000..efef355f74
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getContexts.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_runtime_getContexts() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg === "runtime.getContexts") {
+ try {
+ const filter = args[0];
+ if (!filter) {
+ // Expected to be rejected.
+ await browser.runtime.getContexts();
+ } else {
+ // Expected to be resolved.
+ const result = await browser.runtime.getContexts(filter);
+ browser.test.sendMessage(`${msg}:result`, result);
+ }
+ } catch (err) {
+ browser.test.log(`runtime.getContexts error: ${err}\n`);
+ browser.test.sendMessage(`${msg}:error`, String(err));
+ }
+ }
+ });
+ browser.test.sendMessage("bgpage:loaded");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ },
+ background,
+ files: {
+ "tab.html": `<!DOCTYPE html><html></html>`,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage:loaded");
+
+ const documentOrigin = extension.extension.baseURI.spec.slice(0, -1);
+ const tabDocumentUrl = extension.extension.baseURI.resolve("tab.html");
+ const bgDocumentUrl = extension.extension.baseURI.resolve(
+ "_generated_background_page.html"
+ );
+
+ let expectedBackground = {
+ contextType: "BACKGROUND",
+ documentOrigin,
+ documentUrl: bgDocumentUrl,
+ incognito: false,
+ frameId: 0,
+ tabId: -1,
+ windowId: -1,
+ };
+
+ let expectedTab = {
+ contextType: "TAB",
+ documentOrigin,
+ documentUrl: `${tabDocumentUrl}?fistOpenedTab=true`,
+ incognito: false,
+ frameId: 0,
+ // tabId and windowId are expected to be -1 in xpcshell test
+ // (these are also covered by browser_ext_getContexts.js).
+ tabId: -1,
+ windowId: -1,
+ };
+
+ const assertValidContextId = contextId => {
+ Assert.equal(
+ typeof contextId,
+ "string",
+ "contextId should be set to a string"
+ );
+ Assert.notEqual(
+ contextId.length,
+ 0,
+ "contextId should be set to a non-zero length string"
+ );
+ };
+ const assertGetContextsResult = (
+ actual,
+ expected,
+ msg,
+ { assertContextId = false } = {}
+ ) => {
+ const actualCopy = assertContextId ? actual : actual.map(it => ({ ...it }));
+ if (!assertContextId) {
+ actualCopy.forEach(it => delete it.contextId);
+ }
+ Assert.deepEqual(actualCopy, expected, msg);
+ };
+
+ info(
+ "Test runtime.getContexts rejects when called without any filter parameter"
+ );
+ extension.sendMessage("runtime.getContexts", undefined);
+ let resError = await extension.awaitMessage("runtime.getContexts:error");
+ Assert.equal(
+ resError,
+ "Error: Incorrect argument types for runtime.getContexts.",
+ "Got the expected error message"
+ );
+
+ info(
+ "Test runtime.getContext resolved when called with an empty filter parameter"
+ );
+
+ extension.sendMessage("runtime.getContexts", {});
+ let res = await extension.awaitMessage("runtime.getContexts:result");
+
+ assertGetContextsResult(
+ res,
+ [expectedBackground],
+ "Got the expected properties for the background context"
+ );
+
+ let actualBackground = res[0];
+ assertValidContextId(actualBackground.contextId);
+
+ const page = await ExtensionTestUtils.loadContentPage(
+ `${tabDocumentUrl}?fistOpenedTab=true`
+ );
+
+ res = await page.spawn([], () =>
+ this.content.wrappedJSObject.browser.runtime.getContexts({})
+ );
+
+ const bgItem = res.find(it => it.contextType === "BACKGROUND");
+ const tabItem = res.find(it => it.contextType === "TAB");
+
+ assertValidContextId(tabItem.contextId);
+
+ assertGetContextsResult(
+ res,
+ [expectedBackground, expectedTab],
+ "Got expected properties for backgrond and tab contexts"
+ );
+ assertGetContextsResult(
+ [bgItem],
+ [actualBackground],
+ "Expect the expected properties for the background context (included same contextId)",
+ { assertContextId: true }
+ );
+
+ info("Test runtime.getContexts with a contextType filter");
+ res = await page.spawn([], () =>
+ this.content.wrappedJSObject.browser.runtime.getContexts({
+ contextTypes: ["BACKGROUND"],
+ })
+ );
+ assertGetContextsResult(
+ res,
+ [actualBackground],
+ "Expect only the backgrund context to be included in the results",
+ { assertContextId: true }
+ );
+
+ info("Test runtime.ContextType enum");
+ const contextTypeEnum = await page.spawn([], () => {
+ return this.content.wrappedJSObject.browser.runtime.ContextType;
+ });
+
+ const expectedTypesMap = ["BACKGROUND", "POPUP", "SIDE_PANEL", "TAB"].reduce(
+ (acc, item) => {
+ acc[item] = item;
+ return acc;
+ },
+ {}
+ );
+
+ Assert.deepEqual(
+ contextTypeEnum,
+ expectedTypesMap,
+ "Got the expected values in the ContextType enum"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_service_worker_messaging.js b/toolkit/components/extensions/test/xpcshell/test_ext_service_worker_messaging.js
new file mode 100644
index 0000000000..b064da259b
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_service_worker_messaging.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_runtime_sendMessage() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ browser_specific_settings: {
+ gecko: { id: "@test-messaging" },
+ },
+ },
+ files: {
+ "sw.js": async function () {
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("send", msg, "expected correct message");
+ browser.runtime.sendMessage("hello-from-sw");
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ "page.html": '<!DOCTYPE html><script src="page.js"></script>',
+ "page.js"() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(
+ "hello-from-sw",
+ msg,
+ "expected message from service worker"
+ );
+
+ const { contextId, ...otherProps } = sender;
+ browser.test.assertTrue(!!contextId, "expected a truthy contextId");
+ browser.test.assertDeepEq(
+ {
+ envType: "addon_child",
+ id: "@test-messaging",
+ origin: self.origin,
+ url: browser.runtime.getURL("sw.js"),
+ },
+ otherProps,
+ "expected correct sender props"
+ );
+
+ browser.test.sendMessage("page-done");
+ });
+
+ browser.test.sendMessage("page-ready");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ const page = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/page.html`,
+ { extension }
+ );
+ await extension.awaitMessage("page-ready");
+
+ extension.sendMessage("send");
+ await extension.awaitMessage("page-done");
+
+ await page.close();
+ await extension.unload();
+});
+
+add_task(async function test_runtime_connect() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ browser_specific_settings: {
+ gecko: { id: "@test-messaging" },
+ },
+ },
+ files: {
+ "sw.js": async function () {
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("connect", msg, "expected correct message");
+ browser.runtime.connect();
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ "page.html": '<!DOCTYPE html><script src="page.js"></script>',
+ "page.js"() {
+ browser.runtime.onConnect.addListener(port => {
+ const { contextId, ...otherProps } = port.sender;
+ browser.test.assertTrue(!!contextId, "expected a truthy contextId");
+ browser.test.assertDeepEq(
+ {
+ envType: "addon_child",
+ id: "@test-messaging",
+ origin: self.origin,
+ url: browser.runtime.getURL("sw.js"),
+ },
+ otherProps,
+ "expected correct sender props"
+ );
+
+ browser.test.sendMessage("page-done");
+ });
+
+ browser.test.sendMessage("page-ready");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ const page = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/page.html`,
+ { extension }
+ );
+ await extension.awaitMessage("page-ready");
+
+ extension.sendMessage("connect");
+ await extension.awaitMessage("page-done");
+
+ await page.close();
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js
index d0448b7b2e..4b98597b11 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js
@@ -15,55 +15,42 @@ const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
-const HISTOGRAM_JSON_IDS = [
- "WEBEXT_STORAGE_LOCAL_SET_MS",
- "WEBEXT_STORAGE_LOCAL_GET_MS",
-];
-const KEYED_HISTOGRAM_JSON_IDS = [
- "WEBEXT_STORAGE_LOCAL_SET_MS_BY_ADDONID",
- "WEBEXT_STORAGE_LOCAL_GET_MS_BY_ADDONID",
-];
-
-const HISTOGRAM_IDB_IDS = [
+const HISTOGRAM_IDS = [
"WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
"WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
];
-const KEYED_HISTOGRAM_IDB_IDS = [
+const KEYED_HISTOGRAM_IDS = [
"WEBEXT_STORAGE_LOCAL_IDB_SET_MS_BY_ADDONID",
"WEBEXT_STORAGE_LOCAL_IDB_GET_MS_BY_ADDONID",
];
-const HISTOGRAM_IDS = [].concat(HISTOGRAM_JSON_IDS, HISTOGRAM_IDB_IDS);
-const KEYED_HISTOGRAM_IDS = [].concat(
- KEYED_HISTOGRAM_JSON_IDS,
- KEYED_HISTOGRAM_IDB_IDS
-);
-
const EXTENSION_ID1 = "@test-extension1";
const EXTENSION_ID2 = "@test-extension2";
async function test_telemetry_background() {
const { GleanTimingDistribution } = globalThis;
+
+ // NOTE: we do not collect telemetry for the legacy JSON backend anymore
+ // and so if the IDB backend is not enabled we expect the related telemetry
+ // histograms and timing distributions to be empty.
const expectedEmptyGleanMetrics = ExtensionStorageIDB.isBackendEnabled
- ? ["storageLocalGetJson", "storageLocalSetJson"]
+ ? []
: ["storageLocalGetIdb", "storageLocalSetIdb"];
const expectedNonEmptyGleanMetrics = ExtensionStorageIDB.isBackendEnabled
? ["storageLocalGetIdb", "storageLocalSetIdb"]
- : ["storageLocalGetJson", "storageLocalSetJson"];
-
+ : [];
const expectedEmptyHistograms = ExtensionStorageIDB.isBackendEnabled
- ? HISTOGRAM_JSON_IDS
- : HISTOGRAM_IDB_IDS;
+ ? []
+ : HISTOGRAM_IDS;
const expectedEmptyKeyedHistograms = ExtensionStorageIDB.isBackendEnabled
- ? KEYED_HISTOGRAM_JSON_IDS
- : KEYED_HISTOGRAM_IDB_IDS;
-
+ ? []
+ : KEYED_HISTOGRAM_IDS;
const expectedNonEmptyHistograms = ExtensionStorageIDB.isBackendEnabled
- ? HISTOGRAM_IDB_IDS
- : HISTOGRAM_JSON_IDS;
+ ? HISTOGRAM_IDS
+ : [];
const expectedNonEmptyKeyedHistograms = ExtensionStorageIDB.isBackendEnabled
- ? KEYED_HISTOGRAM_IDB_IDS
- : KEYED_HISTOGRAM_JSON_IDS;
+ ? KEYED_HISTOGRAM_IDS
+ : [];
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_eventPage_StreamFilter.js b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_eventPage_StreamFilter.js
index 23c29aa155..820e04956e 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_eventPage_StreamFilter.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_eventPage_StreamFilter.js
@@ -105,7 +105,11 @@ async function test_idletimeout_on_streamfilter({
).catch(err => {
// This request is expected to be aborted when cleared after the test is exiting,
// otherwise rethrow the error to trigger an explicit failure.
- if (/The operation was aborted/.test(err.message)) {
+ if (
+ /Content-Length header of network response exceeds response Body/.test(
+ err.message
+ )
+ ) {
info(`Test webRequest fetching "${testURL}" aborted`);
} else {
ok(
diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml b/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
index 6d47012eca..fd0b2d50d2 100644
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
@@ -432,6 +432,8 @@ skip-if = ["os == 'android' && debug"]
["test_ext_runtime_getBrowserInfo.js"]
+["test_ext_runtime_getContexts.js"]
+
["test_ext_runtime_getPlatformInfo.js"]
["test_ext_runtime_id.js"]
diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-serviceworker.toml b/toolkit/components/extensions/test/xpcshell/xpcshell-serviceworker.toml
index b300959970..83ca99bc2f 100644
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-serviceworker.toml
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-serviceworker.toml
@@ -49,3 +49,5 @@ run-sequentially = "very high failure rate in parallel"
["test_ext_scripting_contentScripts_file.js"]
["test_ext_scripting_updateContentScripts.js"]
+
+["test_ext_service_worker_messaging.js"]
diff --git a/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp b/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp
index 5ad9f2dfd8..3dd4303729 100644
--- a/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp
+++ b/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp
@@ -299,7 +299,7 @@ NS_IMETHODIMP ExtensionEventListener::CallListener(
RefPtr<ExtensionListenerCallWorkerRunnable> runnable =
new ExtensionListenerCallWorkerRunnable(this, std::move(argsHolder),
aCallOptions, retPromise);
- runnable->Dispatch();
+ runnable->Dispatch(GetWorkerPrivate());
retPromise.forget(aPromiseResult);
return NS_OK;
@@ -332,7 +332,6 @@ bool ExtensionListenerCallWorkerRunnable::WorkerRun(
JSContext* aCx, dom::WorkerPrivate* aWorkerPrivate) {
MOZ_ASSERT(aWorkerPrivate);
aWorkerPrivate->AssertIsOnWorkerThread();
- MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate);
auto global = mListener->GetGlobalObject();
if (NS_WARN_IF(!global)) {
return true;
diff --git a/toolkit/components/extensions/webidl-api/ExtensionEventListener.h b/toolkit/components/extensions/webidl-api/ExtensionEventListener.h
index e986e4f58b..f11d3e7f6b 100644
--- a/toolkit/components/extensions/webidl-api/ExtensionEventListener.h
+++ b/toolkit/components/extensions/webidl-api/ExtensionEventListener.h
@@ -120,7 +120,8 @@ class ExtensionEventListener final : public mozIExtensionEventListener {
// A WorkerRunnable subclass used to call an ExtensionEventListener
// in the thread that owns the dom::Function wrapped by the
// ExtensionEventListener class.
-class ExtensionListenerCallWorkerRunnable final : public dom::WorkerRunnable {
+class ExtensionListenerCallWorkerRunnable final
+ : public dom::WorkerThreadRunnable {
friend class ExtensionListenerCallPromiseResultHandler;
public:
@@ -133,8 +134,7 @@ class ExtensionListenerCallWorkerRunnable final : public dom::WorkerRunnable {
UniquePtr<dom::StructuredCloneHolder> aArgsHolder,
ListenerCallOptions* aCallOptions,
RefPtr<dom::Promise> aPromiseRetval = nullptr)
- : WorkerRunnable(aExtensionEventListener->GetWorkerPrivate(),
- "ExtensionListenerCallWorkerRunnable", WorkerThread),
+ : WorkerThreadRunnable("ExtensionListenerCallWorkerRunnable"),
mListener(aExtensionEventListener),
mArgsHolder(std::move(aArgsHolder)),
mPromiseResult(std::move(aPromiseRetval)),