--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -510,16 +510,20 @@ this.tabs = class extends ExtensionAPI {
},
async removeCSS(tabId, details) {
let tab = await promiseTabWhenReady(tabId);
return tab.removeCSS(context, details);
},
+ async setContentScriptOptions(contentScriptOptions) {
+ await context.extension.broadcastContentScriptOptions(contentScriptOptions);
+ },
+
async move(tabIds, moveProperties) {
let index = moveProperties.index;
let tabsMoved = [];
if (!Array.isArray(tabIds)) {
tabIds = [tabIds];
}
let destinationWindow = null;
--- a/browser/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -44,16 +44,17 @@ let expectedBackgroundApisTargetSpecific
"tabs.onReplaced",
"tabs.onUpdated",
"tabs.onZoomChange",
"tabs.query",
"tabs.reload",
"tabs.remove",
"tabs.removeCSS",
"tabs.sendMessage",
+ "tabs.setContentScriptOptions",
"tabs.setZoom",
"tabs.setZoomSettings",
"tabs.update",
"windows.CreateType",
"windows.WINDOW_ID_CURRENT",
"windows.WINDOW_ID_NONE",
"windows.WindowState",
"windows.WindowType",
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -888,16 +888,22 @@ this.Extension = class extends Extension
let serial = this.serialize();
data["Extension:Extensions"].push(serial);
return this.broadcast("Extension:Startup", serial).then(() => {
return Promise.all(promises);
});
}
+ broadcastContentScriptOptions(action, propertyName, propertyValue) {
+ return this.broadcast("Extension:SetContentScriptOptions", {
+ id: this.id, action, propertyName, propertyValue,
+ });
+ }
+
callOnClose(obj) {
this.onShutdown.add(obj);
}
forgetOnClose(obj) {
this.onShutdown.delete(obj);
}
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -100,16 +100,38 @@ var apiManager = new class extends Schem
this.initialized = true;
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
this.loadScript(value);
}
}
}
}();
+function setProcessContentScriptOptions(extensionId, contentScriptOptions) {
+ // Init the contentScriptOptions map in the initialProcessData object.
+ if (!Services.cpmm.initialProcessData["Extension:ContentScriptOptions"]) {
+ Services.cpmm.initialProcessData["Extension:ContentScriptOptions"] = {};
+ }
+
+ let optionsMap = Services.cpmm.initialProcessData["Extension:ContentScriptOptions"];
+ optionsMap[extensionId] = contentScriptOptions;
+}
+
+function getProcessContentScriptOptions(extensionId) {
+ let optionsMap = Services.cpmm.initialProcessData["Extension:ContentScriptOptions"] || {};
+ return optionsMap[extensionId];
+}
+
+function clearProcessContentScriptOptions(extensionId) {
+ let optionsMap = Services.cpmm.initialProcessData["Extension:ContentScriptOptions"];
+ if (optionsMap) {
+ delete optionsMap[extensionId];
+ }
+}
+
const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
const scriptCaches = new WeakSet();
const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
@@ -766,16 +788,20 @@ DocumentManager = {
}
},
"memory-pressure"(subject, topic, data) {
let timeout = data === "heap-minimize" ? 0 : undefined;
for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
cache.clear(timeout);
}
+
+ // TODO(rpl): clear the cached process contentScriptOptions object on memory-pressure?
+ // all the extension will get an undefined browser.runtime.contentScriptOptions
+ // object on the next load of the extensions content scripts.
},
},
observe(subject, topic, data) {
this.observers[topic].call(this, subject, topic, data);
},
handleEvent(event) {
@@ -889,16 +915,17 @@ DocumentManager = {
startupExtension(extensionId) {
if (this.extensionCount == 0) {
this.init();
}
this.extensionCount++;
let extension = ExtensionManager.get(extensionId);
+
for (let global of ExtensionContent.globals.keys()) {
// Note that we miss windows in the bfcache here. In theory we
// could execute content scripts on a pageshow event for that
// window, but that seems extreme.
for (let window of this.enumerateWindows(global.docShell)) {
for (let script of extension.scripts) {
if (script.matches(window)) {
let context = this.getContentScriptContext(extension, window);
@@ -1067,16 +1094,20 @@ class BrowserExtensionContent extends Ev
hasPermission(perm) {
let match = /^manifest:(.*)/.exec(perm);
if (match) {
return this.manifest[match[1]] != null;
}
return this.permissions.has(perm);
}
+
+ get contentScriptOptions() {
+ return getProcessContentScriptOptions(this.id);
+ }
}
defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
return new ScriptCache({hasReturnValue: false});
});
defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
return new ScriptCache({hasReturnValue: true});
@@ -1096,32 +1127,34 @@ ExtensionManager = {
init() {
Schemas.init();
ExtensionChild.initOnce();
Services.cpmm.addMessageListener("Extension:Startup", this);
Services.cpmm.addMessageListener("Extension:Shutdown", this);
Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+ Services.cpmm.addMessageListener("Extension:SetContentScriptOptions", this);
if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
for (let data of extensions) {
this.extensions.set(data.id, new BrowserExtensionContent(data));
DocumentManager.startupExtension(data.id);
}
}
},
get(extensionId) {
return this.extensions.get(extensionId);
},
receiveMessage({name, data}) {
let extension;
+
switch (name) {
case "Extension:Startup": {
extension = new BrowserExtensionContent(data);
this.extensions.set(data.id, extension);
DocumentManager.startupExtension(data.id);
@@ -1131,24 +1164,41 @@ ExtensionManager = {
case "Extension:Shutdown": {
extension = this.extensions.get(data.id);
extension.shutdown();
DocumentManager.shutdownExtension(data.id);
this.extensions.delete(data.id);
+ clearProcessContentScriptOptions(data.id);
+
break;
}
case "Extension:FlushJarCache": {
flushJarCache(data.path);
Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
break;
}
+
+ case "Extension:SetContentScriptOptions": {
+ const options = getProcessContentScriptOptions(data.id) || {};
+
+ if (data.action === "set") {
+ options[data.propertyName] = data.propertyValue;
+ setProcessContentScriptOptions(data.id, options);
+ } else if (data.action === "clear") {
+ clearProcessContentScriptOptions(data.id);
+ } else if (data.action === "unset") {
+ delete options[data.propertyName];
+ }
+
+ break;
+ }
}
},
};
class ExtensionGlobal {
constructor(global) {
this.global = global;
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -52,16 +52,17 @@ function readJSON(url) {
// strip off for this to be valid JSON. As a hack, we just
// look for the first '[' character, which signals the start
// of the JSON content.
let index = text.indexOf("[");
text = text.slice(index);
resolve(JSON.parse(text));
} catch (e) {
+ Cu.reportError(new Error(`Error while loading '${url}' (${e.name})`));
reject(e);
}
});
});
}
/**
* Defines a lazy getter for the given property on the given object. Any
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-contentScriptOptions.js
@@ -0,0 +1,34 @@
+"use strict";
+
+this.contentScriptOptions = class extends ExtensionAPI {
+ getAPI(context) {
+ let {extension} = context;
+
+ return {
+ contentScriptOptions: {
+ getProperty(propertyName) {
+ if (context.envType !== "content_child") {
+ throw new Error('This method is only supported in content scripts');
+ }
+
+ const options = extension.contentScriptOptions || {};
+ return Cu.cloneInto(options[propertyName], context.cloneScope);
+ },
+ getPropertyNames() {
+ if (context.envType !== "content_child") {
+ throw new Error('This method is only supported in content scripts');
+ }
+ const options = extension.contentScriptOptions || {};
+ return Cu.cloneInto(Object.keys(options), context.cloneScope);
+ },
+ getUsageInfo() {
+ if (context.envType !== "addon_child") {
+ throw new Error('This method is not support in content scripts and devtools pages');
+ }
+ return context.childManager
+ .callParentAsyncFunction("contentScriptOptions.getUsageInfo", []);
+ }
+ },
+ };
+ }
+};
--- a/toolkit/components/extensions/ext-c-runtime.js
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -92,12 +92,16 @@ this.runtime = class extends ExtensionAP
return Cu.cloneInto(extension.manifest, context.cloneScope);
},
id: extension.id,
getURL: function(url) {
return extension.baseURI.resolve(url);
},
+
+ get contentScriptOptions() {
+ return Cu.cloneInto(extension.contentScriptOptions, context.cloneScope);
+ },
},
};
}
};
--- a/toolkit/components/extensions/ext-c-toolkit.js
+++ b/toolkit/components/extensions/ext-c-toolkit.js
@@ -27,16 +27,23 @@ extensions.registerModules({
url: "chrome://extensions/content/ext-c-backgroundPage.js",
scopes: ["addon_child"],
manifest: ["background"],
paths: [
["extension", "getBackgroundPage"],
["runtime", "getBackgroundPage"],
],
},
+ contentScriptOptions: {
+ url: "chrome://extensions/content/ext-c-contentScriptOptions.js",
+ scopes: ["addon_child", "content_child"],
+ paths: [
+ ["contentScriptOptions"],
+ ],
+ },
extension: {
url: "chrome://extensions/content/ext-c-extension.js",
scopes: ["addon_child", "content_child", "devtools_child", "proxy_script"],
paths: [
["extension"],
],
},
i18n: {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-contentScriptOptions.js
@@ -0,0 +1,57 @@
+"use strict";
+
+// TODO(rpl): set reasonable limits on the maximum contentScriptOptions size per process,
+// and/or move these values into preferences.
+const MAX_CONTENT_SCRIPT_OPTIONS_LENGTH = 1024;
+const MAX_STRING_PROPERTY_LENGTH = 1024;
+
+// Track the usage of contentScriptOptions for the running extension.
+const contentScriptOptionsUsage = new WeakMap();
+
+this.contentScriptOptions = class extends ExtensionAPI {
+ getAPI(context) {
+ let {extension} = context;
+
+ let usageInfo;
+
+ if (!contentScriptOptionsUsage.has(extension)) {
+ usageInfo = {propertyNames: new Set()};
+ contentScriptOptionsUsage.set(extension, usageInfo);
+ } else {
+ usageInfo = contentScriptOptionsUsage.get(extension);
+ }
+
+ return {
+ contentScriptOptions: {
+ setProperty(propertyName, propertyValue) {
+ if (!usageInfo.propertyNames.has(propertyName) &&
+ usageInfo.propertyNames.size + 1 > MAX_CONTENT_SCRIPT_OPTIONS_LENGTH) {
+ throw new Error(`No more than ${MAX_CONTENT_SCRIPT_OPTIONS_LENGTH}` +
+ " content script options are allowed to be set");
+ }
+
+ if (typeof propertyValue === "string" &&
+ propertyValue.length > MAX_STRING_PROPERTY_LENGTH) {
+ throw new Error(`No strings larger than ${MAX_STRING_PROPERTY_LENGTH}` +
+ " are allowed as content script options property values");
+ }
+ usageInfo.propertyNames.add(propertyName);
+ extension.broadcastContentScriptOptions("set", propertyName, propertyValue);
+ },
+ unsetProperty(propertyName) {
+ usageInfo.propertyNames.delete(propertyName);
+ extension.broadcastContentScriptOptions("unset", propertyName);
+ },
+ clear() {
+ usageInfo.propertyNames.clear();
+ extension.broadcastContentScriptOptions("clear");
+ },
+ getUsageInfo() {
+ return Promise.resolve({
+ propertyNames: Array.from(usageInfo.propertyNames.values()),
+ });
+ }
+ },
+ };
+ }
+};
--- a/toolkit/components/extensions/ext-toolkit.js
+++ b/toolkit/components/extensions/ext-toolkit.js
@@ -69,16 +69,24 @@ extensions.registerModules({
["alarms"],
],
},
backgroundPage: {
url: "chrome://extensions/content/ext-backgroundPage.js",
scopes: ["addon_parent"],
manifest: ["background"],
},
+ contentScriptOptions: {
+ url: "chrome://extensions/content/ext-contentScriptOptions.js",
+ schema: "chrome://extensions/content/schemas/content_script_options.json",
+ scopes: ["addon_parent"],
+ paths: [
+ ["contentScriptOptions"],
+ ],
+ },
contextualIdentities: {
url: "chrome://extensions/content/ext-contextualIdentities.js",
schema: "chrome://extensions/content/schemas/contextual_identities.json",
scopes: ["addon_parent"],
paths: [
["contextualIdentities"],
],
},
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -2,16 +2,17 @@
# 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/.
toolkit.jar:
% content extensions %content/extensions/
content/extensions/ext-alarms.js
content/extensions/ext-backgroundPage.js
content/extensions/ext-browser-content.js
+ content/extensions/ext-contentScriptOptions.js
content/extensions/ext-contextualIdentities.js
content/extensions/ext-cookies.js
content/extensions/ext-downloads.js
content/extensions/ext-extension.js
content/extensions/ext-geolocation.js
content/extensions/ext-i18n.js
content/extensions/ext-idle.js
content/extensions/ext-management.js
@@ -25,16 +26,17 @@ toolkit.jar:
content/extensions/ext-theme.js
content/extensions/ext-toolkit.js
content/extensions/ext-topSites.js
content/extensions/ext-webRequest.js
content/extensions/ext-webNavigation.js
# Below is a separate group using the naming convention ext-c-*.js that run
# in the child process.
content/extensions/ext-c-backgroundPage.js
+ content/extensions/ext-c-contentScriptOptions.js
content/extensions/ext-c-extension.js
#ifndef ANDROID
content/extensions/ext-c-identity.js
#endif
content/extensions/ext-c-permissions.js
content/extensions/ext-c-runtime.js
content/extensions/ext-c-storage.js
content/extensions/ext-c-test.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/content_script_options.json
@@ -0,0 +1,108 @@
+/* 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/. */
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "contentScriptOptions"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "contentScriptOptions",
+ "permissions": ["contentScriptOptions"],
+ "allowedContexts": ["content"],
+ "description": "Use the <code>browser.contentScriptOptions</code> API to set the properties on an object value synchronously available to the code running in the extension content scripts",
+ "functions": [
+ {
+ "name": "setProperty",
+ "type": "function",
+ "description": "Set a property of the contentScriptOptions",
+ "async": true,
+ "parameters": [
+ {
+ "type": "string",
+ "name": "propertyName",
+ "description": "The name of the property to set",
+ "minLength": 1
+ },
+ {
+ "name": "propertyValue",
+ "description": "The value of the contentScriptOptions property.",
+ "choices": [
+ {"type": "string"},
+ {"type": "number"},
+ {"type": "boolean"}
+ ]
+ }
+ ]
+ },
+ {
+ "name": "unsetProperty",
+ "type": "function",
+ "async": true,
+ "description": "Clear all the contentScriptOptions properties",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "propertyName",
+ "description": "The name of the property to set",
+ "minLength": 1
+ }
+ ]
+ },
+ {
+ "name": "clear",
+ "type": "function",
+ "async": true,
+ "description": "Clear all the contentScriptOptions properties",
+ "parameters": []
+ },
+ {
+ "name": "getUsageInfo",
+ "type": "function",
+ "async": true,
+ "description": "Retrieve the contentScriptOptions usage info asynchronously",
+ "parameters": []
+ },
+ {
+ "name": "getProperty",
+ "allowedContexts": ["content"],
+ "type": "function",
+ "description": "Get a contentScriptOptions property",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "propertyName",
+ "description": "The name of the property to get",
+ "minLength": 1
+ }
+ ],
+ "returns": {
+ "type": "any",
+ "description": "The property value."
+ }
+ },
+ {
+ "name": "getPropertyNames",
+ "allowedContexts": ["content"],
+ "type": "function",
+ "description": "Get all the contentScriptOptions property names",
+ "parameters": [],
+ "returns": {
+ "type": "array",
+ "items": {"type": "string"}
+ }
+ }
+ ]
+ }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -1,15 +1,16 @@
# 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/.
toolkit.jar:
% content extensions %content/extensions/
content/extensions/schemas/alarms.json
+ content/extensions/schemas/content_script_options.json
content/extensions/schemas/contextual_identities.json
content/extensions/schemas/cookies.json
content/extensions/schemas/downloads.json
content/extensions/schemas/events.json
content/extensions/schemas/experiments.json
content/extensions/schemas/extension.json
content/extensions/schemas/extension_types.json
content/extensions/schemas/extension_protocol_handlers.json
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
@@ -95,8 +95,95 @@ add_task(function* test_contentscript()
yield contentPage.close();
equal(loadingCount, 1, "document_start script ran exactly once");
equal(interactiveCount, 1, "document_end script ran exactly once");
equal(completeCount, 1, "document_idle script ran exactly once");
yield extension.unload();
});
+
+add_task(function* test_contentscriptOptions() {
+ function background() {
+ browser.test.onMessage.addListener((event, testData) => {
+ if (event !== "content-script-options") {
+ return;
+ }
+
+ if (testData) {
+ for (const prop of Object.keys(testData)) {
+ browser.contentScriptOptions.setProperty(prop, testData[prop]);
+ }
+ } else {
+ browser.contentScriptOptions.clear();
+ }
+ });
+
+ browser.test.sendMessage("background-script-ready");
+ }
+
+ function contentScriptStart() {
+ let propertyNames = browser.contentScriptOptions.getPropertyNames();
+ let testData = {};
+ for (const prop of propertyNames) {
+ testData[prop] = browser.contentScriptOptions.getProperty(prop);
+ }
+ browser.test.sendMessage("content-script-loaded", {testData});
+ }
+
+ let extensionData = {
+ manifest: {
+ applications: {gecko: {id: "contentscript@tests.mozilla.org"}},
+ permissions: ["contentScriptOptions"],
+ content_scripts: [
+ {
+ "matches": ["http://*/*/file_sample.html"],
+ "js": ["content_script_start.js"],
+ "run_at": "document_start",
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script_start.js": contentScriptStart,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ const PAGE_URL = `${BASE_URL}/file_sample.html`;
+ const expectedInitialValue = {key: "content value"};
+ const expectedUpdatedValue = {newkey: "updated content value"};
+
+ const contentPage = yield ExtensionTestUtils.loadContentPage(PAGE_URL);
+
+ yield extension.awaitMessage("background-script-ready");
+
+ extension.sendMessage("content-script-options", expectedInitialValue);
+
+ let res = yield extension.awaitMessage("content-script-loaded");
+ deepEqual(res, {testData: {}}, "contentScriptOptions is undefined on the first load");
+
+ yield contentPage.loadURL(PAGE_URL);
+ res = yield extension.awaitMessage("content-script-loaded");
+ deepEqual(res, {testData: expectedInitialValue},
+ "contentScriptOptions has the expected value on the second load");
+
+ extension.sendMessage("content-script-options", expectedUpdatedValue);
+
+ yield contentPage.loadURL(PAGE_URL);
+ res = yield extension.awaitMessage("content-script-loaded");
+ deepEqual(res, {testData: Object.assign({}, expectedInitialValue, expectedUpdatedValue)},
+ "contentScriptOptions has the expected value on the third load");
+
+ extension.sendMessage("content-script-options", null);
+ yield contentPage.loadURL(PAGE_URL);
+ res = yield extension.awaitMessage("content-script-loaded");
+ deepEqual(res, {testData: {}},
+ "contentScriptOptions has the expected value on the last load");
+
+ yield contentPage.close();
+
+ yield extension.unload();
+});