зеркало из 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/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();
|
||||||
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче