Bug 1263011: Part 2 - Implement WebExtensions Experiments prototype. r=aswan

MozReview-Commit-ID: 4KO4cCLRsLf

--HG--
extra : rebase_source : 40e5ec808e557e845a771bb21e8863a8edcd4faf
This commit is contained in:
Kris Maglione 2016-08-05 14:20:54 -07:00
Родитель b022382989
Коммит 73b1b5f221
13 изменённых файлов: 534 добавлений и 61 удалений

Просмотреть файл

@ -26,6 +26,14 @@ Cu.importGlobalProperties(["TextEncoder"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
"resource://gre/modules/ExtensionAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale", XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm"); "resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log", XPCOMUtils.defineLazyModuleGetter(this, "Log",
@ -34,10 +42,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
"resource://gre/modules/MatchPattern.jsm"); "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"resource://gre/modules/MatchPattern.jsm"); "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm"); "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm"); "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
@ -48,12 +56,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm"); "resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm"); "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyGetter(this, "require", () => { XPCOMUtils.defineLazyGetter(this, "require", () => {
let obj = {}; let obj = {};
@ -68,6 +70,12 @@ const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas"; const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts"; const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
let schemaURLs = new Set();
if (!AppConstants.RELEASE_BUILD) {
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
}
Cu.import("resource://gre/modules/ExtensionUtils.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var { var {
BaseContext, BaseContext,
@ -115,8 +123,11 @@ var Management = {
// extended by other schemas, so needs to be loaded first. // extended by other schemas, so needs to be loaded first.
let promise = Schemas.load(BASE_SCHEMA).then(() => { let promise = Schemas.load(BASE_SCHEMA).then(() => {
let promises = []; let promises = [];
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) { for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
promises.push(Schemas.load(value)); promises.push(Schemas.load(url));
}
for (let url of schemaURLs) {
promises.push(Schemas.load(url));
} }
return Promise.all(promises); return Promise.all(promises);
}); });
@ -196,6 +207,10 @@ var Management = {
copy(obj, api); copy(obj, api);
} }
for (let api of extension.apis) {
copy(obj, api.getAPI(context));
}
return obj; return obj;
}, },
@ -741,6 +756,10 @@ this.ExtensionData = class {
this.localeData = null; this.localeData = null;
this._promiseLocales = null; this._promiseLocales = null;
this.apiNames = new Set();
this.dependencies = new Set();
this.permissions = new Set();
this.errors = []; this.errors = [];
} }
@ -923,6 +942,25 @@ this.ExtensionData = class {
// Errors are handled by the type checks above. // Errors are handled by the type checks above.
} }
let permissions = this.manifest.permissions || [];
let whitelist = [];
for (let perm of permissions) {
this.permissions.add(perm);
let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
if (!match) {
whitelist.push(perm);
} else if (match[1] == "experiments" && match[2]) {
this.apiNames.add(match[2]);
}
}
this.whiteListedHosts = new MatchPattern(whitelist);
for (let api of this.apiNames) {
this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
}
return this.manifest; return this.manifest;
}); });
} }
@ -1173,7 +1211,7 @@ this.Extension = class extends ExtensionData {
this.uninstallURL = null; this.uninstallURL = null;
this.permissions = new Set(); this.apis = [];
this.whiteListedHosts = null; this.whiteListedHosts = null;
this.webAccessibleResources = null; this.webAccessibleResources = null;
@ -1249,10 +1287,14 @@ this.Extension = class extends ExtensionData {
provide(files, ["manifest.json"], manifest); provide(files, ["manifest.json"], manifest);
return this.generateZipFile(files);
}
static generateZipFile(files, baseName = "generated-extension.xpi") {
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter"); let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
let zipW = new ZipWriter(); let zipW = new ZipWriter();
let file = FileUtils.getFile("TmpD", ["generated-extension.xpi"]); let file = FileUtils.getFile("TmpD", [baseName]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
const MODE_WRONLY = 0x02; const MODE_WRONLY = 0x02;
@ -1277,7 +1319,7 @@ this.Extension = class extends ExtensionData {
let script = files[filename]; let script = files[filename];
if (typeof(script) == "function") { if (typeof(script) == "function") {
script = "(" + script.toString() + ")()"; script = "(" + script.toString() + ")()";
} else if (instanceOf(script, "Object")) { } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
script = JSON.stringify(script); script = JSON.stringify(script);
} }
@ -1355,6 +1397,25 @@ this.Extension = class extends ExtensionData {
return common == this.baseURI.spec; return common == this.baseURI.spec;
} }
readManifest() {
return super.readManifest().then(manifest => {
if (AppConstants.RELEASE_BUILD) {
return manifest;
}
// Load Experiments APIs that this extension depends on.
return Promise.all(
Array.from(this.apiNames, api => ExtensionAPIs.load(api))
).then(apis => {
for (let API of apis) {
this.apis.push(new API(this));
}
return manifest;
});
});
}
// Representation of the extension to send to content // Representation of the extension to send to content
// processes. This should include anything the content process might // processes. This should include anything the content process might
// need. // need.
@ -1388,17 +1449,6 @@ this.Extension = class extends ExtensionData {
} }
runManifest(manifest) { runManifest(manifest) {
let permissions = manifest.permissions || [];
let whitelist = [];
for (let perm of permissions) {
this.permissions.add(perm);
if (!/^\w+(\.\w+)*$/.test(perm)) {
whitelist.push(perm);
}
}
this.whiteListedHosts = new MatchPattern(whitelist);
// Strip leading slashes from web_accessible_resources. // Strip leading slashes from web_accessible_resources.
let strippedWebAccessibleResources = []; let strippedWebAccessibleResources = [];
if (manifest.web_accessible_resources) { if (manifest.web_accessible_resources) {
@ -1544,6 +1594,10 @@ this.Extension = class extends ExtensionData {
obj.close(); obj.close();
} }
for (let api of this.apis) {
api.destroy();
}
Management.emit("shutdown", this); Management.emit("shutdown", this);
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id}); Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});

Просмотреть файл

@ -0,0 +1,81 @@
/* 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";
this.EXPORTED_SYMBOLS = ["ExtensionAPI", "ExtensionAPIs"];
/* exported ExtensionAPIs */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const global = this;
class ExtensionAPI {
constructor(extension) {
this.extension = extension;
}
destroy() {
}
getAPI(context) {
throw new Error("Not Implemented");
}
}
var ExtensionAPIs = {
apis: ExtensionManagement.APIs.apis,
load(apiName) {
let api = this.apis.get(apiName);
if (api.loadPromise) {
return api.loadPromise;
}
let {script, schema} = api;
let addonId = `${api}@experiments.addons.mozilla.org`;
api.sandbox = Cu.Sandbox(global, {
wantXrays: false,
sandboxName: script,
addonId,
metadata: {addonID: addonId},
});
api.sandbox.ExtensionAPI = ExtensionAPI;
Services.scriptloader.loadSubScript(script, api.sandbox, "UTF-8");
api.loadPromise = Schemas.load(schema).then(() => {
return Cu.evalInSandbox("API", api.sandbox);
});
return api.loadPromise;
},
unload(apiName) {
let api = this.apis.get(apiName);
let {schema} = api;
Schemas.unload(schema);
Cu.nukeSandbox(api.sandbox);
api.sandbox = null;
api.loadPromise = null;
},
};

Просмотреть файл

@ -85,6 +85,26 @@ var Frames = {
}; };
Frames.init(); Frames.init();
var APIs = {
apis: new Map(),
register(namespace, schema, script) {
if (this.apis.has(namespace)) {
throw new Error(`API namespace already exists: ${namespace}`);
}
this.apis.set(namespace, {schema, script});
},
unregister(namespace) {
if (!this.apis.has(namespace)) {
throw new Error(`API namespace does not exist: ${namespace}`);
}
this.apis.delete(namespace);
},
};
// This object manages various platform-level issues related to // This object manages various platform-level issues related to
// moz-extension:// URIs. It lives here so that it can be used in both // moz-extension:// URIs. It lives here so that it can be used in both
// the parent and child processes. // the parent and child processes.
@ -274,6 +294,9 @@ this.ExtensionManagement = {
startupExtension: Service.startupExtension.bind(Service), startupExtension: Service.startupExtension.bind(Service),
shutdownExtension: Service.shutdownExtension.bind(Service), shutdownExtension: Service.shutdownExtension.bind(Service),
registerAPI: APIs.register.bind(APIs),
unregisterAPI: APIs.unregister.bind(APIs),
getFrameId: Frames.getId.bind(Frames), getFrameId: Frames.getId.bind(Frames),
getParentFrameId: Frames.getParentId.bind(Frames), getParentFrameId: Frames.getParentId.bind(Frames),
@ -281,4 +304,6 @@ this.ExtensionManagement = {
getAddonIdForWindow, getAddonIdForWindow,
getAPILevelForWindow, getAPILevelForWindow,
API_LEVELS, API_LEVELS,
APIs,
}; };

Просмотреть файл

@ -1247,9 +1247,6 @@ class Event extends CallEntry {
this.Schemas = { this.Schemas = {
initialized: false, initialized: false,
// Set of URLs that we have loaded via the load() method.
loadedUrls: new Set(),
// Maps a schema URL to the JSON contained in that schema file. This // Maps a schema URL to the JSON contained in that schema file. This
// is useful for sending the JSON across processes. // is useful for sending the JSON across processes.
schemaJSON: new Map(), schemaJSON: new Map(),
@ -1545,50 +1542,77 @@ this.Schemas = {
} }
Services.cpmm.addMessageListener("Schema:Add", this); Services.cpmm.addMessageListener("Schema:Add", this);
} }
this.flushSchemas();
}, },
receiveMessage(msg) { receiveMessage(msg) {
switch (msg.name) { switch (msg.name) {
case "Schema:Add": case "Schema:Add":
this.schemaJSON.set(msg.data.url, msg.data.schema); this.schemaJSON.set(msg.data.url, msg.data.schema);
this.flushSchemas();
break;
case "Schema:Delete":
this.schemaJSON.delete(msg.data.url);
this.flushSchemas();
break; break;
} }
}, },
load(url) { flushSchemas() {
let loadFromJSON = json => { XPCOMUtils.defineLazyGetter(this, "namespaces",
for (let namespace of json) { () => this.parseSchemas());
let name = namespace.namespace; },
let types = namespace.types || []; parseSchemas() {
for (let type of types) { Object.defineProperty(this, "namespaces", {
this.loadType(name, type); enumerable: true,
} configurable: true,
value: new Map(),
});
let properties = namespace.properties || {}; for (let json of this.schemaJSON.values()) {
for (let propertyName of Object.keys(properties)) { this.parseSchema(json);
this.loadProperty(name, propertyName, properties[propertyName]); }
}
let functions = namespace.functions || []; return this.namespaces;
for (let fun of functions) { },
this.loadFunction(name, fun);
}
let events = namespace.events || []; parseSchema(json) {
for (let event of events) { for (let namespace of json) {
this.loadEvent(name, event); let name = namespace.namespace;
}
if (namespace.permissions) { let types = namespace.types || [];
let ns = this.namespaces.get(name); for (let type of types) {
ns.permissions = namespace.permissions; this.loadType(name, type);
}
} }
};
let properties = namespace.properties || {};
for (let propertyName of Object.keys(properties)) {
this.loadProperty(name, propertyName, properties[propertyName]);
}
let functions = namespace.functions || [];
for (let fun of functions) {
this.loadFunction(name, fun);
}
let events = namespace.events || [];
for (let event of events) {
this.loadEvent(name, event);
}
if (namespace.permissions) {
let ns = this.namespaces.get(name);
ns.permissions = namespace.permissions;
}
}
},
load(url) {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) { if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
let result = readJSON(url).then(json => { return readJSON(url).then(json => {
this.schemaJSON.set(url, json); this.schemaJSON.set(url, json);
let data = Services.ppmm.initialProcessData; let data = Services.ppmm.initialProcessData;
@ -1596,17 +1620,20 @@ this.Schemas = {
Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json}); Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json});
loadFromJSON(json); this.flushSchemas();
}); });
return result;
} }
if (this.loadedUrls.has(url)) { },
return;
}
this.loadedUrls.add(url);
let schema = this.schemaJSON.get(url); unload(url) {
loadFromJSON(schema); this.schemaJSON.delete(url);
let data = Services.ppmm.initialProcessData;
data["Extension:Schemas"] = this.schemaJSON;
Services.ppmm.broadcastAsyncMessage("Schema:Delete", {url});
this.flushSchemas();
}, },
inject(dest, wrapperFuncs) { inject(dest, wrapperFuncs) {

Просмотреть файл

@ -6,6 +6,7 @@
EXTRA_JS_MODULES += [ EXTRA_JS_MODULES += [
'Extension.jsm', 'Extension.jsm',
'ExtensionAPI.jsm',
'ExtensionContent.jsm', 'ExtensionContent.jsm',
'ExtensionManagement.jsm', 'ExtensionManagement.jsm',
'ExtensionStorage.jsm', 'ExtensionStorage.jsm',

Просмотреть файл

@ -0,0 +1,16 @@
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [
{
"type": "string",
"pattern": "^experiments(\\.\\w+)+$"
}
]
}
]
}
]

Просмотреть файл

@ -8,6 +8,7 @@ toolkit.jar:
content/extensions/schemas/cookies.json content/extensions/schemas/cookies.json
content/extensions/schemas/downloads.json content/extensions/schemas/downloads.json
content/extensions/schemas/events.json content/extensions/schemas/events.json
content/extensions/schemas/experiments.json
content/extensions/schemas/extension.json content/extensions/schemas/extension.json
content/extensions/schemas/extension_types.json content/extensions/schemas/extension_types.json
content/extensions/schemas/i18n.json content/extensions/schemas/i18n.json

Просмотреть файл

@ -0,0 +1,163 @@
"use strict";
/* globals browser */
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
function promiseAddonStartup() {
const {Management} = Cu.import("resource://gre/modules/Extension.jsm");
return new Promise(resolve => {
let listener = (extension) => {
Management.off("startup", listener);
resolve(extension);
};
Management.on("startup", listener);
});
}
add_task(function* setup() {
yield ExtensionTestUtils.startAddonManager();
});
add_task(function* test_experiments_api() {
let apiAddonFile = Extension.generateZipFile({
"install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest"
em:id="meh@experiments.addons.mozilla.org"
em:name="Meh Experiment"
em:type="256"
em:version="0.1"
em:description="Meh experiment"
em:creator="Mozilla">
<em:targetApplication>
<Description
em:id="xpcshell@tests.mozilla.org"
em:minVersion="48"
em:maxVersion="*"/>
</em:targetApplication>
</Description>
</RDF>
`,
"api.js": String.raw`
Components.utils.import("resource://gre/modules/Services.jsm");
Services.obs.notifyObservers(null, "webext-api-loaded", "");
class API extends ExtensionAPI {
getAPI(context) {
return {
meh: {
hello(text) {
Services.obs.notifyObservers(null, "webext-api-hello", text);
}
}
}
}
}
`,
"schema.json": [
{
"namespace": "meh",
"description": "All full of meh.",
"permissions": ["experiments.meh"],
"functions": [
{
"name": "hello",
"type": "function",
"description": "Hates you. This is all.",
"parameters": [
{"type": "string", "name": "text"},
],
},
],
},
],
});
let addonFile = Extension.generateXPI("meh@web.extension", {
manifest: {
permissions: ["experiments.meh"],
},
background() {
browser.meh.hello("Here I am");
},
});
let boringAddonFile = Extension.generateXPI("boring@web.extension", {
background() {
if (browser.meh) {
browser.meh.hello("Here I should not be");
}
},
});
do_register_cleanup(() => {
for (let file of [apiAddonFile, addonFile, boringAddonFile]) {
Services.obs.notifyObservers(file, "flush-cache-entry", null);
file.remove(false);
}
});
let resolveHello;
let observer = (subject, topic, data) => {
if (topic == "webext-api-loaded") {
ok(!!resolveHello, "Should not see API loaded until dependent extension loads");
} else if (topic == "webext-api-hello") {
resolveHello(data);
}
};
Services.obs.addObserver(observer, "webext-api-loaded", false);
Services.obs.addObserver(observer, "webext-api-hello", false);
do_register_cleanup(() => {
Services.obs.removeObserver(observer, "webext-api-loaded");
Services.obs.removeObserver(observer, "webext-api-hello");
});
// Install API add-on.
let apiAddon = yield AddonManager.installTemporaryAddon(apiAddonFile);
let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
ok(APIs.apis.has("meh"), "Should have meh API.");
// Install boring WebExtension add-on.
let boringAddon = yield AddonManager.installTemporaryAddon(boringAddonFile);
yield promiseAddonStartup();
// Install interesting WebExtension add-on.
let promise = new Promise(resolve => {
resolveHello = resolve;
});
let addon = yield AddonManager.installTemporaryAddon(addonFile);
yield promiseAddonStartup();
let hello = yield promise;
equal(hello, "Here I am", "Should get hello from add-on");
// Cleanup.
apiAddon.uninstall();
boringAddon.userDisabled = true;
yield new Promise(do_execute_soon);
equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed.");
addon.uninstall();
boringAddon.uninstall();
});

Просмотреть файл

@ -26,6 +26,8 @@ skip-if = os == "android"
skip-if = os == "android" skip-if = os == "android"
[test_ext_downloads_search.js] [test_ext_downloads_search.js]
skip-if = os == "android" skip-if = os == "android"
[test_ext_experiments.js]
skip-if = release_build
[test_ext_extension.js] [test_ext_extension.js]
[test_ext_idle.js] [test_ext_idle.js]
[test_ext_json_parser.js] [test_ext_json_parser.js]

Просмотреть файл

@ -0,0 +1,39 @@
/* 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";
Components.utils.import("resource://gre/modules/ExtensionManagement.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
var namespace;
var resource;
var resProto;
function install(data, reason) {
}
function startup(data, reason) {
namespace = data.id.replace(/@.*/, "");
resource = `extension-${namespace}-api`;
resProto = Services.io.getProtocolHandler("resource")
.QueryInterface(Components.interfaces.nsIResProtocolHandler);
resProto.setSubstitution(resource, data.resourceURI);
ExtensionManagement.registerAPI(
namespace,
`resource://${resource}/schema.json`,
`resource://${resource}/api.js`);
}
function shutdown(data, reason) {
resProto.setSubstitution(resource, null);
ExtensionManagement.unregisterAPI(namespace);
}
function uninstall(data, reason) {
}

Просмотреть файл

@ -224,10 +224,14 @@ const TYPES = {
experiment: 128, experiment: 128,
}; };
if (!AppConstants.RELEASE_BUILD)
TYPES.apiextension = 256;
// Some add-on types that we track internally are presented as other types // Some add-on types that we track internally are presented as other types
// externally // externally
const TYPE_ALIASES = { const TYPE_ALIASES = {
"webextension": "extension", "webextension": "extension",
"apiextension": "extension",
}; };
const CHROME_TYPES = new Set([ const CHROME_TYPES = new Set([
@ -241,12 +245,14 @@ const RESTARTLESS_TYPES = new Set([
"dictionary", "dictionary",
"experiment", "experiment",
"locale", "locale",
"apiextension",
]); ]);
const SIGNED_TYPES = new Set([ const SIGNED_TYPES = new Set([
"webextension", "webextension",
"extension", "extension",
"experiment", "experiment",
"apiextension",
]); ]);
// This is a random number array that can be used as "salt" when generating // This is a random number array that can be used as "salt" when generating
@ -949,6 +955,7 @@ var loadManifestFromWebManifest = Task.async(function*(aUri) {
addon.optionsURL = null; addon.optionsURL = null;
addon.optionsType = null; addon.optionsType = null;
addon.aboutURL = null; addon.aboutURL = null;
addon.dependencies = Object.freeze(Array.from(extension.dependencies));
if (manifest.options_ui) { if (manifest.options_ui) {
addon.optionsURL = extension.getURL(manifest.options_ui.page); addon.optionsURL = extension.getURL(manifest.options_ui.page);
@ -4711,6 +4718,8 @@ this.XPIProvider = {
uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js" uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
else if (aType == "webextension") else if (aType == "webextension")
uri = "resource://gre/modules/addons/WebExtensionBootstrap.js" uri = "resource://gre/modules/addons/WebExtensionBootstrap.js"
else if (aType == "apiextension")
uri = "resource://gre/modules/addons/APIExtensionBootstrap.js"
activeAddon.bootstrapScope = activeAddon.bootstrapScope =
new Cu.Sandbox(principal, { sandboxName: uri, new Cu.Sandbox(principal, { sandboxName: uri,

Просмотреть файл

@ -9,6 +9,7 @@ EXTRA_JS_MODULES.addons += [
'AddonRepository.jsm', 'AddonRepository.jsm',
'AddonRepository_SQLiteMigrator.jsm', 'AddonRepository_SQLiteMigrator.jsm',
'AddonUpdateChecker.jsm', 'AddonUpdateChecker.jsm',
'APIExtensionBootstrap.js',
'Content.js', 'Content.js',
'E10SAddonsRollout.jsm', 'E10SAddonsRollout.jsm',
'GMPProvider.jsm', 'GMPProvider.jsm',

Просмотреть файл

@ -2,6 +2,8 @@
* http://creativecommons.org/publicdomain/zero/1.0/ * http://creativecommons.org/publicdomain/zero/1.0/
*/ */
Components.utils.import("resource://gre/modules/AppConstants.jsm");
const ID = "webextension1@tests.mozilla.org"; const ID = "webextension1@tests.mozilla.org";
const PREF_SELECTED_LOCALE = "general.useragent.locale"; const PREF_SELECTED_LOCALE = "general.useragent.locale";
@ -295,3 +297,55 @@ add_task(function* test_options_ui() {
addon.uninstall(); addon.uninstall();
}); });
// Test that experiments permissions add the appropriate dependencies.
add_task(function* test_experiments_dependencies() {
if (AppConstants.RELEASE_BUILD)
// Experiments are not enabled on release builds.
return;
let addonFile = createTempWebExtensionFile({
id: "meh@experiment",
manifest: {
"permissions": ["experiments.meh"],
},
});
yield promiseInstallAllFiles([addonFile]);
let addon = yield new Promise(resolve => AddonManager.getAddonByID("meh@experiment", resolve));
deepEqual(addon.dependencies, ["meh@experiments.addons.mozilla.org"],
"Addon should have the expected dependencies");
equal(addon.appDisabled, true, "Add-on should be app disabled due to missing dependencies");
addon.uninstall();
});
// Test that experiments API extensions install correctly.
add_task(function* test_experiments_api() {
if (AppConstants.RELEASE_BUILD)
// Experiments are not enabled on release builds.
return;
const ID = "meh@experiments.addons.mozilla.org";
let addonFile = createTempXPIFile({
id: ID,
type: 256,
version: "0.1",
name: "Meh API",
});
yield promiseInstallAllFiles([addonFile]);
let addons = yield new Promise(resolve => AddonManager.getAddonsByTypes(["apiextension"], resolve));
let addon = addons.pop();
equal(addon.id, ID, "Add-on should be installed as an API extension");
addons = yield new Promise(resolve => AddonManager.getAddonsByTypes(["extension"], resolve));
equal(addons.pop().id, ID, "Add-on type should be aliased to extension");
addon.uninstall();
});