зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1263011: Part 2 - Implement WebExtensions Experiments prototype. r=aswan
MozReview-Commit-ID: 4KO4cCLRsLf --HG-- extra : rebase_source : 40e5ec808e557e845a771bb21e8863a8edcd4faf
This commit is contained in:
Родитель
b022382989
Коммит
73b1b5f221
|
@ -26,6 +26,14 @@ Cu.importGlobalProperties(["TextEncoder"]);
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.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",
|
||||
"resource://gre/modules/Locale.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
||||
|
@ -34,10 +42,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
|
|||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
|
@ -48,12 +56,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
|||
"resource://gre/modules/Schemas.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"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", () => {
|
||||
let obj = {};
|
||||
|
@ -68,6 +70,12 @@ const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
|
|||
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
|
||||
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");
|
||||
var {
|
||||
BaseContext,
|
||||
|
@ -115,8 +123,11 @@ var Management = {
|
|||
// extended by other schemas, so needs to be loaded first.
|
||||
let promise = Schemas.load(BASE_SCHEMA).then(() => {
|
||||
let promises = [];
|
||||
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
|
||||
promises.push(Schemas.load(value));
|
||||
for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
|
||||
promises.push(Schemas.load(url));
|
||||
}
|
||||
for (let url of schemaURLs) {
|
||||
promises.push(Schemas.load(url));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
});
|
||||
|
@ -196,6 +207,10 @@ var Management = {
|
|||
copy(obj, api);
|
||||
}
|
||||
|
||||
for (let api of extension.apis) {
|
||||
copy(obj, api.getAPI(context));
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
|
@ -741,6 +756,10 @@ this.ExtensionData = class {
|
|||
this.localeData = null;
|
||||
this._promiseLocales = null;
|
||||
|
||||
this.apiNames = new Set();
|
||||
this.dependencies = new Set();
|
||||
this.permissions = new Set();
|
||||
|
||||
this.errors = [];
|
||||
}
|
||||
|
||||
|
@ -923,6 +942,25 @@ this.ExtensionData = class {
|
|||
// 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;
|
||||
});
|
||||
}
|
||||
|
@ -1173,7 +1211,7 @@ this.Extension = class extends ExtensionData {
|
|||
|
||||
this.uninstallURL = null;
|
||||
|
||||
this.permissions = new Set();
|
||||
this.apis = [];
|
||||
this.whiteListedHosts = null;
|
||||
this.webAccessibleResources = null;
|
||||
|
||||
|
@ -1249,10 +1287,14 @@ this.Extension = class extends ExtensionData {
|
|||
|
||||
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 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);
|
||||
|
||||
const MODE_WRONLY = 0x02;
|
||||
|
@ -1277,7 +1319,7 @@ this.Extension = class extends ExtensionData {
|
|||
let script = files[filename];
|
||||
if (typeof(script) == "function") {
|
||||
script = "(" + script.toString() + ")()";
|
||||
} else if (instanceOf(script, "Object")) {
|
||||
} else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
|
||||
script = JSON.stringify(script);
|
||||
}
|
||||
|
||||
|
@ -1355,6 +1397,25 @@ this.Extension = class extends ExtensionData {
|
|||
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
|
||||
// processes. This should include anything the content process might
|
||||
// need.
|
||||
|
@ -1388,17 +1449,6 @@ this.Extension = class extends ExtensionData {
|
|||
}
|
||||
|
||||
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.
|
||||
let strippedWebAccessibleResources = [];
|
||||
if (manifest.web_accessible_resources) {
|
||||
|
@ -1544,6 +1594,10 @@ this.Extension = class extends ExtensionData {
|
|||
obj.close();
|
||||
}
|
||||
|
||||
for (let api of this.apis) {
|
||||
api.destroy();
|
||||
}
|
||||
|
||||
Management.emit("shutdown", this);
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
// moz-extension:// URIs. It lives here so that it can be used in both
|
||||
// the parent and child processes.
|
||||
|
@ -274,6 +294,9 @@ this.ExtensionManagement = {
|
|||
startupExtension: Service.startupExtension.bind(Service),
|
||||
shutdownExtension: Service.shutdownExtension.bind(Service),
|
||||
|
||||
registerAPI: APIs.register.bind(APIs),
|
||||
unregisterAPI: APIs.unregister.bind(APIs),
|
||||
|
||||
getFrameId: Frames.getId.bind(Frames),
|
||||
getParentFrameId: Frames.getParentId.bind(Frames),
|
||||
|
||||
|
@ -281,4 +304,6 @@ this.ExtensionManagement = {
|
|||
getAddonIdForWindow,
|
||||
getAPILevelForWindow,
|
||||
API_LEVELS,
|
||||
|
||||
APIs,
|
||||
};
|
||||
|
|
|
@ -1247,9 +1247,6 @@ class Event extends CallEntry {
|
|||
this.Schemas = {
|
||||
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
|
||||
// is useful for sending the JSON across processes.
|
||||
schemaJSON: new Map(),
|
||||
|
@ -1545,50 +1542,77 @@ this.Schemas = {
|
|||
}
|
||||
Services.cpmm.addMessageListener("Schema:Add", this);
|
||||
}
|
||||
|
||||
this.flushSchemas();
|
||||
},
|
||||
|
||||
receiveMessage(msg) {
|
||||
switch (msg.name) {
|
||||
case "Schema:Add":
|
||||
this.schemaJSON.set(msg.data.url, msg.data.schema);
|
||||
this.flushSchemas();
|
||||
break;
|
||||
|
||||
case "Schema:Delete":
|
||||
this.schemaJSON.delete(msg.data.url);
|
||||
this.flushSchemas();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
load(url) {
|
||||
let loadFromJSON = json => {
|
||||
for (let namespace of json) {
|
||||
let name = namespace.namespace;
|
||||
flushSchemas() {
|
||||
XPCOMUtils.defineLazyGetter(this, "namespaces",
|
||||
() => this.parseSchemas());
|
||||
},
|
||||
|
||||
let types = namespace.types || [];
|
||||
for (let type of types) {
|
||||
this.loadType(name, type);
|
||||
}
|
||||
parseSchemas() {
|
||||
Object.defineProperty(this, "namespaces", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: new Map(),
|
||||
});
|
||||
|
||||
let properties = namespace.properties || {};
|
||||
for (let propertyName of Object.keys(properties)) {
|
||||
this.loadProperty(name, propertyName, properties[propertyName]);
|
||||
}
|
||||
for (let json of this.schemaJSON.values()) {
|
||||
this.parseSchema(json);
|
||||
}
|
||||
|
||||
let functions = namespace.functions || [];
|
||||
for (let fun of functions) {
|
||||
this.loadFunction(name, fun);
|
||||
}
|
||||
return this.namespaces;
|
||||
},
|
||||
|
||||
let events = namespace.events || [];
|
||||
for (let event of events) {
|
||||
this.loadEvent(name, event);
|
||||
}
|
||||
parseSchema(json) {
|
||||
for (let namespace of json) {
|
||||
let name = namespace.namespace;
|
||||
|
||||
if (namespace.permissions) {
|
||||
let ns = this.namespaces.get(name);
|
||||
ns.permissions = namespace.permissions;
|
||||
}
|
||||
let types = namespace.types || [];
|
||||
for (let type of types) {
|
||||
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) {
|
||||
let result = readJSON(url).then(json => {
|
||||
return readJSON(url).then(json => {
|
||||
this.schemaJSON.set(url, json);
|
||||
|
||||
let data = Services.ppmm.initialProcessData;
|
||||
|
@ -1596,17 +1620,20 @@ this.Schemas = {
|
|||
|
||||
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);
|
||||
loadFromJSON(schema);
|
||||
unload(url) {
|
||||
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) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
EXTRA_JS_MODULES += [
|
||||
'Extension.jsm',
|
||||
'ExtensionAPI.jsm',
|
||||
'ExtensionContent.jsm',
|
||||
'ExtensionManagement.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/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/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"
|
||||
[test_ext_downloads_search.js]
|
||||
skip-if = os == "android"
|
||||
[test_ext_experiments.js]
|
||||
skip-if = release_build
|
||||
[test_ext_extension.js]
|
||||
[test_ext_idle.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,
|
||||
};
|
||||
|
||||
if (!AppConstants.RELEASE_BUILD)
|
||||
TYPES.apiextension = 256;
|
||||
|
||||
// Some add-on types that we track internally are presented as other types
|
||||
// externally
|
||||
const TYPE_ALIASES = {
|
||||
"webextension": "extension",
|
||||
"apiextension": "extension",
|
||||
};
|
||||
|
||||
const CHROME_TYPES = new Set([
|
||||
|
@ -241,12 +245,14 @@ const RESTARTLESS_TYPES = new Set([
|
|||
"dictionary",
|
||||
"experiment",
|
||||
"locale",
|
||||
"apiextension",
|
||||
]);
|
||||
|
||||
const SIGNED_TYPES = new Set([
|
||||
"webextension",
|
||||
"extension",
|
||||
"experiment",
|
||||
"apiextension",
|
||||
]);
|
||||
|
||||
// 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.optionsType = null;
|
||||
addon.aboutURL = null;
|
||||
addon.dependencies = Object.freeze(Array.from(extension.dependencies));
|
||||
|
||||
if (manifest.options_ui) {
|
||||
addon.optionsURL = extension.getURL(manifest.options_ui.page);
|
||||
|
@ -4711,6 +4718,8 @@ this.XPIProvider = {
|
|||
uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
|
||||
else if (aType == "webextension")
|
||||
uri = "resource://gre/modules/addons/WebExtensionBootstrap.js"
|
||||
else if (aType == "apiextension")
|
||||
uri = "resource://gre/modules/addons/APIExtensionBootstrap.js"
|
||||
|
||||
activeAddon.bootstrapScope =
|
||||
new Cu.Sandbox(principal, { sandboxName: uri,
|
||||
|
|
|
@ -9,6 +9,7 @@ EXTRA_JS_MODULES.addons += [
|
|||
'AddonRepository.jsm',
|
||||
'AddonRepository_SQLiteMigrator.jsm',
|
||||
'AddonUpdateChecker.jsm',
|
||||
'APIExtensionBootstrap.js',
|
||||
'Content.js',
|
||||
'E10SAddonsRollout.jsm',
|
||||
'GMPProvider.jsm',
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
Components.utils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
const ID = "webextension1@tests.mozilla.org";
|
||||
|
||||
const PREF_SELECTED_LOCALE = "general.useragent.locale";
|
||||
|
@ -295,3 +297,55 @@ add_task(function* test_options_ui() {
|
|||
|
||||
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();
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче