Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// This file expects tabTracker to be defined in the global scope (e.g.
// by ext-browser.js or ext-android.js).
/* global tabTracker */
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",
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"gRuntimeTimeout",
"extensions.webextensions.runtime.timeout",
5000
);
this.runtime = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// Despite not being part of PERSISTENT_EVENTS, the following events are
// still triggered (after waking up the background context if needed):
// - runtime.onConnect
// - runtime.onConnectExternal
// - runtime.onMessage
// - runtime.onMessageExternal
// - runtime.onUserScriptMessage
// - runtime.onUserScriptConnect
// For details, see bug 1852317 and test_ext_eventpage_messaging_wakeup.js.
onInstalled({ fire }) {
let { extension } = this;
let temporary = !!extension.addonData.temporarilyInstalled;
let listener = () => {
switch (extension.startupReason) {
case "APP_STARTUP":
if (AddonManagerPrivate.browserUpdated) {
fire.sync({ reason: "browser_update", temporary });
}
break;
case "ADDON_INSTALL":
fire.sync({ reason: "install", temporary });
break;
case "ADDON_UPGRADE":
fire.sync({
reason: "update",
previousVersion: extension.addonData.oldVersion,
temporary,
});
break;
}
};
extension.on("background-first-run", listener);
return {
unregister() {
extension.off("background-first-run", listener);
},
convert(_fire) {
fire = _fire;
},
};
},
onUpdateAvailable({ fire }) {
let { extension } = this;
let instanceID = extension.addonData.instanceID;
AddonManager.addUpgradeListener(instanceID, upgrade => {
extension.upgrade = upgrade;
let details = {
version: upgrade.version,
};
fire.sync(details);
});
return {
unregister() {
AddonManager.removeUpgradeListener(instanceID);
},
convert(_fire) {
fire = _fire;
},
};
},
onPerformanceWarning({ fire }) {
let { extension } = this;
let observer = subject => {
let report = subject.QueryInterface(Ci.nsIHangReport);
if (report?.addonId !== extension.id) {
return;
}
const performanceWarningEventDetails = {
category: "content_script",
severity: "high",
description:
"Slow extension content script caused a page hang, user was warned.",
};
let scriptBrowser = report.scriptBrowser;
let nativeTab =
scriptBrowser?.ownerGlobal.gBrowser?.getTabForBrowser(scriptBrowser);
if (nativeTab) {
performanceWarningEventDetails.tabId = tabTracker.getId(nativeTab);
}
fire.async(performanceWarningEventDetails);
};
Services.obs.addObserver(observer, "process-hang-report");
return {
unregister: () => {
Services.obs.removeObserver(observer, "process-hang-report");
},
convert(_fire) {
fire = _fire;
},
};
},
};
// 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
//
// 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({
context,
module: "runtime",
event: "onStartup",
register: fire => {
if (context.incognito || extension.startupReason != "APP_STARTUP") {
// This event should not fire if we are operating in a private profile.
return () => {};
}
let listener = () => {
return fire.sync();
};
extension.on("background-first-run", listener);
return () => {
extension.off("background-first-run", listener);
};
},
}).api(),
onInstalled: new EventManager({
context,
module: "runtime",
event: "onInstalled",
extensionApi: this,
}).api(),
onUpdateAvailable: new EventManager({
context,
module: "runtime",
event: "onUpdateAvailable",
extensionApi: this,
}).api(),
onSuspend: new EventManager({
context,
name: "runtime.onSuspend",
resetIdleOnEvent: false,
register: fire => {
let listener = async () => {
let timedOut = false;
async function promiseFire() {
try {
await fire.async();
} catch (e) {}
}
await Promise.race([
promiseFire(),
ExtensionUtils.promiseTimeout(gRuntimeTimeout).then(() => {
timedOut = true;
}),
]);
if (timedOut) {
Cu.reportError(
`runtime.onSuspend in ${extension.id} took too long`
);
}
};
extension.on("background-script-suspend", listener);
return () => {
extension.off("background-script-suspend", listener);
};
},
}).api(),
onSuspendCanceled: new EventManager({
context,
name: "runtime.onSuspendCanceled",
register: fire => {
let listener = () => {
fire.async();
};
extension.on("background-script-suspend-canceled", listener);
return () => {
extension.off("background-script-suspend-canceled", listener);
};
},
}).api(),
onPerformanceWarning: new EventManager({
context,
module: "runtime",
event: "onPerformanceWarning",
extensionApi: this,
}).api(),
reload: async () => {
if (extension.upgrade) {
// If there is a pending update, install it now.
extension.upgrade.install();
} else {
// Otherwise, reload the current extension.
let addon = await AddonManager.getAddonByID(extension.id);
addon.reload();
}
},
get lastError() {
// TODO(robwu): Figure out how to make sure that errors in the parent
// process are propagated to the child process.
// lastError should not be accessed from the parent.
return context.lastError;
},
getBrowserInfo: function () {
const { name, vendor, version, appBuildID } = Services.appinfo;
const info = { name, vendor, version, buildID: appBuildID };
return Promise.resolve(info);
},
getPlatformInfo: function () {
return Promise.resolve(ExtensionParent.PlatformInfo);
},
openOptionsPage: function () {
if (!extension.optionsPageProperties) {
return Promise.reject({ message: "No `options_ui` declared" });
}
// This expects openOptionsPage to be defined in the file using this,
// e.g. the browser/ version of ext-runtime.js
/* global openOptionsPage:false */
return openOptionsPage(extension).then(() => {});
},
setUninstallURL: function (url) {
if (url === null || url.length === 0) {
extension.uninstallURL = null;
return Promise.resolve();
}
let uri;
try {
uri = new URL(url);
} catch (e) {
return Promise.reject({
message: `Invalid URL: ${JSON.stringify(url)}`,
});
}
if (uri.protocol != "http:" && uri.protocol != "https:") {
return Promise.reject({
message: "url must have the scheme http or https",
});
}
extension.uninstallURL = url;
return Promise.resolve();
},
// This function is not exposed to the extension js code and it is only
// used by the alert function redefined into the background pages to be
// able to open the BrowserConsole from the main process.
openBrowserConsole() {
if (AppConstants.platform !== "android") {
DevToolsShim.openBrowserConsole();
}
},
async internalWakeupBackground() {
const { background } = extension.manifest;
if (
background &&
(background.page || background.scripts) &&
// Note: if background.service_worker is specified, it takes
// precedence over page/scripts, and persistentBackground is false.
!extension.persistentBackground
) {
await extension.wakeupBackground();
}
},
},
};
}
};