summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/parent/ext-userScripts.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/parent/ext-userScripts.js')
-rw-r--r--toolkit/components/extensions/parent/ext-userScripts.js158
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]);
+ },
+ },
+ };
+ }
+};