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