/* -*- 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/. */ "use strict"; /* exported registerContentScript, unregisterContentScript */ /* global registerContentScript, unregisterContentScript */ var { ExtensionUtils } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionUtils.sys.mjs" ); var { ExtensionError, getUniqueId } = ExtensionUtils; function getOriginAttributesPatternForCookieStoreId(cookieStoreId) { if (isDefaultCookieStoreId(cookieStoreId)) { return { userContextId: Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, privateBrowsingId: Ci.nsIScriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID, }; } if (isPrivateCookieStoreId(cookieStoreId)) { return { userContextId: Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, privateBrowsingId: 1, }; } if (isContainerCookieStoreId(cookieStoreId)) { let userContextId = getContainerForCookieStoreId(cookieStoreId); if (userContextId !== null) { return { userContextId }; } } throw new ExtensionError("Invalid cookieStoreId"); } /** * Represents (in the main browser process) a content script registered * programmatically (instead of being included in the addon manifest). * * @param {ProxyContextParent} context * The parent proxy context related to the extension context which * has registered the content script. * @param {RegisteredContentScriptOptions} details * The options object related to the registered content script * (which has the properties described in the content_scripts.json * JSON API schema file). */ class ContentScriptParent { constructor({ context, details }) { this.context = context; this.scriptId = getUniqueId(); this.blobURLs = new Set(); this.options = this._convertOptions(details); context.callOnClose(this); } close() { this.destroy(); } destroy() { if (this.destroyed) { throw new Error("Unable to destroy ContentScriptParent twice"); } this.destroyed = true; this.context.forgetOnClose(this); for (const blobURL of this.blobURLs) { this.context.cloneScope.URL.revokeObjectURL(blobURL); } this.blobURLs.clear(); this.context = null; this.options = null; } _convertOptions(details) { const { context } = this; const options = { matches: details.matches, excludeMatches: details.excludeMatches, includeGlobs: details.includeGlobs, excludeGlobs: details.excludeGlobs, allFrames: details.allFrames, matchAboutBlank: details.matchAboutBlank, runAt: details.runAt || "document_idle", jsPaths: [], cssPaths: [], originAttributesPatterns: null, }; if (details.cookieStoreId != null) { const cookieStoreIds = Array.isArray(details.cookieStoreId) ? details.cookieStoreId : [details.cookieStoreId]; options.originAttributesPatterns = cookieStoreIds.map(cookieStoreId => getOriginAttributesPatternForCookieStoreId(cookieStoreId) ); } const convertCodeToURL = (data, mime) => { const blob = new context.cloneScope.Blob(data, { type: mime }); const blobURL = context.cloneScope.URL.createObjectURL(blob); this.blobURLs.add(blobURL); return blobURL; }; if (details.js && details.js.length) { options.jsPaths = details.js.map(data => { if (data.file) { return data.file; } return convertCodeToURL([data.code], "text/javascript"); }); } if (details.css && details.css.length) { options.cssPaths = details.css.map(data => { if (data.file) { return data.file; } return convertCodeToURL([data.code], "text/css"); }); } return options; } serialize() { return this.options; } } this.contentScripts = class extends ExtensionAPI { getAPI(context) { const { extension } = context; // Map of the content script registered from the extension context. // // Map ContentScriptParent> const parentScriptsMap = new Map(); // Unregister all the scriptId related to a context when it is closed. context.callOnClose({ close() { if (parentScriptsMap.size === 0) { return; } const scriptIds = Array.from(parentScriptsMap.keys()); for (let scriptId of scriptIds) { extension.registeredContentScripts.delete(scriptId); } extension.updateContentScripts(); extension.broadcast("Extension:UnregisterContentScripts", { id: extension.id, scriptIds, }); }, }); return { contentScripts: { async register(details) { for (let origin of details.matches) { if (!extension.allowedOrigins.subsumes(new MatchPattern(origin))) { throw new ExtensionError( `Permission denied to register a content script for ${origin}` ); } } const contentScript = new ContentScriptParent({ context, details }); const { scriptId } = contentScript; parentScriptsMap.set(scriptId, contentScript); const scriptOptions = contentScript.serialize(); extension.registeredContentScripts.set(scriptId, scriptOptions); extension.updateContentScripts(); await extension.broadcast("Extension:RegisterContentScripts", { id: extension.id, scripts: [{ scriptId, options: scriptOptions }], }); return scriptId; }, // This method is not available to the extension code, the extension code // doesn't have access to the internally used scriptId, on the contrary // the extension code will call script.unregister on the script API object // that is resolved from the register API method returned promise. async unregister(scriptId) { const contentScript = parentScriptsMap.get(scriptId); if (!contentScript) { Cu.reportError(new Error(`No such content script ID: ${scriptId}`)); return; } parentScriptsMap.delete(scriptId); extension.registeredContentScripts.delete(scriptId); extension.updateContentScripts(); contentScript.destroy(); await extension.broadcast("Extension:UnregisterContentScripts", { id: extension.id, scriptIds: [scriptId], }); }, }, }; } };