--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -6,16 +6,18 @@ module.exports = {
"Cc": true,
"Ci": true,
"Cr": true,
"Cu": true,
"TextDecoder": false,
"TextEncoder": false,
"MatchGlob": false,
+ "MatchPattern": true,
+ "MatchPatternSet": false,
// Specific to WebExtensions:
"AppConstants": true,
"Extension": true,
"ExtensionAPI": true,
"ExtensionManagement": true,
"ExtensionUtils": true,
"extensions": true,
"getContainerForCookieStoreId": true,
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -57,18 +57,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
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");
-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, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -418,17 +416,17 @@ this.ExtensionData = class {
// This method should return a structured representation of any
// capabilities this extension has access to, as derived from the
// manifest. The current implementation just returns the contents
// of the permissions attribute, if we add things like url_overrides,
// they should also be added here.
get userPermissions() {
let result = {
- origins: this.whiteListedHosts.pat,
+ origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern),
apis: [...this.apiNames],
};
if (Array.isArray(this.manifest.content_scripts)) {
for (let entry of this.manifest.content_scripts) {
result.origins.push(...entry.matches);
}
}
@@ -530,25 +528,29 @@ this.ExtensionData = class {
if (perm === "geckoProfiler") {
const acceptedExtensions = Preferences.get("extensions.geckoProfiler.acceptedExtensionIds");
if (!acceptedExtensions.split(",").includes(this.id)) {
this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
continue;
}
}
- this.permissions.add(perm);
let type = classifyPermission(perm);
if (type.origin) {
- whitelist.push(perm);
+ let matcher = new MatchPattern(perm, {ignorePath: true});
+
+ whitelist.push(matcher);
+ perm = matcher.pattern;
} else if (type.api) {
this.apiNames.add(type.api);
}
+
+ this.permissions.add(perm);
}
- this.whiteListedHosts = new MatchPattern(whitelist);
+ this.whiteListedHosts = new MatchPatternSet(whitelist, {ignorePath: true});
for (let api of this.apiNames) {
this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
}
return this.manifest;
}
@@ -732,28 +734,34 @@ this.Extension = class extends Extension
/* 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));
+ let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
+
+ this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins],
+ {ignorePath: true});
}
});
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);
- }
+ let origins = permissions.origins.map(
+ origin => new MatchPattern(origin, {ignorePath: true}).pattern);
+
+ this.whiteListedHosts = new MatchPatternSet(
+ this.whiteListedHosts.patterns
+ .filter(host => !origins.includes(host.pattern)));
});
/* eslint-enable mozilla/balanced-listeners */
}
static generateXPI(data) {
return ExtensionTestCommon.generateXPI(data);
}
@@ -851,17 +859,17 @@ this.Extension = class extends Extension
id: this.id,
uuid: this.uuid,
instanceId: this.instanceId,
manifest: this.manifest,
resourceURL: this.addonData.resourceURI.spec,
baseURL: this.baseURI.spec,
content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
webAccessibleResources: this.webAccessibleResources.map(res => res.glob),
- whiteListedHosts: this.whiteListedHosts.serialize(),
+ whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
localeData: this.localeData.serialize(),
permissions: this.permissions,
principal: this.principal,
optionalPermissions: this.manifest.optional_permissions,
};
}
broadcast(msg, data) {
@@ -987,17 +995,20 @@ this.Extension = class extends Extension
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));
+ let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
+
+ this.whiteListedHosts = new MatchPatternSet([...patterns, ...perms.origins],
+ {ignorePath: true});
}
// 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);
@@ -1124,17 +1135,17 @@ this.Extension = class extends Extension
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);
+ this._optionalOrigins = new MatchPatternSet(origins, {ignorePath: true});
}
return this._optionalOrigins;
}
};
Services.ppmm.loadProcessScript("data:,new " + function() {
Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
}, true);
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -21,18 +21,16 @@ const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
"resource://gre/modules/ExtensionContent.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
- "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
"resource://gre/modules/NativeMessaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
@@ -477,17 +475,17 @@ class BrowserExtensionContent extends Ev
this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
defineLazyGetter(this, "scripts", () => {
return data.content_scripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
});
this.webAccessibleResources = data.webAccessibleResources.map(res => new MatchGlob(res));
- this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+ this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts, {ignorePath: true});
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);
@@ -502,31 +500,37 @@ class BrowserExtensionContent extends Ev
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));
+ let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
+
+ this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins],
+ {ignorePath: true});
}
});
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);
- }
+ let origins = permissions.origins.map(
+ origin => new MatchPattern(origin, {ignorePath: true}).pattern);
+
+ this.whiteListedHosts = new MatchPatternSet(
+ this.whiteListedHosts.patterns
+ .filter(host => !origins.includes(host.pattern)));
}
});
/* eslint-enable mozilla/balanced-listeners */
ExtensionManager.extensions.set(this.id, this);
}
shutdown() {
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1062,17 +1062,17 @@ class SchemaAPIManager extends EventEmit
* @returns {object} A sandbox that is used as the global by `loadScript`.
*/
_createExtGlobal() {
let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
wantXrays: false,
sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
});
- Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, extensions: this});
+ Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, MatchPattern, MatchPatternSet, extensions: this});
Cu.import("resource://gre/modules/AppConstants.jsm", global);
Cu.import("resource://gre/modules/ExtensionAPI.jsm", global);
XPCOMUtils.defineLazyGetter(global, "console", getConsole);
XPCOMUtils.defineLazyModuleGetter(global, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
--- a/toolkit/components/extensions/ExtensionPermissions.jsm
+++ b/toolkit/components/extensions/ExtensionPermissions.jsm
@@ -52,17 +52,19 @@ this.ExtensionPermissions = {
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) {
+ origin = new MatchPattern(origin, {ignorePath: true}).pattern;
if (!origins.includes(origin)) {
added.origins.push(origin);
origins.push(origin);
}
}
if (added.permissions.length > 0 || added.origins.length > 0) {
prefs.saveSoon();
@@ -84,17 +86,20 @@ this.ExtensionPermissions = {
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) {
+ origin = new MatchPattern(origin, {ignorePath: true}).pattern;
+
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) {
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -64,17 +64,17 @@ function checkSetCookiePermissions(exten
//
// See source/netwerk/cookie/nsCookieService.cpp, in particular
// CheckDomain() and SetCookieInternal().
if (uri.scheme != "http" && uri.scheme != "https") {
return false;
}
- if (!extension.whiteListedHosts.matchesIgnoringPath(uri)) {
+ if (!extension.whiteListedHosts.matches(uri)) {
return false;
}
if (!cookie.host) {
// If no explicit host is specified, this becomes a host-only cookie.
cookie.host = uri.host;
return true;
}
--- a/toolkit/components/extensions/ext-management.js
+++ b/toolkit/components/extensions/ext-management.js
@@ -51,20 +51,24 @@ function getExtensionInfoForAddon(extens
enabled: addon.isActive,
optionsUrl: addon.optionsURL || "",
installType: installType(addon),
type: addon.type,
};
if (extension) {
let m = extension.manifest;
+
+ let hostPerms = extension.whiteListedHosts.patterns.map(matcher => matcher.patttern);
+
extInfo.permissions = Array.from(extension.permissions).filter(perm => {
- return !extension.whiteListedHosts.pat.includes(perm);
+ return hostPerms.includes(perm);
});
- extInfo.hostPermissions = extension.whiteListedHosts.pat;
+ extInfo.hostPermissions = hostPerms;
+
extInfo.shortName = m.short_name || "";
if (m.icons) {
extInfo.icons = Object.keys(m.icons).map(key => {
return {size: Number(key), url: m.icons[key]};
});
}
}
--- a/toolkit/components/extensions/ext-permissions.js
+++ b/toolkit/components/extensions/ext-permissions.js
@@ -25,17 +25,17 @@ this.permissions = class extends Extensi
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)) {
+ if (!optionalOrigins.subsumes(new MatchPattern(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: {
@@ -66,17 +66,17 @@ this.permissions = class extends Extensi
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)) {
+ if (!context.extension.whiteListedHosts.subsumes(new MatchPattern(origin))) {
return false;
}
}
return true;
},
async remove(permissions) {
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -1,14 +1,12 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
- "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
"resource://gre/modules/WebRequest.jsm");
// EventManager-like class specifically for WebRequest. Inherits from
// SingletonEventManager. Takes care of converting |details| parameter
// when invoking listeners.
function WebRequestEventManager(context, eventName) {
let name = `webRequest.${eventName}`;
@@ -17,23 +15,23 @@ function WebRequestEventManager(context,
// Prevent listening in on requests originating from system principal to
// prevent tinkering with OCSP, app and addon updates, etc.
if (data.isSystemPrincipal) {
return;
}
// Check hosts permissions for both the resource being requested,
const hosts = context.extension.whiteListedHosts;
- if (!hosts.matchesIgnoringPath(Services.io.newURI(data.url))) {
+ if (!hosts.matches(Services.io.newURI(data.url))) {
return;
}
// and the origin that is loading the resource.
const origin = data.documentUrl;
const own = origin && origin.startsWith(context.extension.getURL());
- if (origin && !own && !hosts.matchesIgnoringPath(Services.io.newURI(origin))) {
+ if (origin && !own && !hosts.matches(Services.io.newURI(origin))) {
return;
}
let browserData = {tabId: -1, windowId: -1};
if (data.browser) {
browserData = tabTracker.getBrowserData(data.browser);
}
if (filter.tabId != null && browserData.tabId != filter.tabId) {
@@ -73,18 +71,22 @@ function WebRequestEventManager(context,
}
}
return fire.sync(data2);
};
let filter2 = {};
if (filter.urls) {
- filter2.urls = new MatchPattern(filter.urls);
- if (!filter2.urls.overlapsPermissions(context.extension.whiteListedHosts, context.extension.optionalOrigins)) {
+ let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns,
+ ...context.extension.optionalOrigins.patterns]);
+
+ filter2.urls = new MatchPatternSet(filter.urls);
+
+ if (!perms.overlapsAll(filter2.urls)) {
Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions.");
}
}
if (filter.types) {
filter2.types = filter.types;
}
if (filter.tabId) {
filter2.tabId = filter.tabId;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -24,18 +24,21 @@ function findWinUtils(extension) {
notEqual(bgwin, null, "Found background window for the test extension");
return bgwin.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
}
add_task(async function test_permissions() {
const REQUIRED_PERMISSIONS = ["downloads"];
const REQUIRED_ORIGINS = ["*://site.com/", "*://*.domain.com/"];
+ const REQUIRED_ORIGINS_NORMALIZED = ["*://site.com/*", "*://*.domain.com/*"];
+
const OPTIONAL_PERMISSIONS = ["idle", "clipboardWrite"];
const OPTIONAL_ORIGINS = ["http://optionalsite.com/", "https://*.optionaldomain.com/"];
+ const OPTIONAL_ORIGINS_NORMALIZED = ["http://optionalsite.com/*", "https://*.optionaldomain.com/*"];
let acceptPrompt = false;
const observer = {
observe(subject, topic, data) {
if (topic == "webextension-optional-permission-prompt") {
let {resolve} = subject.wrappedJSObject;
resolve(acceptPrompt);
}
@@ -87,17 +90,17 @@ add_task(async function test_permissions
function call(method, arg) {
extension.sendMessage(method, arg);
return extension.awaitMessage(`${method}.result`);
}
let result = await call("getAll");
deepEqual(result.permissions, REQUIRED_PERMISSIONS);
- deepEqual(result.origins, REQUIRED_ORIGINS);
+ deepEqual(result.origins, REQUIRED_ORIGINS_NORMALIZED);
for (let perm of REQUIRED_PERMISSIONS) {
result = await call("contains", {permissions: [perm]});
equal(result, true, `contains() returns true for fixed permission ${perm}`);
}
for (let origin of REQUIRED_ORIGINS) {
result = await call("contains", {origins: [origin]});
equal(result, true, `contains() returns true for fixed origin ${origin}`);
@@ -150,17 +153,17 @@ add_task(async function test_permissions
};
result = await call("request", allOptional);
equal(result.status, "success", "request() returned cleanly");
equal(result.result, true, "request() returned true for accepted permissions");
userInputHandle.destruct();
let allPermissions = {
permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS],
- origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS],
+ origins: [...REQUIRED_ORIGINS_NORMALIZED, ...OPTIONAL_ORIGINS_NORMALIZED],
};
result = await call("getAll");
deepEqual(result, allPermissions, "getAll() returns required and runtime requested permissions");
result = await call("contains", allPermissions);
equal(result, true, "contains() returns true for runtime requested permissions");
@@ -172,25 +175,25 @@ add_task(async function test_permissions
deepEqual(result, allPermissions, "Runtime requested permissions are still present after restart");
// Check remove()
result = await call("remove", {permissions: OPTIONAL_PERMISSIONS});
equal(result, true, "remove() succeeded");
let perms = {
permissions: REQUIRED_PERMISSIONS,
- origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS],
+ origins: [...REQUIRED_ORIGINS_NORMALIZED, ...OPTIONAL_ORIGINS_NORMALIZED],
};
result = await call("getAll");
deepEqual(result, perms, "Expected permissions remain after removing some");
result = await call("remove", {origins: OPTIONAL_ORIGINS});
equal(result, true, "remove() succeeded");
- perms.origins = REQUIRED_ORIGINS;
+ perms.origins = REQUIRED_ORIGINS_NORMALIZED;
result = await call("getAll");
deepEqual(result, perms, "Back to default permissions after removing more");
await extension.unload();
});
add_task(async function test_startup() {
async function background() {
@@ -202,17 +205,17 @@ add_task(async function test_startup() {
let all = await browser.permissions.getAll();
browser.test.sendMessage("perms", all);
}
const PERMS1 = {
permissions: ["clipboardRead", "tabs"],
};
const PERMS2 = {
- origins: ["https://site2.com/"],
+ origins: ["https://site2.com/*"],
};
let extension1 = ExtensionTestUtils.loadExtension({
background,
manifest: {optional_permissions: PERMS1.permissions},
useAddonManager: "permanent",
});
let extension2 = ExtensionTestUtils.loadExtension({
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -106,16 +106,18 @@ SingleMatchPattern.prototype = {
);
},
// Tests if this can possibly overlap with the |other| SingleMatchPattern.
overlapsIgnoringPath(other) {
return this.schemes.some(scheme => other.schemes.includes(scheme)) &&
(this.hostMatch(other) || other.hostMatch(this));
},
+
+ get pattern() { return this.pat; },
};
this.MatchPattern = function(pat) {
this.pat = pat;
if (!pat) {
this.matchers = [];
} else if (pat instanceof String || typeof(pat) == "string") {
this.matchers = [new SingleMatchPattern(pat)];
@@ -131,16 +133,18 @@ this.MatchPattern = function(pat) {
};
MatchPattern.prototype = {
// |uri| should be an nsIURI.
matches(uri) {
return this.matchers.some(matcher => matcher.matches(uri));
},
+ get patterns() { return this.matchers; },
+
matchesIgnoringPath(uri, explicit = false) {
if (explicit) {
return this.explicitMatchers.some(matcher => matcher.matches(uri, true));
}
return this.matchers.some(matcher => matcher.matches(uri, true));
},
// Checks that this match pattern grants access to read the given
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -300,17 +300,17 @@ var ContentPolicyManager = {
addListener(callback, opts) {
// Clone opts, since we're going to modify them for IPC.
opts = Object.assign({}, opts);
let id = this.nextId++;
opts.id = id;
if (opts.filter.urls) {
opts.filter = Object.assign({}, opts.filter);
- opts.filter.urls = opts.filter.urls.serialize();
+ opts.filter.urls = opts.filter.urls.patterns.map(url => url.pattern);
}
Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts);
this.policyData.set(id, opts);
this.policies.set(id, callback);
this.idMap.set(callback, id);
},