1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 * This file handles privileged extension page logic that runs in the
14 ChromeUtils.defineESModuleGetters(lazy, {
15 ExtensionChildDevToolsUtils:
16 "resource://gre/modules/ExtensionChildDevToolsUtils.sys.mjs",
17 Schemas: "resource://gre/modules/Schemas.sys.mjs",
20 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
21 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
23 import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
26 ExtensionActivityLogChild,
27 } from "resource://gre/modules/ExtensionChild.sys.mjs";
28 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
30 const { getInnerWindowID, promiseEvent } = ExtensionUtils;
32 const { BaseContext, CanOfAPIs, SchemaAPIManager, redefineGetter } =
35 const { ChildAPIManager, Messenger } = ExtensionChild;
37 const initializeBackgroundPage = context => {
38 // Override the `alert()` method inside background windows;
39 // we alias it to console.log().
40 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1203394
41 let alertDisplayedWarning = false;
42 const innerWindowID = getInnerWindowID(context.contentWindow);
44 /** @param {{ text, filename, lineNumber?, columnNumber? }} options */
45 function logWarningMessage({ text, filename, lineNumber, columnNumber }) {
46 let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
49 consoleMsg.initWithWindowID(
55 Ci.nsIScriptError.warningFlag,
59 Services.console.logMessage(consoleMsg);
62 function ignoredSuspendListener() {
64 text: "Background event page was not terminated on idle because a DevTools toolbox is attached to the extension.",
65 filename: context.contentWindow.location.href,
69 if (!context.extension.manifest.background.persistent) {
71 "background-script-suspend-ignored",
72 ignoredSuspendListener
76 context.extension.off(
77 "background-script-suspend-ignored",
78 ignoredSuspendListener
84 let alertOverwrite = text => {
85 const { filename, columnNumber, lineNumber } = Components.stack.caller;
87 if (!alertDisplayedWarning) {
88 context.childManager.callParentAsyncFunction(
89 "runtime.openBrowserConsole",
94 text: "alert() is not supported in background windows; please use console.log instead.",
100 alertDisplayedWarning = true;
103 logWarningMessage({ text, filename, lineNumber, columnNumber });
105 Cu.exportFunction(alertOverwrite, context.contentWindow, {
110 var apiManager = new (class extends SchemaAPIManager {
112 super("addon", lazy.Schemas);
113 this.initialized = false;
117 if (!this.initialized) {
118 this.initialized = true;
120 for (let { value } of Services.catMan.enumerateCategory(
121 CATEGORY_EXTENSION_SCRIPTS_ADDON
123 this.loadScript(value);
129 var devtoolsAPIManager = new (class extends SchemaAPIManager {
131 super("devtools", lazy.Schemas);
132 this.initialized = false;
136 if (!this.initialized) {
137 this.initialized = true;
139 for (let { value } of Services.catMan.enumerateCategory(
140 CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS
142 this.loadScript(value);
148 export function getContextChildManagerGetter(
150 ChildAPIManagerClass = ChildAPIManager
154 envType === "devtools_parent"
156 : this.extension.apiManager;
158 apiManager.lazyInit();
161 let can = new CanOfAPIs(this, apiManager, localApis);
163 let childManager = new ChildAPIManagerClass(
169 viewType: this.viewType,
171 incognito: this.incognito,
172 // Additional data a BaseContext subclass may optionally send
173 // as part of the CreateProxyContext request sent to the main process
174 // (e.g. WorkerContexChild implements this method to send the service
175 // worker descriptor id along with the details send by default here).
176 ...this.getCreateProxyContextData?.(),
180 this.callOnClose(childManager);
186 export class ExtensionBaseContextChild extends BaseContext {
188 * This ExtensionBaseContextChild represents an addon execution environment
189 * that is running in an addon or devtools child process.
191 * @param {BrowserExtensionContent} extension This context's owner.
192 * @param {object} params
193 * @param {string} params.envType One of "addon_child" or "devtools_child".
194 * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
195 * @param {string} params.viewType One of "background", "popup", "tab",
196 * "sidebar", "devtools_page" or "devtools_panel".
197 * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
198 * @param {nsIURI} [params.uri] The URI of the page.
200 constructor(extension, params) {
201 if (!params.envType) {
202 throw new Error("Missing envType");
205 super(params.envType, extension);
206 let { viewType = "tab", uri, contentWindow, tabId } = params;
207 this.viewType = viewType;
208 this.uri = uri || extension.baseURI;
210 this.setContentWindow(contentWindow);
211 this.browsingContextId = contentWindow.docShell.browsingContext.id;
213 if (viewType == "tab") {
214 Object.defineProperty(this, "tabId", {
221 lazy.Schemas.exportLazyGetter(contentWindow, "browser", () => {
222 return this.browserObj;
225 lazy.Schemas.exportLazyGetter(contentWindow, "chrome", () => {
226 // For MV3 and later, this is just an alias for browser.
227 if (extension.manifestVersion > 2) {
228 return this.browserObj;
230 // Chrome compat is only used with MV2
231 let chromeApiWrapper = Object.create(this.childManager);
232 chromeApiWrapper.isChromeCompat = true;
234 let chromeObj = Cu.createObjectIn(contentWindow);
235 chromeApiWrapper.inject(chromeObj);
241 const browserObj = Cu.createObjectIn(this.contentWindow);
242 this.childManager.inject(browserObj);
243 return redefineGetter(this, "browserObj", browserObj);
246 logActivity(type, name, data) {
247 ExtensionActivityLogChild.log(this, type, name, data);
251 return this.contentWindow;
255 return this.contentWindow.document.nodePrincipal;
259 // Will be overwritten in the constructor if necessary.
263 // Called when the extension shuts down.
265 if (this.contentWindow) {
266 this.contentWindow.close();
272 // This method is called when an extension page navigates away or
273 // its tab is closed.
275 // Note that without this guard, we end up running unload code
276 // multiple times for tab pages closed by the "page-unload" handlers
286 return redefineGetter(this, "messenger", new Messenger(this));
289 /** @type {ReturnType<ReturnType<getContextChildManagerGetter>>} */
291 throw new Error("childManager getter must be overridden");
295 class ExtensionPageContextChild extends ExtensionBaseContextChild {
297 * This ExtensionPageContextChild represents a privileged addon
298 * execution environment that has full access to the WebExtensions
299 * APIs (provided that the correct permissions have been requested).
301 * This is the child side of the ExtensionPageContextParent class
302 * defined in ExtensionParent.sys.mjs.
304 * @param {BrowserExtensionContent} extension This context's owner.
305 * @param {object} params
306 * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
307 * @param {string} params.viewType One of "background", "popup", "sidebar" or "tab".
308 * "background", "sidebar" and "tab" are used by `browser.extension.getViews`.
309 * "popup" is only used internally to identify page action and browser
310 * action popups and options_ui pages.
311 * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
312 * @param {nsIURI} [params.uri] The URI of the page.
314 constructor(extension, params) {
315 super(extension, Object.assign(params, { envType: "addon_child" }));
317 if (this.viewType == "background") {
318 initializeBackgroundPage(this);
321 this.extension.views.add(this);
326 this.extension.views.delete(this);
330 const childManager = getContextChildManagerGetter({
331 envType: "addon_parent",
333 return redefineGetter(this, "childManager", childManager);
337 export class DevToolsContextChild extends ExtensionBaseContextChild {
339 * This DevToolsContextChild represents a devtools-related addon execution
340 * environment that has access to the devtools API namespace and to the same subset
341 * of APIs available in a content script execution environment.
343 * @param {BrowserExtensionContent} extension This context's owner.
344 * @param {object} params
345 * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
346 * @param {string} params.viewType One of "devtools_page" or "devtools_panel".
347 * @param {object} [params.devtoolsToolboxInfo] This devtools toolbox's information,
348 * used if viewType is "devtools_page" or "devtools_panel".
349 * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
350 * @param {nsIURI} [params.uri] The URI of the page.
352 constructor(extension, params) {
353 super(extension, Object.assign(params, { envType: "devtools_child" }));
355 this.devtoolsToolboxInfo = params.devtoolsToolboxInfo;
356 lazy.ExtensionChildDevToolsUtils.initThemeChangeObserver(
357 params.devtoolsToolboxInfo.themeName,
361 this.extension.devtoolsViews.add(this);
366 this.extension.devtoolsViews.delete(this);
370 const childManager = getContextChildManagerGetter({
371 envType: "devtools_parent",
373 return redefineGetter(this, "childManager", childManager);
377 export var ExtensionPageChild = {
380 // Map<innerWindowId, ExtensionPageContextChild>
381 extensionContexts: new Map(),
386 if (this.initialized) {
389 this.initialized = true;
391 Services.obs.addObserver(this, "inner-window-destroyed"); // eslint-ignore-line mozilla/balanced-listeners
394 observe(subject, topic) {
395 if (topic === "inner-window-destroyed") {
396 let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
398 this.destroyExtensionContext(windowId);
402 expectViewLoad(global, viewType) {
407 /** @param {{target: Window|any}} event */
409 event.target.location != "about:blank" &&
410 // Ignore DOMContentLoaded bubbled from child frames:
411 event.target.defaultView === global.content
413 let windowId = getInnerWindowID(global.content);
414 let context = this.extensionContexts.get(windowId);
415 // This initializes ChildAPIManager (and creation of ProxyContextParent)
416 // if they don't exist already at this point.
417 let childId = context?.childManager.id;
418 if (viewType === "background") {
419 global.sendAsyncMessage("Extension:BackgroundViewLoaded", { childId });
425 * Create a privileged context at initial-document-element-inserted.
427 * @param {BrowserExtensionContent} extension
428 * The extension for which the context should be created.
429 * @param {nsIDOMWindow} contentWindow The global of the page.
431 initExtensionContext(extension, contentWindow) {
434 if (!WebExtensionPolicy.isExtensionProcess) {
436 "Cannot create an extension page context in current process"
440 let windowId = getInnerWindowID(contentWindow);
441 let context = this.extensionContexts.get(windowId);
443 if (context.extension !== extension) {
445 "A different extension context already exists for this frame"
449 "An extension context was already initialized for this frame"
453 let uri = contentWindow.document.documentURIObject;
455 let mm = contentWindow.docShell.messageManager;
456 let data = mm.sendSyncMessage("Extension:GetFrameData")[0];
458 let policy = WebExtensionPolicy.getByHostname(uri.host);
459 // TODO bug 1749116: Handle this unexpected result, because data
460 // (viewType in particular) should never be void for extension documents.
461 Cu.reportError(`FrameData missing for ${policy?.id} page ${uri.spec}`);
463 let { viewType, tabId, devtoolsToolboxInfo } = data ?? {};
465 if (viewType && contentWindow.top === contentWindow) {
466 ExtensionPageChild.expectViewLoad(mm, viewType);
469 if (devtoolsToolboxInfo) {
470 context = new DevToolsContextChild(extension, {
478 context = new ExtensionPageContextChild(extension, {
486 this.extensionContexts.set(windowId, context);
490 * Close the ExtensionPageContextChild belonging to the given window, if any.
492 * @param {number} windowId The inner window ID of the destroyed context.
494 destroyExtensionContext(windowId) {
495 let context = this.extensionContexts.get(windowId);
498 this.extensionContexts.delete(windowId);
502 shutdownExtension(extensionId) {
503 for (let [windowId, context] of this.extensionContexts) {
504 if (context.extension.id == extensionId) {
506 this.extensionContexts.delete(windowId);