diff options
Diffstat (limited to 'toolkit/components/extensions/parent/ext-contentScripts.js')
-rw-r--r-- | toolkit/components/extensions/parent/ext-contentScripts.js | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-contentScripts.js b/toolkit/components/extensions/parent/ext-contentScripts.js new file mode 100644 index 0000000000..068b2c7403 --- /dev/null +++ b/toolkit/components/extensions/parent/ext-contentScripts.js @@ -0,0 +1,232 @@ +/* -*- 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"; + +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<scriptId -> 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], + }); + }, + }, + }; + } +}; |