/* 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/. */ import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs"; // This will inject `Debugger` in the global scope // eslint-disable-next-line mozilla/reject-globalThis-modification addDebuggerToGlobal(globalThis); const lazy = {}; const EXTENSION_CONTENT_SYS_MJS = "resource://gre/modules/ExtensionContent.sys.mjs"; // Do not import Targets/index.js to prevent having to load DevTools module loader. const CONTENT_SCRIPT = "content_script"; ChromeUtils.defineESModuleGetters( lazy, { // ExtensionContent.sys.mjs is a singleton and must be loaded through the // main loader. Note that the user of lazy.ExtensionContent elsewhere in // this file (at webextensionsContentScriptGlobals) looks up the module // via Cu.isESModuleLoaded, which also uses the main loader as desired. ExtensionContent: EXTENSION_CONTENT_SYS_MJS, }, { global: "shared" } ); let gDbg = null; function watch() { // Listen for new globals via Spidermonkey Debugger API // in order to catch new Content Script sandboxes created later on gDbg = new Debugger(); gDbg.onNewGlobalObject = onNewGlobal; // Also listen to this event emitted by ExtensionContent to know // when some Content Script sandboxes are destroyed. // It happens when a page navigates/reload and also on add-on disabling. Services.obs.addObserver(observe, "content-script-destroyed"); } function unwatch() { gDbg.onNewGlobalObject = undefined; gDbg = null; Services.obs.removeObserver(observe, "content-script-destroyed"); } /** * Called whenever a new global is instantiated in the current process * * @param {Debugger.Object} global */ function onNewGlobal(global) { // Content scripts are only using Sandboxes as global. if (global.class != "Sandbox") { return; } // WebExtension codebase will flag its sandboxes with a browser-id attribute // in order to inform which tab they are injected against. const contentScriptSandbox = global.unsafeDereference(); const metadata = Cu.getSandboxMetadata(contentScriptSandbox); if (!metadata) { return; } const sandboxBrowserId = metadata["browser-id"]; if (!sandboxBrowserId) { return; } for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( CONTENT_SCRIPT )) { const { sessionContext } = watcherDataObject; const { browserId, type } = sessionContext; // Accept all the content scripts if we are debugging everything (session context type == all) if (type != "all" && browserId != sandboxBrowserId) { continue; } createContentScriptTargetActor(watcherDataObject, { sessionContext, contentScriptSandbox, }); } } /** * Listen to an Observer Service notification emitted by ExtensionContent * when any content script sandbox is destroyed. */ function observe(sandbox, topic) { if (topic != "content-script-destroyed") { return; } for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( CONTENT_SCRIPT )) { const targetActor = watcherDataObject.actors.find( actor => actor.contentScriptSandbox === sandbox ); if (!targetActor) { continue; } ContentProcessWatcherRegistry.destroyTargetActor( watcherDataObject, targetActor, {} ); } } function createTargetsForWatcher(watcherDataObject, _isProcessActorStartup) { // Ignore this process if there is no extension activity in it if (!Cu.isESModuleLoaded(EXTENSION_CONTENT_SYS_MJS)) { return; } const { sessionContext } = watcherDataObject; // Only try spawning Content Script targets when debugging tabs or everything. if ( sessionContext.type != "browser-element" && sessionContext.type != "all" ) { return; } const { browserId, type } = sessionContext; const sandboxes = lazy.ExtensionContent.getAllContentScriptGlobals(); for (const contentScriptSandbox of sandboxes) { if (!contentScriptSandbox || Cu.isDeadWrapper(contentScriptSandbox)) { // Bug 1979448: Skip invalid or dead sandboxes. continue; } const metadata = Cu.getSandboxMetadata(contentScriptSandbox); // Accept all the content scripts if we are debugging everything (session context type == all) // // Ignore sandboxes without metadata which are related to the hack // of bug 1214658, which spawns content script sandboxes in order // to expose chrome/browser API to iframes loading extension documents. if (type != "all" && (!metadata || metadata["browser-id"] != browserId)) { continue; } createContentScriptTargetActor(watcherDataObject, { sessionContext, contentScriptSandbox, }); } } function destroyTargetsForWatcher(watcherDataObject, options) { // Unregister and destroy the existing target actors for this target type const actorsToDestroy = watcherDataObject.actors.filter( actor => actor.targetType == CONTENT_SCRIPT ); watcherDataObject.actors = watcherDataObject.actors.filter( actor => actor.targetType != CONTENT_SCRIPT ); for (const actor of actorsToDestroy) { ContentProcessWatcherRegistry.destroyTargetActor( watcherDataObject, actor, options ); } } /** * Instantiate a ContentScript target actor for a given content script sandbox * and a given watcher actor. * * @param {object} watcherDataObject * @param {Sandbox} sandbox */ function createContentScriptTargetActor(watcherDataObject, sandbox) { const { connection, loader } = ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher( watcherDataObject.watcherActorID, false ); const { WebExtensionContentScriptTargetActor } = loader.require( "devtools/server/actors/targets/content-script" ); // Create the actual target actor. const targetActor = new WebExtensionContentScriptTargetActor( connection, sandbox ); ContentProcessWatcherRegistry.onNewTargetActor( watcherDataObject, targetActor, false ); } export const ContentScriptTargetWatcher = { watch, unwatch, createTargetsForWatcher, destroyTargetsForWatcher, };