Bug 1197420 Part 3 Initial browser.permissions api support r=kmag

With this patch, permissions are not actually applied,
but the permissions api is in place.

MozReview-Commit-ID: CTaXz5sa1xy

--HG--
extra : rebase_source : d5cc18abbae6809b196f8497ff91608d662d5030
extra : source : e4c13d11e401ae3bd40be3a5a7fb0aaf95c992bb
This commit is contained in:
Andrew Swan 2017-03-24 13:55:09 -07:00
parent 3656a695a2
commit 1fa2f9e217
14 changed files with 475 additions and 8 deletions

View File

@ -84,6 +84,17 @@ webextPerms.updateText=%S has been updated. You must approve new permissions bef
webextPerms.updateAccept.label=Update
webextPerms.updateAccept.accessKey=U
# LOCALIZATION NOTE (webextPerms.optionalPermsHheader)
# %S is replace with the localized name of the extension requested new
# permissions.
# Note, this string will be used as raw markup. Avoid characters like <, >, &
webextPerms.optionalPermsHeader=%S requests additional permissions.
webextPerms.optionalPermsListIntro=It wants to:
webextPerms.optionalPermsAllow.label=Allow
webextPerms.optionalPermsAllow.accessKey=A
webextPerms.optionalPermsDeny.label=Deny
webextPerms.optionalPermsDeny.accessKey=D
webextPerms.description.bookmarks=Read and modify bookmarks
webextPerms.description.clipboardRead=Get data from the clipboard
webextPerms.description.clipboardWrite=Input data to the clipboard

View File

@ -41,6 +41,7 @@ this.ExtensionsUI = {
Services.obs.addObserver(this, "webextension-permission-prompt", false);
Services.obs.addObserver(this, "webextension-update-permissions", false);
Services.obs.addObserver(this, "webextension-install-notify", false);
Services.obs.addObserver(this, "webextension-optional-permission-prompt", false);
this._checkForSideloaded();
},
@ -203,6 +204,14 @@ this.ExtensionsUI = {
callback();
}
});
} else if (topic == "webextension-optional-permission-prompt") {
let {browser, name, icon, permissions, resolve} = subject.wrappedJSObject;
let strings = this._buildStrings({
type: "optional",
addon: {name},
permissions,
});
resolve(this.showPermissionsPrompt(browser, strings, icon));
}
},

View File

@ -81,6 +81,7 @@ var {
} = ExtensionParent;
const {
classifyPermission,
EventEmitter,
LocaleData,
StartupCache,
@ -482,12 +483,11 @@ this.ExtensionData = class {
}
this.permissions.add(perm);
let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
if (!match) {
let type = classifyPermission(perm);
if (type.origin) {
whitelist.push(perm);
} else if (match[1] == "experiments" && match[2]) {
this.apiNames.add(match[2]);
} else if (type.api) {
this.apiNames.add(type.api);
}
}
this.whiteListedHosts = new MatchPattern(whitelist);
@ -680,6 +680,7 @@ this.Extension = class extends ExtensionData {
this.apis = [];
this.whiteListedHosts = null;
this._optionalOrigins = null;
this.webAccessibleResources = null;
this.emitter = new EventEmitter();
@ -1018,4 +1019,12 @@ this.Extension = class extends ExtensionData {
get name() {
return this.manifest.name;
}
get optionalOrigins() {
if (this._optionalOrigins == null) {
let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin);
this._optionalOrigins = new MatchPattern(origins);
}
return this._optionalOrigins;
}
};

View File

@ -0,0 +1,111 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
"resource://gre/modules/JSONFile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
this.EXPORTED_SYMBOLS = ["ExtensionPermissions"];
const FILE_NAME = "extension-preferences.json";
let prefs;
let _initPromise;
function lazyInit() {
if (!_initPromise) {
prefs = new JSONFile({path: OS.Path.join(OS.Constants.Path.profileDir, FILE_NAME)});
_initPromise = prefs.load();
}
return _initPromise;
}
function emptyPermissions() {
return {permissions: [], origins: []};
}
this.ExtensionPermissions = {
async get(extension) {
await lazyInit();
let perms = emptyPermissions();
if (prefs.data[extension.id]) {
Object.assign(perms, prefs.data[extension.id]);
}
return perms;
},
// Add new permissions for the given extension. `permissions` is
// in the format that is passed to browser.permissions.request().
async add(extension, perms) {
await lazyInit();
if (!prefs.data[extension.id]) {
prefs.data[extension.id] = emptyPermissions();
}
let {permissions, origins} = prefs.data[extension.id];
let added = emptyPermissions();
for (let perm of perms.permissions) {
if (!permissions.includes(perm)) {
added.permissions.push(perm);
permissions.push(perm);
}
}
for (let origin of perms.origins) {
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
}
},
// Revoke permissions from the given extension. `permissions` is
// in the format that is passed to browser.permissions.remove().
async remove(extension, perms) {
await lazyInit();
if (!prefs.data[extension.id]) {
return;
}
let {permissions, origins} = prefs.data[extension.id];
let removed = emptyPermissions();
for (let perm of perms.permissions) {
let i = permissions.indexOf(perm);
if (i >= 0) {
removed.permissions.push(perm);
permissions.splice(i, 1);
}
}
for (let origin of perms.origins) {
let i = origins.indexOf(origin);
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
}
},
async removeAll(extension) {
await lazyInit();
delete prefs.data[extension.id];
prefs.saveSoon();
},
};

View File

@ -1328,7 +1328,31 @@ class MessageManagerProxy {
}
}
/**
* Classify an individual permission from a webextension manifest
* as a host/origin permission, an api permission, or a regular permission.
*
* @param {string} perm The permission string to classify
*
* @returns {object}
* An object with exactly one of the following properties:
* "origin" to indicate this is a host/origin permission.
* "api" to indicate this is an api permission
* (as used for webextensions experiments).
* "permission" to indicate this is a regular permission.
*/
function classifyPermission(perm) {
let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
if (!match) {
return {origin: perm};
} else if (match[1] == "experiments" && match[2]) {
return {api: match[2]};
}
return {permission: perm};
}
this.ExtensionUtils = {
classifyPermission,
defineLazyGetter,
detectLanguage,
extend,

View File

@ -0,0 +1,19 @@
"use strict";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {ExtensionError} = ExtensionUtils;
extensions.registerSchemaAPI("permissions", "addon_child", context => {
return {
permissions: {
async request(perms) {
let winUtils = context.contentWindow.getInterface(Ci.nsIDOMWindowUtils);
if (!winUtils.isHandlingUserInput) {
throw new ExtensionError("May only request permissions from a user input handler");
}
return context.childManager.callParentAsyncFunction("permissions.request_parent", [perms]);
},
},
};
});

View File

@ -0,0 +1,97 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPermissions",
"resource://gre/modules/ExtensionPermissions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const {
ExtensionError,
} = ExtensionUtils;
XPCOMUtils.defineLazyPreferenceGetter(this, "promptsEnabled",
"extensions.webextOptionalPermissionPrompts");
extensions.registerSchemaAPI("permission", "addon_parent", context => {
return {
permissions: {
async request_parent(perms) {
let {permissions, origins} = perms;
let manifestPermissions = context.extension.manifest.optional_permissions;
for (let perm of permissions) {
if (!manifestPermissions.includes(perm)) {
throw new ExtensionError(`Cannot request permission ${perm} since it was not declared in optional_permissions`);
}
}
let optionalOrigins = context.extension.optionalOrigins;
for (let origin of origins) {
if (!optionalOrigins.subsumes(origin)) {
throw new ExtensionError(`Cannot request origin permission for ${origin} since it was not declared in optional_permissions`);
}
}
if (promptsEnabled) {
let allow = await new Promise(resolve => {
let subject = {
wrappedJSObject: {
browser: context.xulBrowser,
name: context.extension.name,
icon: context.extension.iconURL,
permissions: {permissions, origins},
resolve,
},
};
Services.obs.notifyObservers(subject, "webextension-optional-permission-prompt", null);
});
if (!allow) {
return false;
}
}
await ExtensionPermissions.add(context.extension, perms);
return true;
},
async getAll() {
let perms = context.extension.userPermissions;
delete perms.apis;
return perms;
},
async contains(permissions) {
for (let perm of permissions.permissions) {
if (!context.extension.hasPermission(perm)) {
return false;
}
}
for (let origin of permissions.origins) {
if (!context.extension.whiteListedHosts.subsumes(origin)) {
return false;
}
}
return true;
},
async remove(permissions) {
await ExtensionPermissions.remove(context.extension, permissions);
return true;
},
},
};
});
/* eslint-disable mozilla/balanced-listeners */
extensions.on("uninstall", extension => {
ExtensionPermissions.removeAll(extension);
});

View File

@ -11,6 +11,7 @@ category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
category webextension-scripts idle chrome://extensions/content/ext-idle.js
category webextension-scripts management chrome://extensions/content/ext-management.js
category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
category webextension-scripts permissions chrome://extensions/content/ext-permissions.js
category webextension-scripts privacy chrome://extensions/content/ext-privacy.js
category webextension-scripts proxy chrome://extensions/content/ext-proxy.js
category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
@ -23,6 +24,7 @@ category webextension-scripts webRequest chrome://extensions/content/ext-webRequ
# scripts specific for content process.
category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js
category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js
category webextension-scripts-content permissions chrome://extensions/content/ext-c-permissions.js
category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js
category webextension-scripts-content test chrome://extensions/content/ext-c-test.js
@ -41,6 +43,7 @@ category webextension-scripts-addon i18n chrome://extensions/content/ext-i18n.js
#ifndef ANDROID
category webextension-scripts-addon identity chrome://extensions/content/ext-c-identity.js
#endif
category webextension-scripts-addon permissions chrome://extensions/content/ext-c-permissions.js
category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
@ -62,6 +65,7 @@ category webextension-schemas idle chrome://extensions/content/schemas/idle.json
category webextension-schemas management chrome://extensions/content/schemas/management.json
category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
category webextension-schemas permissions chrome://extensions/content/schemas/permissions.json
category webextension-schemas privacy chrome://extensions/content/schemas/privacy.json
category webextension-schemas proxy chrome://extensions/content/schemas/proxy.json
category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json

View File

@ -16,6 +16,7 @@ toolkit.jar:
content/extensions/ext-idle.js
content/extensions/ext-management.js
content/extensions/ext-notifications.js
content/extensions/ext-permissions.js
content/extensions/ext-privacy.js
content/extensions/ext-protocolHandlers.js
content/extensions/ext-proxy.js
@ -32,6 +33,7 @@ toolkit.jar:
#ifndef ANDROID
content/extensions/ext-c-identity.js
#endif
content/extensions/ext-c-permissions.js
content/extensions/ext-c-runtime.js
content/extensions/ext-c-storage.js
content/extensions/ext-c-test.js

View File

@ -12,6 +12,7 @@ EXTRA_JS_MODULES += [
'ExtensionContent.jsm',
'ExtensionManagement.jsm',
'ExtensionParent.jsm',
'ExtensionPermissions.jsm',
'ExtensionPreferencesManager.jsm',
'ExtensionSettingsStore.jsm',
'ExtensionStorage.jsm',

View File

@ -22,6 +22,7 @@ toolkit.jar:
content/extensions/schemas/manifest.json
content/extensions/schemas/native_host_manifest.json
content/extensions/schemas/notifications.json
content/extensions/schemas/permissions.json
content/extensions/schemas/proxy.json
content/extensions/schemas/privacy.json
content/extensions/schemas/runtime.json

View File

@ -0,0 +1,153 @@
[
{
"namespace": "permissions",
"permissions": ["manifest:optional_permissions"],
"types": [
{
"id": "Permissions",
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": { "$ref": "manifest.OptionalPermission" },
"optional": true,
"default": []
},
"origins": {
"type": "array",
"items": { "$ref": "manifest.MatchPattern" },
"optional": true,
"default": []
}
}
},
{
"id": "AnyPermissions",
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": { "$ref": "manifest.Permission" },
"optional": true,
"default": []
},
"origins": {
"type": "array",
"items": { "$ref": "manifest.MatchPattern" },
"optional": true,
"default": []
}
}
}
],
"functions": [
{
"name": "getAll",
"type": "function",
"async": "callback",
"description": "Get a list of all the extension's permissions.",
"parameters": [
{
"name": "callback",
"type": "function",
"parameters": [
{
"name": "permissions",
"$ref": "AnyPermissions"
}
]
}
]
},
{
"name": "contains",
"type": "function",
"async": "callback",
"description": "Check if the extension has the given permissions.",
"parameters": [
{
"name": "permissions",
"$ref": "AnyPermissions"
},
{
"name": "callback",
"type": "function",
"parameters": [
{
"name": "result",
"type": "boolean"
}
]
}
]
},
{
"name": "request",
"type": "function",
"allowedContexts": ["content"],
"async": "callback",
"description": "Request the given permissions.",
"parameters": [
{
"name": "permissions",
"$ref": "Permissions"
},
{
"name": "callback",
"type": "function",
"parameters": [
{
"name": "granted",
"type": "boolean"
}
]
}
]
},
{
"name": "remove",
"type": "function",
"async": "callback",
"description": "Relinquish the given permissions.",
"parameters": [
{
"name": "permissions",
"$ref": "Permissions"
},
{
"name": "callback",
"type": "function",
"parameters": [
]
}
]
}
],
"events": [
{
"name": "onAdded",
"type": "function",
"unsupported": true,
"description": "Fired when the extension acquires new permissions.",
"parameters": [
{
"name": "permissions",
"$ref": "Permissions"
}
]
},
{
"name": "onRemoved",
"type": "function",
"unsupported": true,
"description": "Fired when permissions are removed from the extension.",
"parameters": [
{
"name": "permissions",
"$ref": "Permissions"
}
]
}
]
}
]

View File

@ -73,6 +73,10 @@ let expectedBackgroundApis = [
"management.ExtensionType",
"management.getSelf",
"management.uninstallSelf",
"permissions.getAll",
"permissions.contains",
"permissions.request",
"permissions.remove",
"runtime.getBackgroundPage",
"runtime.getBrowserInfo",
"runtime.getPlatformInfo",

View File

@ -21,6 +21,12 @@ this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"];
const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "data"];
const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
// The basic RE for matching patterns
const PATTERN_REGEXP = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
// The schemes/protocols implied by a pattern that starts with *://
const WILDCARD_SCHEMES = ["http", "https"];
// This function converts a glob pattern (containing * and possibly ?
// as wildcards) to a regular expression.
function globToRegexp(pat, allowQuestion) {
@ -47,8 +53,7 @@ function SingleMatchPattern(pat) {
} else if (!pat) {
this.schemes = [];
} else {
let re = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
let match = re.exec(pat);
let match = PATTERN_REGEXP.exec(pat);
if (!match) {
Cu.reportError(`Invalid match pattern: '${pat}'`);
this.schemes = [];
@ -56,7 +61,7 @@ function SingleMatchPattern(pat) {
}
if (match[1] == "*") {
this.schemes = ["http", "https"];
this.schemes = WILDCARD_SCHEMES;
} else {
this.schemes = [match[1]];
}
@ -174,6 +179,23 @@ MatchPattern.prototype = {
return false;
},
// Test if this MatchPattern subsumes the given pattern (i.e., whether
// this pattern matches everything the given pattern does).
// Note, this method considers only to protocols and hosts/domains,
// paths are ignored.
subsumes(pattern) {
let match = PATTERN_REGEXP.exec(pattern);
if (!match) {
throw new Error("Invalid match pattern");
}
if (match[1] == "*") {
return WILDCARD_SCHEMES.every(scheme => this.matchesIgnoringPath({scheme, host: match[2]}));
}
return this.matchesIgnoringPath({scheme: match[1], host: match[2]});
},
serialize() {
return this.pat;
},