/* 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";
ChromeUtils.defineESModuleGetters(
this, {
ExtensionPermissions:
"resource://gre/modules/ExtensionPermissions.sys.mjs",
Schemas:
"resource://gre/modules/Schemas.sys.mjs",
});
var { ExtensionError } = ExtensionUtils;
XPCOMUtils.defineLazyPreferenceGetter(
this,
"promptsEnabled",
"extensions.webextOptionalPermissionPrompts"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"dataCollectionPermissionsEnabled",
"extensions.dataCollectionPermissions.enabled",
false
);
ChromeUtils.defineLazyGetter(
this,
"OPTIONAL_ONLY_PERMISSIONS", () => {
// Schemas.getPermissionNames() depends on API schemas to have been loaded.
// This is always the case here - extension APIs can only be called when an
// extension has started. And as part of startup, extension schemas are
// always parsed, via extension.loadManifest at:
// https://searchfox.org/mozilla-central/rev/2deb9bcf801f9de83d4f30c890d442072b9b6595/toolkit/components/extensions/Extension.sys.mjs#2094
return new Set(Schemas.getPermissionNames([
"OptionalOnlyPermission"]));
});
function normalizePermissions(perms) {
perms = { ...perms };
perms.permissions = perms.permissions.filter(
perm => !perm.startsWith(
"internal:") && perm !==
"<all_urls>"
);
if (!dataCollectionPermissionsEnabled) {
delete perms.data_collection;
}
return perms;
}
this.permissions =
class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
onAdded({ fire }) {
let { extension } =
this;
let callback = (event, change) => {
if (change.extensionId == extension.id && change.added) {
let perms = normalizePermissions(change.added);
if (
perms.permissions.length ||
perms.origins.length ||
(dataCollectionPermissionsEnabled && perms.data_collection.length)
) {
fire.async(perms);
}
}
};
extensions.on(
"change-permissions", callback);
return {
unregister() {
extensions.off(
"change-permissions", callback);
},
convert(_fire) {
fire = _fire;
},
};
},
onRemoved({ fire }) {
let { extension } =
this;
let callback = (event, change) => {
if (change.extensionId == extension.id && change.removed) {
let perms = normalizePermissions(change.removed);
if (
perms.permissions.length ||
perms.origins.length ||
(dataCollectionPermissionsEnabled && perms.data_collection.length)
) {
fire.async(perms);
}
}
};
extensions.on(
"change-permissions", callback);
return {
unregister() {
extensions.off(
"change-permissions", callback);
},
convert(_fire) {
fire = _fire;
},
};
},
};
getAPI(context) {
let { extension } = context;
return {
permissions: {
async request(perms) {
let { permissions, origins, data_collection } = perms;
let { optionalPermissions } = context.extension;
for (let perm of permissions) {
if (!optionalPermissions.includes(perm)) {
throw new ExtensionError(
`Cannot request permission ${perm} since it was not declared in optional_permissions`
);
}
if (
OPTIONAL_ONLY_PERMISSIONS.has(perm) &&
(permissions.length >
1 ||
origins.length ||
(dataCollectionPermissionsEnabled && data_collection.length))
) {
throw new ExtensionError(
`Cannot request permission ${perm} with another permission`
);
}
}
let optionalOrigins = context.extension.optionalOrigins;
for (let origin of origins) {
if (!optionalOrigins.subsumes(
new MatchPattern(origin))) {
throw new ExtensionError(
`Cannot request origin permission
for ${origin} since it was not declared in the manifest`
);
}
}
if (dataCollectionPermissionsEnabled) {
let { optionalDataCollectionPermissions } = context.extension;
for (let perm of data_collection) {
if (!optionalDataCollectionPermissions.includes(perm)) {
throw new ExtensionError(
`Cannot request data collection permission ${perm} since it ` +
"was not declared in data_collection_permissions.optional"
);
}
}
}
// Per Chrome, a per-addon ExtensionSettings entry overrides the
// global "*" blocked_permissions even if it doesn't set its own.
// Chrome throws "Permissions are blocked by enterprise policy" when
// any requested permission is blocked, so we match that contract
// (extensions can distinguish admin-block from user-deny in catch).
// getExtensionSettings returns the effective blocked list, which
// already accounts for allowed_permissions.
if (
Services.policies
?.getExtensionSettings(extension.id)
?.blocked_permissions?.some(p => permissions.includes(p))
) {
throw new ExtensionError(
"Permissions are blocked by enterprise policy."
);
}
if (promptsEnabled) {
permissions = permissions.filter(
perm => !context.extension.hasPermission(perm)
);
origins = origins.filter(
origin =>
!context.extension.allowedOrigins.subsumes(
new MatchPattern(origin)
)
);
data_collection = data_collection.filter(
perm => !context.extension.dataCollectionPermissions.has(perm)
);
if (
!permissions.length &&
!origins.length &&
!data_collection.length
) {
return true;
}
let browser = context.pendingEventBrowser || context.xulBrowser;
let allowPromise =
new Promise(resolve => {
let subject = {
wrappedJSObject: {
browser,
name: context.extension.name,
id: context.extension.id,
icon: context.extension.getPreferredIcon(
32),
permissions: { permissions, origins, data_collection },
resolve,
},
};
Services.obs.notifyObservers(
subject,
"webextension-optional-permission-prompt"
);
});
if (context.isBackgroundContext) {
extension.emit(
"background-script-idle-waituntil", {
promise: allowPromise,
reason:
"permissions_request",
});
}
if (!(await allowPromise)) {
return false;
}
}
await ExtensionPermissions.add(extension.id, perms, extension);
return true;
},
async getAll() {
let perms = normalizePermissions(context.extension.activePermissions);
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.allowedOrigins.subsumes(
new MatchPattern(origin)
)
) {
return false;
}
}
if (dataCollectionPermissionsEnabled) {
for (let perm of permissions.data_collection) {
if (!context.extension.dataCollectionPermissions.has(perm)) {
return false;
}
}
}
return true;
},
async remove(permissions) {
if (Services.policies?.isAddonRequiredByPolicy(extension.id)) {
// Match Chrome: any origin subsumed by a required manifest origin
// (host_permissions or content_scripts patterns) cannot be removed.
let manifestSet = extension.getManifestOriginsMatchPatternSet();
for (let origin of permissions.origins) {
if (manifestSet.subsumes(
new MatchPattern(origin))) {
throw new ExtensionError(
"You cannot remove required permissions. " +
"Host permissions are locked by enterprise policies."
);
}
}
}
await ExtensionPermissions.remove(
extension.id,
permissions,
extension
);
return true;
},
onAdded:
new EventManager({
context,
module:
"permissions",
event:
"onAdded",
extensionApi:
this,
}).api(),
onRemoved:
new EventManager({
context,
module:
"permissions",
event:
"onRemoved",
extensionApi:
this,
}).api(),
},
};
}
};