diff options
Diffstat (limited to 'toolkit/components/extensions/parent/ext-userScripts.js')
-rw-r--r-- | toolkit/components/extensions/parent/ext-userScripts.js | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-userScripts.js b/toolkit/components/extensions/parent/ext-userScripts.js new file mode 100644 index 0000000000..9c008a4e8d --- /dev/null +++ b/toolkit/components/extensions/parent/ext-userScripts.js @@ -0,0 +1,158 @@ +/* -*- 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 } = ExtensionUtils; + +/** + * Represents (in the main browser process) a user script. + * + * @param {UserScriptOptions} details + * The options object related to the user script + * (which has the properties described in the user_scripts.json + * JSON API schema file). + */ +class UserScriptParent { + constructor(details) { + this.scriptId = details.scriptId; + this.options = this._convertOptions(details); + } + + destroy() { + if (this.destroyed) { + throw new Error("Unable to destroy UserScriptParent twice"); + } + + this.destroyed = true; + this.options = null; + } + + _convertOptions(details) { + 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: details.js, + userScriptOptions: { + scriptMetadata: details.scriptMetadata, + }, + originAttributesPatterns: null, + }; + + if (details.cookieStoreId != null) { + const cookieStoreIds = Array.isArray(details.cookieStoreId) + ? details.cookieStoreId + : [details.cookieStoreId]; + options.originAttributesPatterns = cookieStoreIds.map(cookieStoreId => + getOriginAttributesPatternForCookieStoreId(cookieStoreId) + ); + } + + return options; + } + + serialize() { + return this.options; + } +} + +this.userScripts = class extends ExtensionAPI { + constructor(...args) { + super(...args); + + // Map<scriptId -> UserScriptParent> + this.userScriptsMap = new Map(); + } + + getAPI(context) { + const { extension } = context; + + // Set of the scriptIds registered from this context. + const registeredScriptIds = new Set(); + + const unregisterContentScripts = scriptIds => { + if (scriptIds.length === 0) { + return Promise.resolve(); + } + + for (let scriptId of scriptIds) { + registeredScriptIds.delete(scriptId); + extension.registeredContentScripts.delete(scriptId); + this.userScriptsMap.delete(scriptId); + } + extension.updateContentScripts(); + + return context.extension.broadcast("Extension:UnregisterContentScripts", { + id: context.extension.id, + scriptIds, + }); + }; + + // Unregister all the scriptId related to a context when it is closed, + // and revoke all the created blob urls once the context is destroyed. + context.callOnClose({ + close() { + unregisterContentScripts(Array.from(registeredScriptIds)); + }, + }); + + return { + userScripts: { + register: async details => { + for (let origin of details.matches) { + if (!extension.allowedOrigins.subsumes(new MatchPattern(origin))) { + throw new ExtensionError( + `Permission denied to register a user script for ${origin}` + ); + } + } + + const userScript = new UserScriptParent(details); + const { scriptId } = userScript; + + this.userScriptsMap.set(scriptId, userScript); + registeredScriptIds.add(scriptId); + + const scriptOptions = userScript.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. + unregister: async scriptId => { + const userScript = this.userScriptsMap.get(scriptId); + if (!userScript) { + throw new Error(`No such user script ID: ${scriptId}`); + } + + userScript.destroy(); + + await unregisterContentScripts([scriptId]); + }, + }, + }; + } +}; |