mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-01 00:38:50 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			507 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			507 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* 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/. */
 | |
| 
 | |
| /**
 | |
|  * This file handles privileged extension page logic that runs in the
 | |
|  * child process.
 | |
|  */
 | |
| 
 | |
| const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 | |
| const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 | |
| 
 | |
| import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
 | |
| import {
 | |
|   ChildAPIManager,
 | |
|   ExtensionActivityLogChild,
 | |
|   Messenger,
 | |
| } from "resource://gre/modules/ExtensionChild.sys.mjs";
 | |
| import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| const lazy = XPCOMUtils.declareLazy({
 | |
|   ExtensionChildDevToolsUtils:
 | |
|     "resource://gre/modules/ExtensionChildDevToolsUtils.sys.mjs",
 | |
|   Schemas: "resource://gre/modules/Schemas.sys.mjs",
 | |
| });
 | |
| 
 | |
| const { getInnerWindowID, promiseEvent } = ExtensionUtils;
 | |
| 
 | |
| const { BaseContext, CanOfAPIs, SchemaAPIManager, redefineGetter } =
 | |
|   ExtensionCommon;
 | |
| 
 | |
| const initializeBackgroundPage = context => {
 | |
|   // Override the `alert()` method inside background windows;
 | |
|   // we alias it to console.log().
 | |
|   // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1203394
 | |
|   let alertDisplayedWarning = false;
 | |
|   const innerWindowID = getInnerWindowID(context.contentWindow);
 | |
| 
 | |
|   /** @param {{ text, filename, lineNumber?, columnNumber? }} options */
 | |
|   function logWarningMessage({ text, filename, lineNumber, columnNumber }) {
 | |
|     let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
 | |
|       Ci.nsIScriptError
 | |
|     );
 | |
|     consoleMsg.initWithWindowID(
 | |
|       text,
 | |
|       filename,
 | |
|       lineNumber,
 | |
|       columnNumber,
 | |
|       Ci.nsIScriptError.warningFlag,
 | |
|       "webextension",
 | |
|       innerWindowID
 | |
|     );
 | |
|     Services.console.logMessage(consoleMsg);
 | |
|   }
 | |
| 
 | |
|   function ignoredSuspendListener() {
 | |
|     logWarningMessage({
 | |
|       text: "Background event page was not terminated on idle because a DevTools toolbox is attached to the extension.",
 | |
|       filename: context.contentWindow.location.href,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   if (!context.extension.manifest.background.persistent) {
 | |
|     context.extension.on(
 | |
|       "background-script-suspend-ignored",
 | |
|       ignoredSuspendListener
 | |
|     );
 | |
|     context.callOnClose({
 | |
|       close: () => {
 | |
|         context.extension.off(
 | |
|           "background-script-suspend-ignored",
 | |
|           ignoredSuspendListener
 | |
|         );
 | |
|       },
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   let alertOverwrite = text => {
 | |
|     const { filename, columnNumber, lineNumber } = Components.stack.caller;
 | |
| 
 | |
|     if (!alertDisplayedWarning) {
 | |
|       context.childManager.callParentAsyncFunction(
 | |
|         "runtime.openBrowserConsole",
 | |
|         []
 | |
|       );
 | |
| 
 | |
|       logWarningMessage({
 | |
|         text: "alert() is not supported in background windows; please use console.log instead.",
 | |
|         filename,
 | |
|         lineNumber,
 | |
|         columnNumber,
 | |
|       });
 | |
| 
 | |
|       alertDisplayedWarning = true;
 | |
|     }
 | |
| 
 | |
|     logWarningMessage({ text, filename, lineNumber, columnNumber });
 | |
|   };
 | |
|   Cu.exportFunction(alertOverwrite, context.contentWindow, {
 | |
|     defineAs: "alert",
 | |
|   });
 | |
| };
 | |
| 
 | |
| var apiManager = new (class extends SchemaAPIManager {
 | |
|   constructor() {
 | |
|     super("addon", lazy.Schemas);
 | |
|     this.initialized = false;
 | |
|   }
 | |
| 
 | |
|   lazyInit() {
 | |
|     if (!this.initialized) {
 | |
|       this.initialized = true;
 | |
|       this.initGlobal();
 | |
|       for (let { value } of Services.catMan.enumerateCategory(
 | |
|         CATEGORY_EXTENSION_SCRIPTS_ADDON
 | |
|       )) {
 | |
|         this.loadScript(value);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| })();
 | |
| 
 | |
| var devtoolsAPIManager = new (class extends SchemaAPIManager {
 | |
|   constructor() {
 | |
|     super("devtools", lazy.Schemas);
 | |
|     this.initialized = false;
 | |
|   }
 | |
| 
 | |
|   lazyInit() {
 | |
|     if (!this.initialized) {
 | |
|       this.initialized = true;
 | |
|       this.initGlobal();
 | |
|       for (let { value } of Services.catMan.enumerateCategory(
 | |
|         CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS
 | |
|       )) {
 | |
|         this.loadScript(value);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| })();
 | |
| 
 | |
| export function getContextChildManagerGetter(
 | |
|   { envType },
 | |
|   ChildAPIManagerClass = ChildAPIManager
 | |
| ) {
 | |
|   return function () {
 | |
|     let apiManager =
 | |
|       envType === "devtools_parent"
 | |
|         ? devtoolsAPIManager
 | |
|         : this.extension.apiManager;
 | |
| 
 | |
|     apiManager.lazyInit();
 | |
| 
 | |
|     let localApis = {};
 | |
|     let can = new CanOfAPIs(this, apiManager, localApis);
 | |
| 
 | |
|     let childManager = new ChildAPIManagerClass(
 | |
|       this,
 | |
|       this.messageManager,
 | |
|       can,
 | |
|       {
 | |
|         envType,
 | |
|         viewType: this.viewType,
 | |
|         url: this.uri.spec,
 | |
|         incognito: this.incognito,
 | |
|         // Additional data a BaseContext subclass may optionally send
 | |
|         // as part of the CreateProxyContext request sent to the main process
 | |
|         // (e.g. WorkerContexChild implements this method to send the service
 | |
|         // worker descriptor id along with the details send by default here).
 | |
|         ...this.getCreateProxyContextData?.(),
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     this.callOnClose(childManager);
 | |
| 
 | |
|     return childManager;
 | |
|   };
 | |
| }
 | |
| 
 | |
| export class ExtensionBaseContextChild extends BaseContext {
 | |
|   /**
 | |
|    * This ExtensionBaseContextChild represents an addon execution environment
 | |
|    * that is running in an addon or devtools child process.
 | |
|    *
 | |
|    * @param {ExtensionChild} extension This context's owner.
 | |
|    * @param {object} params
 | |
|    * @param {string} params.envType One of "addon_child" or "devtools_child".
 | |
|    * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
 | |
|    * @param {string} params.viewType One of "background", "popup", "tab",
 | |
|    *   "sidebar", "devtools_page" or "devtools_panel".
 | |
|    * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
 | |
|    * @param {nsIURI} [params.uri] The URI of the page.
 | |
|    */
 | |
|   constructor(extension, params) {
 | |
|     if (!params.envType) {
 | |
|       throw new Error("Missing envType");
 | |
|     }
 | |
| 
 | |
|     super(params.envType, extension);
 | |
|     let { viewType = "tab", uri, contentWindow, tabId } = params;
 | |
|     this.viewType = viewType;
 | |
|     this.uri = uri || extension.baseURI;
 | |
| 
 | |
|     this.setContentWindow(contentWindow);
 | |
|     this.browsingContextId = contentWindow.docShell.browsingContext.id;
 | |
| 
 | |
|     if (viewType == "tab") {
 | |
|       Object.defineProperty(this, "tabId", {
 | |
|         value: tabId,
 | |
|         enumerable: true,
 | |
|         configurable: true,
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     lazy.Schemas.exportLazyGetter(contentWindow, "browser", () => {
 | |
|       return this.browserObj;
 | |
|     });
 | |
| 
 | |
|     lazy.Schemas.exportLazyGetter(contentWindow, "chrome", () => {
 | |
|       // For MV3 and later, this is just an alias for browser.
 | |
|       if (extension.manifestVersion > 2) {
 | |
|         return this.browserObj;
 | |
|       }
 | |
|       // Chrome compat is only used with MV2
 | |
|       let chromeApiWrapper = Object.create(this.childManager);
 | |
|       chromeApiWrapper.isChromeCompat = true;
 | |
| 
 | |
|       let chromeObj = Cu.createObjectIn(contentWindow);
 | |
|       chromeApiWrapper.inject(chromeObj);
 | |
|       return chromeObj;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   get browserObj() {
 | |
|     const browserObj = Cu.createObjectIn(this.contentWindow);
 | |
|     this.childManager.inject(browserObj);
 | |
|     return redefineGetter(this, "browserObj", browserObj);
 | |
|   }
 | |
| 
 | |
|   logActivity(type, name, data) {
 | |
|     ExtensionActivityLogChild.log(this, type, name, data);
 | |
|   }
 | |
| 
 | |
|   get cloneScope() {
 | |
|     return this.contentWindow;
 | |
|   }
 | |
| 
 | |
|   get principal() {
 | |
|     return this.contentWindow.document.nodePrincipal;
 | |
|   }
 | |
| 
 | |
|   get tabId() {
 | |
|     // Will be overwritten in the constructor if necessary.
 | |
|     return -1;
 | |
|   }
 | |
| 
 | |
|   // Called when the extension shuts down.
 | |
|   shutdown() {
 | |
|     if (this.contentWindow) {
 | |
|       this.contentWindow.close();
 | |
|     }
 | |
| 
 | |
|     this.unload();
 | |
|   }
 | |
| 
 | |
|   // This method is called when an extension page navigates away or
 | |
|   // its tab is closed.
 | |
|   unload() {
 | |
|     // Note that without this guard, we end up running unload code
 | |
|     // multiple times for tab pages closed by the "page-unload" handlers
 | |
|     // triggered below.
 | |
|     if (this.unloaded) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     super.unload();
 | |
|   }
 | |
| 
 | |
|   get messenger() {
 | |
|     return redefineGetter(this, "messenger", new Messenger(this));
 | |
|   }
 | |
| 
 | |
|   /** @type {ReturnType<ReturnType<getContextChildManagerGetter>>} */
 | |
|   get childManager() {
 | |
|     throw new Error("childManager getter must be overridden");
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ExtensionPageContextChild extends ExtensionBaseContextChild {
 | |
|   /**
 | |
|    * This ExtensionPageContextChild represents a privileged addon
 | |
|    * execution environment that has full access to the WebExtensions
 | |
|    * APIs (provided that the correct permissions have been requested).
 | |
|    *
 | |
|    * This is the child side of the ExtensionPageContextParent class
 | |
|    * defined in ExtensionParent.sys.mjs.
 | |
|    *
 | |
|    * @param {ExtensionChild} extension This context's owner.
 | |
|    * @param {object} params
 | |
|    * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
 | |
|    * @param {string} params.viewType One of "background", "popup", "sidebar" or "tab".
 | |
|    *     "background", "sidebar" and "tab" are used by `browser.extension.getViews`.
 | |
|    *     "popup" is only used internally to identify page action and browser
 | |
|    *     action popups and options_ui pages.
 | |
|    * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
 | |
|    * @param {nsIURI} [params.uri] The URI of the page.
 | |
|    */
 | |
|   constructor(extension, params) {
 | |
|     super(extension, Object.assign(params, { envType: "addon_child" }));
 | |
| 
 | |
|     if (this.viewType == "background") {
 | |
|       initializeBackgroundPage(this);
 | |
|     }
 | |
| 
 | |
|     this.extension.views.add(this);
 | |
|   }
 | |
| 
 | |
|   unload() {
 | |
|     super.unload();
 | |
|     this.extension.views.delete(this);
 | |
|   }
 | |
| 
 | |
|   get childManager() {
 | |
|     const childManager = getContextChildManagerGetter({
 | |
|       envType: "addon_parent",
 | |
|     }).call(this);
 | |
|     return redefineGetter(this, "childManager", childManager);
 | |
|   }
 | |
| }
 | |
| 
 | |
| export class DevToolsContextChild extends ExtensionBaseContextChild {
 | |
|   /**
 | |
|    * This DevToolsContextChild represents a devtools-related addon execution
 | |
|    * environment that has access to the devtools API namespace and to the same subset
 | |
|    * of APIs available in a content script execution environment.
 | |
|    *
 | |
|    * @param {ExtensionChild} extension This context's owner.
 | |
|    * @param {object} params
 | |
|    * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
 | |
|    * @param {string} params.viewType One of "devtools_page" or "devtools_panel".
 | |
|    * @param {object} [params.devtoolsToolboxInfo] This devtools toolbox's information,
 | |
|    *   used if viewType is "devtools_page" or "devtools_panel".
 | |
|    * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
 | |
|    * @param {nsIURI} [params.uri] The URI of the page.
 | |
|    */
 | |
|   constructor(extension, params) {
 | |
|     super(extension, Object.assign(params, { envType: "devtools_child" }));
 | |
| 
 | |
|     this.devtoolsToolboxInfo = params.devtoolsToolboxInfo;
 | |
|     lazy.ExtensionChildDevToolsUtils.initThemeChangeObserver(
 | |
|       params.devtoolsToolboxInfo.themeName,
 | |
|       this
 | |
|     );
 | |
| 
 | |
|     this.extension.devtoolsViews.add(this);
 | |
|   }
 | |
| 
 | |
|   unload() {
 | |
|     super.unload();
 | |
|     this.extension.devtoolsViews.delete(this);
 | |
|   }
 | |
| 
 | |
|   get childManager() {
 | |
|     const childManager = getContextChildManagerGetter({
 | |
|       envType: "devtools_parent",
 | |
|     }).call(this);
 | |
|     return redefineGetter(this, "childManager", childManager);
 | |
|   }
 | |
| }
 | |
| 
 | |
| export var ExtensionPageChild = {
 | |
|   initialized: false,
 | |
| 
 | |
|   // Map<innerWindowId, ExtensionPageContextChild>
 | |
|   extensionContexts: new Map(),
 | |
| 
 | |
|   apiManager,
 | |
| 
 | |
|   _init() {
 | |
|     if (this.initialized) {
 | |
|       return;
 | |
|     }
 | |
|     this.initialized = true;
 | |
| 
 | |
|     Services.obs.addObserver(this, "inner-window-destroyed"); // eslint-ignore-line mozilla/balanced-listeners
 | |
|   },
 | |
| 
 | |
|   observe(subject, topic) {
 | |
|     if (topic === "inner-window-destroyed") {
 | |
|       let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
 | |
| 
 | |
|       this.destroyExtensionContext(windowId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   expectViewLoad(global, viewType) {
 | |
|     promiseEvent(
 | |
|       global,
 | |
|       "DOMContentLoaded",
 | |
|       true,
 | |
|       /** @param {{target: Window|any}} event */
 | |
|       event =>
 | |
|         event.target.location != "about:blank" &&
 | |
|         // Ignore DOMContentLoaded bubbled from child frames:
 | |
|         event.target.defaultView === global.content
 | |
|     ).then(() => {
 | |
|       let windowId = getInnerWindowID(global.content);
 | |
|       let context = this.extensionContexts.get(windowId);
 | |
|       // This initializes ChildAPIManager (and creation of ProxyContextParent)
 | |
|       // if they don't exist already at this point.
 | |
|       let childId = context?.childManager.id;
 | |
|       if (viewType === "background") {
 | |
|         global.sendAsyncMessage("Extension:BackgroundViewLoaded", { childId });
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create a privileged context at initial-document-element-inserted.
 | |
|    *
 | |
|    * @param {ExtensionChild} extension
 | |
|    *     The extension for which the context should be created.
 | |
|    * @param {nsIDOMWindow} contentWindow The global of the page.
 | |
|    */
 | |
|   initExtensionContext(extension, contentWindow) {
 | |
|     this._init();
 | |
| 
 | |
|     if (!WebExtensionPolicy.isExtensionProcess) {
 | |
|       throw new Error(
 | |
|         "Cannot create an extension page context in current process"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let windowId = getInnerWindowID(contentWindow);
 | |
|     let context = this.extensionContexts.get(windowId);
 | |
|     if (context) {
 | |
|       if (context.extension !== extension) {
 | |
|         throw new Error(
 | |
|           "A different extension context already exists for this frame"
 | |
|         );
 | |
|       }
 | |
|       throw new Error(
 | |
|         "An extension context was already initialized for this frame"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let uri = contentWindow.document.documentURIObject;
 | |
| 
 | |
|     let mm = contentWindow.docShell.messageManager;
 | |
|     let data = mm.sendSyncMessage("Extension:GetFrameData")[0];
 | |
|     if (!data) {
 | |
|       let policy = WebExtensionPolicy.getByHostname(uri.host);
 | |
|       // TODO bug 1749116: Handle this unexpected result, because data
 | |
|       // (viewType in particular) should never be void for extension documents.
 | |
|       Cu.reportError(`FrameData missing for ${policy?.id} page ${uri.spec}`);
 | |
|     }
 | |
|     let { viewType, tabId, devtoolsToolboxInfo } = data ?? {};
 | |
| 
 | |
|     if (viewType && contentWindow.top === contentWindow) {
 | |
|       ExtensionPageChild.expectViewLoad(mm, viewType);
 | |
|     }
 | |
| 
 | |
|     if (devtoolsToolboxInfo) {
 | |
|       context = new DevToolsContextChild(extension, {
 | |
|         viewType,
 | |
|         contentWindow,
 | |
|         uri,
 | |
|         tabId,
 | |
|         devtoolsToolboxInfo,
 | |
|       });
 | |
|     } else {
 | |
|       context = new ExtensionPageContextChild(extension, {
 | |
|         viewType,
 | |
|         contentWindow,
 | |
|         uri,
 | |
|         tabId,
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     this.extensionContexts.set(windowId, context);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Close the ExtensionPageContextChild belonging to the given window, if any.
 | |
|    *
 | |
|    * @param {number} windowId The inner window ID of the destroyed context.
 | |
|    */
 | |
|   destroyExtensionContext(windowId) {
 | |
|     let context = this.extensionContexts.get(windowId);
 | |
|     if (context) {
 | |
|       context.unload();
 | |
|       this.extensionContexts.delete(windowId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   shutdownExtension(extensionId) {
 | |
|     for (let [windowId, context] of this.extensionContexts) {
 | |
|       if (context.extension.id == extensionId) {
 | |
|         context.shutdown();
 | |
|         this.extensionContexts.delete(windowId);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | 
