Bug 1197420 Part 4 Apply dynamic permission changes r=kmag
MozReview-Commit-ID: 6TdcUv1fHPh
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -31,16 +31,18 @@ Cu.import("resource://gre/modules/Servic
XPCOMUtils.defineLazyPreferenceGetter(this, "processCount", "dom.ipc.processCount.extension");
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, "ExtensionPermissions",
+ "resource://gre/modules/ExtensionPermissions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
"resource://testing-common/ExtensionTestCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
@@ -627,17 +629,17 @@ this.ExtensionData = class {
this.localeData.selectedLocale = locale;
return results[0];
}.bind(this));
}
};
let _browserUpdated = false;
-const PROXIED_EVENTS = new Set(["test-harness-message"]);
+const PROXIED_EVENTS = new Set(["test-harness-message", "add-permissions", "remove-permissions"]);
// We create one instance of this class per extension. |addonData|
// comes directly from bootstrap.js when initializing.
this.Extension = class extends ExtensionData {
constructor(addonData, startupReason) {
super(addonData.resourceURI);
this.uuid = UUIDMap.get(addonData.id);
@@ -679,16 +681,38 @@ this.Extension = class extends Extension
this.uninstallURL = null;
this.apis = [];
this.whiteListedHosts = null;
this._optionalOrigins = null;
this.webAccessibleResources = null;
this.emitter = new EventEmitter();
+
+ /* eslint-disable mozilla/balanced-listeners */
+ this.on("add-permissions", (ignoreEvent, permissions) => {
+ for (let perm of permissions.permissions) {
+ this.permissions.add(perm);
+ }
+
+ if (permissions.origins.length > 0) {
+ this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
+ }
+ });
+
+ this.on("remove-permissions", (ignoreEvent, permissions) => {
+ for (let perm of permissions.permissions) {
+ this.permissions.delete(perm);
+ }
+
+ for (let origin of permissions.origins) {
+ this.whiteListedHosts.removeOne(origin);
+ }
+ });
+ /* eslint-enable mozilla/balanced-listeners */
}
static set browserUpdated(updated) {
_browserUpdated = updated;
}
static get browserUpdated() {
return _browserUpdated;
@@ -792,16 +816,17 @@ this.Extension = class extends Extension
resourceURL: this.addonData.resourceURI.spec,
baseURL: this.baseURI.spec,
content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
webAccessibleResources: this.webAccessibleResources.serialize(),
whiteListedHosts: this.whiteListedHosts.serialize(),
localeData: this.localeData.serialize(),
permissions: this.permissions,
principal: this.principal,
+ optionalPermissions: this.manifest.optional_permissions,
};
}
broadcast(msg, data) {
return new Promise(resolve => {
let {ppmm} = Services;
let children = new Set();
for (let i = 0; i < ppmm.childCount; i++) {
@@ -889,58 +914,69 @@ this.Extension = class extends Extension
let match = Locale.findClosestLocale(localeList);
locale = match ? match.name : this.defaultLocale;
}
return super.initLocale(locale);
}
- startup() {
+ async startup() {
let started = false;
- return this.loadManifest().then(() => {
+
+ try {
+ let [, perms] = await Promise.all([this.loadManifest(), ExtensionPermissions.get(this)]);
+
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
started = true;
if (!this.hasShutdown) {
- return this.initLocale();
+ await this.initLocale();
}
- }).then(() => {
+
if (this.errors.length) {
return Promise.reject({errors: this.errors});
}
if (this.hasShutdown) {
return;
}
GlobalManager.init(this);
+ // Apply optional permissions
+ for (let perm of perms.permissions) {
+ this.permissions.add(perm);
+ }
+ if (perms.origins.length > 0) {
+ this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...perms.origins));
+ }
+
// The "startup" Management event sent on the extension instance itself
// is emitted just before the Management "startup" event,
// and it is used to run code that needs to be executed before
// any of the "startup" listeners.
this.emit("startup", this);
Management.emit("startup", this);
- return this.runManifest(this.manifest);
- }).then(() => {
+ await this.runManifest(this.manifest);
+
Management.emit("ready", this);
- }).catch(e => {
+ } catch (e) {
dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
Cu.reportError(e);
if (started) {
ExtensionManagement.shutdownExtension(this.uuid);
}
this.cleanupGeneratedFile();
throw e;
- });
+ }
}
cleanupGeneratedFile() {
if (!this.cleanupFile) {
return;
}
let file = this.cleanupFile;
@@ -1002,23 +1038,31 @@ this.Extension = class extends Extension
}
observe(subject, topic, data) {
if (topic === "xpcom-shutdown") {
this.cleanupGeneratedFile();
}
}
- hasPermission(perm) {
+ hasPermission(perm, includeOptional = false) {
let match = /^manifest:(.*)/.exec(perm);
if (match) {
return this.manifest[match[1]] != null;
}
- return this.permissions.has(perm);
+ if (this.permissions.has(perm)) {
+ return true;
+ }
+
+ if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
+ return true;
+ }
+
+ return false;
}
get name() {
return this.manifest.name;
}
get optionalOrigins() {
if (this._optionalOrigins == null) {
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -622,16 +622,32 @@ class ChildAPIManager {
let params = {
childId: this.id,
extensionId: context.extension.id,
principal: context.principal,
};
Object.assign(params, contextData);
this.messageManager.sendAsyncMessage("API:CreateProxyContext", params);
+
+ this.permissionsChangedCallbacks = new Set();
+ this.updatePermissions = null;
+ if (this.context.extension.optionalPermissions.length > 0) {
+ this.updatePermissions = () => {
+ for (let callback of this.permissionsChangedCallbacks) {
+ try {
+ callback();
+ } catch (err) {
+ Cu.reportError(err);
+ }
+ }
+ };
+ this.context.extension.on("add-permissions", this.updatePermissions);
+ this.context.extension.on("remove-permissions", this.updatePermissions);
+ }
}
receiveMessage({name, messageName, data}) {
if (data.childId != this.id) {
return;
}
switch (name || messageName) {
@@ -721,16 +737,20 @@ class ChildAPIManager {
addListener: (listener, ...args) => impl.addListener(listener, args),
removeListener: (listener) => impl.removeListener(listener),
hasListener: (listener) => impl.hasListener(listener),
};
}
close() {
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
+ if (this.updatePermissions) {
+ this.context.extension.off("add-permissions", this.updatePermissions);
+ this.context.extension.off("remove-permissions", this.updatePermissions);
+ }
}
get cloneScope() {
return this.context.cloneScope;
}
get principal() {
return this.context.principal;
@@ -776,16 +796,24 @@ class ChildAPIManager {
getFallbackImplementation(namespace, name) {
// No local API found, defer implementation to the parent.
return new ProxyAPIImplementation(namespace, name, this);
}
hasPermission(permission) {
return this.context.extension.hasPermission(permission);
}
+
+ isPermissionRevokable(permission) {
+ return this.context.extension.optionalPermissions.includes(permission);
+ }
+
+ setPermissionsChangedCallback(callback) {
+ this.permissionsChangedCallbacks.add(callback);
+ }
}
class ExtensionBaseContextChild extends BaseContext {
/**
* This ExtensionBaseContextChild represents an addon execution environment
* that is running in an addon or devtools child process.
*
* @param {BrowserExtensionContent} extension This context's owner.
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -684,18 +684,21 @@ class SchemaAPIManager extends EventEmit
}
copy(dest[prop], source[prop]);
} else {
Object.defineProperty(dest, prop, desc);
}
}
}
+ function hasPermission(perm) {
+ return context.extension.hasPermission(perm, true);
+ }
for (let api of apis) {
- if (Schemas.checkPermissions(api.namespace, context.extension)) {
+ if (Schemas.checkPermissions(api.namespace, {hasPermission})) {
api = api.getAPI(context);
copy(obj, api);
}
}
}
}
const ExtensionCommon = {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -986,16 +986,17 @@ class BrowserExtensionContent extends Ev
defineLazyGetter(this, "scripts", () => {
return data.content_scripts.map(scriptData => new Script(this, scriptData));
});
this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
this.permissions = data.permissions;
+ this.optionalPermissions = data.optionalPermissions;
this.principal = data.principal;
this.localeData = new LocaleData(data.localeData);
this.manifest = data.manifest;
this.baseURI = Services.io.newURI(data.baseURL);
// Only used in addon processes.
@@ -1005,16 +1006,44 @@ class BrowserExtensionContent extends Ev
this.devtoolsViews = new Set();
let uri = Services.io.newURI(data.resourceURL);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
// Extension.jsm takes care of this in the parent.
ExtensionManagement.startupExtension(this.uuid, uri, this);
}
+
+ /* eslint-disable mozilla/balanced-listeners */
+ this.on("add-permissions", (ignoreEvent, permissions) => {
+ if (permissions.permissions.length > 0) {
+ for (let perm of permissions.permissions) {
+ this.permissions.add(perm);
+ }
+ }
+
+ if (permissions.origins.length > 0) {
+ this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
+ }
+ });
+
+ this.on("remove-permissions", (ignoreEvent, permissions) => {
+ if (permissions.permissions.length > 0) {
+ for (let perm of permissions.permissions) {
+ this.permissions.delete(perm);
+ }
+ }
+
+ if (permissions.origins.length > 0) {
+ for (let origin of permissions.origins) {
+ this.whiteListedHosts.removeOne(origin);
+ }
+ }
+ });
+ /* eslint-enable mozilla/balanced-listeners */
}
shutdown() {
Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
ExtensionManagement.shutdownExtension(this.uuid);
}
--- a/toolkit/components/extensions/ExtensionPermissions.jsm
+++ b/toolkit/components/extensions/ExtensionPermissions.jsm
@@ -61,17 +61,17 @@ this.ExtensionPermissions = {
if (!origins.includes(origin)) {
added.origins.push(origin);
origins.push(origin);
}
}
if (added.permissions.length > 0 || added.origins.length > 0) {
prefs.saveSoon();
- // TODO apply the changes
+ extension.emit("add-permissions", added);
}
},
// Revoke permissions from the given extension. `permissions` is
// in the format that is passed to browser.permissions.remove().
async remove(extension, perms) {
await lazyInit();
@@ -94,17 +94,17 @@ this.ExtensionPermissions = {
if (i >= 0) {
removed.origins.push(origin);
origins.splice(i, 1);
}
}
if (removed.permissions.length > 0 || removed.origins.length > 0) {
prefs.saveSoon();
- // TODO apply the changes
+ extension.emit("remove-permissions", removed);
}
},
async removeAll(extension) {
await lazyInit();
delete prefs.data[extension.id];
prefs.saveSoon();
},
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -177,17 +177,18 @@
]
},
"optional": true
},
"optional_permissions": {
"type": "array",
"items": { "$ref": "OptionalPermission" },
- "optional": true
+ "optional": true,
+ "default": []
},
"web_accessible_resources": {
"type": "array",
"items": { "type": "string" },
"optional": true
},
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -194,16 +194,31 @@ MatchPattern.prototype = {
}
return this.matchesIgnoringPath({scheme: match[1], host: match[2]});
},
serialize() {
return this.pat;
},
+
+ removeOne(pattern) {
+ if (!Array.isArray(this.pat)) {
+ return;
+ }
+
+ let index = this.pat.indexOf(pattern);
+ if (index >= 0) {
+ if (this.matchers[index].pat != pattern) {
+ throw new Error("pat/matcher mismatch in removeOne()");
+ }
+ this.pat.splice(index, 1);
+ this.matchers.splice(index, 1);
+ }
+ },
};
// Globs can match everything. Be careful, this DOES NOT filter by allowed schemes!
this.MatchGlobs = function(globs) {
this.original = globs;
if (globs) {
this.regexps = Array.from(globs, (glob) => globToRegexp(glob, true));
} else {