Bug 1885565 - Part 1: Add mozac_ic_avatar_circle_24 to ui-icons r=android-reviewers...
[gecko.git] / toolkit / components / extensions / ExtensionPageChild.sys.mjs
blob17c208572b21c49199b569e1cda5080c079cb76c
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/. */
7 /**
8  * This file handles privileged extension page logic that runs in the
9  * child process.
10  */
12 const lazy = {};
14 ChromeUtils.defineESModuleGetters(lazy, {
15   ExtensionChildDevToolsUtils:
16     "resource://gre/modules/ExtensionChildDevToolsUtils.sys.mjs",
17   Schemas: "resource://gre/modules/Schemas.sys.mjs",
18 });
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";
24 import {
25   ExtensionChild,
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 } =
33   ExtensionCommon;
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(
47       Ci.nsIScriptError
48     );
49     consoleMsg.initWithWindowID(
50       text,
51       filename,
52       null,
53       lineNumber,
54       columnNumber,
55       Ci.nsIScriptError.warningFlag,
56       "webextension",
57       innerWindowID
58     );
59     Services.console.logMessage(consoleMsg);
60   }
62   function ignoredSuspendListener() {
63     logWarningMessage({
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,
66     });
67   }
69   if (!context.extension.manifest.background.persistent) {
70     context.extension.on(
71       "background-script-suspend-ignored",
72       ignoredSuspendListener
73     );
74     context.callOnClose({
75       close: () => {
76         context.extension.off(
77           "background-script-suspend-ignored",
78           ignoredSuspendListener
79         );
80       },
81     });
82   }
84   let alertOverwrite = text => {
85     const { filename, columnNumber, lineNumber } = Components.stack.caller;
87     if (!alertDisplayedWarning) {
88       context.childManager.callParentAsyncFunction(
89         "runtime.openBrowserConsole",
90         []
91       );
93       logWarningMessage({
94         text: "alert() is not supported in background windows; please use console.log instead.",
95         filename,
96         lineNumber,
97         columnNumber,
98       });
100       alertDisplayedWarning = true;
101     }
103     logWarningMessage({ text, filename, lineNumber, columnNumber });
104   };
105   Cu.exportFunction(alertOverwrite, context.contentWindow, {
106     defineAs: "alert",
107   });
110 var apiManager = new (class extends SchemaAPIManager {
111   constructor() {
112     super("addon", lazy.Schemas);
113     this.initialized = false;
114   }
116   lazyInit() {
117     if (!this.initialized) {
118       this.initialized = true;
119       this.initGlobal();
120       for (let { value } of Services.catMan.enumerateCategory(
121         CATEGORY_EXTENSION_SCRIPTS_ADDON
122       )) {
123         this.loadScript(value);
124       }
125     }
126   }
127 })();
129 var devtoolsAPIManager = new (class extends SchemaAPIManager {
130   constructor() {
131     super("devtools", lazy.Schemas);
132     this.initialized = false;
133   }
135   lazyInit() {
136     if (!this.initialized) {
137       this.initialized = true;
138       this.initGlobal();
139       for (let { value } of Services.catMan.enumerateCategory(
140         CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS
141       )) {
142         this.loadScript(value);
143       }
144     }
145   }
146 })();
148 export function getContextChildManagerGetter(
149   { envType },
150   ChildAPIManagerClass = ChildAPIManager
151 ) {
152   return function () {
153     let apiManager =
154       envType === "devtools_parent"
155         ? devtoolsAPIManager
156         : this.extension.apiManager;
158     apiManager.lazyInit();
160     let localApis = {};
161     let can = new CanOfAPIs(this, apiManager, localApis);
163     let childManager = new ChildAPIManagerClass(
164       this,
165       this.messageManager,
166       can,
167       {
168         envType,
169         viewType: this.viewType,
170         url: this.uri.spec,
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?.(),
177       }
178     );
180     this.callOnClose(childManager);
182     return childManager;
183   };
186 export class ExtensionBaseContextChild extends BaseContext {
187   /**
188    * This ExtensionBaseContextChild represents an addon execution environment
189    * that is running in an addon or devtools child process.
190    *
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.
199    */
200   constructor(extension, params) {
201     if (!params.envType) {
202       throw new Error("Missing envType");
203     }
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", {
215         value: tabId,
216         enumerable: true,
217         configurable: true,
218       });
219     }
221     lazy.Schemas.exportLazyGetter(contentWindow, "browser", () => {
222       return this.browserObj;
223     });
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;
229       }
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);
236       return chromeObj;
237     });
238   }
240   get browserObj() {
241     const browserObj = Cu.createObjectIn(this.contentWindow);
242     this.childManager.inject(browserObj);
243     return redefineGetter(this, "browserObj", browserObj);
244   }
246   logActivity(type, name, data) {
247     ExtensionActivityLogChild.log(this, type, name, data);
248   }
250   get cloneScope() {
251     return this.contentWindow;
252   }
254   get principal() {
255     return this.contentWindow.document.nodePrincipal;
256   }
258   get tabId() {
259     // Will be overwritten in the constructor if necessary.
260     return -1;
261   }
263   // Called when the extension shuts down.
264   shutdown() {
265     if (this.contentWindow) {
266       this.contentWindow.close();
267     }
269     this.unload();
270   }
272   // This method is called when an extension page navigates away or
273   // its tab is closed.
274   unload() {
275     // Note that without this guard, we end up running unload code
276     // multiple times for tab pages closed by the "page-unload" handlers
277     // triggered below.
278     if (this.unloaded) {
279       return;
280     }
282     super.unload();
283   }
285   get messenger() {
286     return redefineGetter(this, "messenger", new Messenger(this));
287   }
289   /** @type {ReturnType<ReturnType<getContextChildManagerGetter>>} */
290   get childManager() {
291     throw new Error("childManager getter must be overridden");
292   }
295 class ExtensionPageContextChild extends ExtensionBaseContextChild {
296   /**
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).
300    *
301    * This is the child side of the ExtensionPageContextParent class
302    * defined in ExtensionParent.sys.mjs.
303    *
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.
313    */
314   constructor(extension, params) {
315     super(extension, Object.assign(params, { envType: "addon_child" }));
317     if (this.viewType == "background") {
318       initializeBackgroundPage(this);
319     }
321     this.extension.views.add(this);
322   }
324   unload() {
325     super.unload();
326     this.extension.views.delete(this);
327   }
329   get childManager() {
330     const childManager = getContextChildManagerGetter({
331       envType: "addon_parent",
332     }).call(this);
333     return redefineGetter(this, "childManager", childManager);
334   }
337 export class DevToolsContextChild extends ExtensionBaseContextChild {
338   /**
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.
342    *
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.
351    */
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,
358       this
359     );
361     this.extension.devtoolsViews.add(this);
362   }
364   unload() {
365     super.unload();
366     this.extension.devtoolsViews.delete(this);
367   }
369   get childManager() {
370     const childManager = getContextChildManagerGetter({
371       envType: "devtools_parent",
372     }).call(this);
373     return redefineGetter(this, "childManager", childManager);
374   }
377 export var ExtensionPageChild = {
378   initialized: false,
380   // Map<innerWindowId, ExtensionPageContextChild>
381   extensionContexts: new Map(),
383   apiManager,
385   _init() {
386     if (this.initialized) {
387       return;
388     }
389     this.initialized = true;
391     Services.obs.addObserver(this, "inner-window-destroyed"); // eslint-ignore-line mozilla/balanced-listeners
392   },
394   observe(subject, topic) {
395     if (topic === "inner-window-destroyed") {
396       let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
398       this.destroyExtensionContext(windowId);
399     }
400   },
402   expectViewLoad(global, viewType) {
403     promiseEvent(
404       global,
405       "DOMContentLoaded",
406       true,
407       /** @param {{target: Window|any}} event */
408       event =>
409         event.target.location != "about:blank" &&
410         // Ignore DOMContentLoaded bubbled from child frames:
411         event.target.defaultView === global.content
412     ).then(() => {
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 });
420       }
421     });
422   },
424   /**
425    * Create a privileged context at initial-document-element-inserted.
426    *
427    * @param {BrowserExtensionContent} extension
428    *     The extension for which the context should be created.
429    * @param {nsIDOMWindow} contentWindow The global of the page.
430    */
431   initExtensionContext(extension, contentWindow) {
432     this._init();
434     if (!WebExtensionPolicy.isExtensionProcess) {
435       throw new Error(
436         "Cannot create an extension page context in current process"
437       );
438     }
440     let windowId = getInnerWindowID(contentWindow);
441     let context = this.extensionContexts.get(windowId);
442     if (context) {
443       if (context.extension !== extension) {
444         throw new Error(
445           "A different extension context already exists for this frame"
446         );
447       }
448       throw new Error(
449         "An extension context was already initialized for this frame"
450       );
451     }
453     let uri = contentWindow.document.documentURIObject;
455     let mm = contentWindow.docShell.messageManager;
456     let data = mm.sendSyncMessage("Extension:GetFrameData")[0];
457     if (!data) {
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}`);
462     }
463     let { viewType, tabId, devtoolsToolboxInfo } = data ?? {};
465     if (viewType && contentWindow.top === contentWindow) {
466       ExtensionPageChild.expectViewLoad(mm, viewType);
467     }
469     if (devtoolsToolboxInfo) {
470       context = new DevToolsContextChild(extension, {
471         viewType,
472         contentWindow,
473         uri,
474         tabId,
475         devtoolsToolboxInfo,
476       });
477     } else {
478       context = new ExtensionPageContextChild(extension, {
479         viewType,
480         contentWindow,
481         uri,
482         tabId,
483       });
484     }
486     this.extensionContexts.set(windowId, context);
487   },
489   /**
490    * Close the ExtensionPageContextChild belonging to the given window, if any.
491    *
492    * @param {number} windowId The inner window ID of the destroyed context.
493    */
494   destroyExtensionContext(windowId) {
495     let context = this.extensionContexts.get(windowId);
496     if (context) {
497       context.unload();
498       this.extensionContexts.delete(windowId);
499     }
500   },
502   shutdownExtension(extensionId) {
503     for (let [windowId, context] of this.extensionContexts) {
504       if (context.extension.id == extensionId) {
505         context.shutdown();
506         this.extensionContexts.delete(windowId);
507       }
508     }
509   },