/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; /* exported registerContentScript, unregisterContentScript */ /* global registerContentScript, unregisterContentScript */ ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); var { ExtensionError, getUniqueId, } = ExtensionUtils; /** * 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, exclude_matches: details.excludeMatches, include_globs: details.includeGlobs, exclude_globs: details.excludeGlobs, all_frames: details.allFrames, match_about_blank: details.matchAboutBlank, run_at: details.runAt, js: [], css: [], }; 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 > 0) { options.js = details.js.map(data => { if (data.file) { return data.file; } return convertCodeToURL([data.code], "text/javascript"); }); } if (details.css && details.css.length > 0) { options.css = 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.broadcast("Extension:UnregisterContentScripts", { id: extension.id, scriptIds, }); }, }); return { contentScripts: { async register(details) { for (let origin of details.matches) { if (!extension.whiteListedHosts.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(); await extension.broadcast("Extension:RegisterContentScript", { id: extension.id, options: scriptOptions, scriptId, }); extension.registeredContentScripts.set(scriptId, 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); contentScript.destroy(); await extension.broadcast("Extension:UnregisterContentScripts", { id: extension.id, scriptIds: [scriptId], }); }, }, }; } };