Backed out 4 changesets (bug 1437864, bug 1470466, bug 1437861) for Android xpcshell failures CLOSED TREE

Backed out changeset a6185abfc2f8 (bug 1470466)
Backed out changeset 82d60e34a977 (bug 1437864)
Backed out changeset 7634d9d14449 (bug 1437864)
Backed out changeset a92b6e069d7e (bug 1437861)
This commit is contained in:
Bogdan Tara 2018-09-12 00:57:00 +03:00
Родитель 55bc80584c
Коммит 9c802a42f1
26 изменённых файлов: 82 добавлений и 1322 удалений

Просмотреть файл

@ -13,7 +13,6 @@
"use strict";
/* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */
let expectedContentApisTargetSpecific = [
"userScripts.setScriptAPIs",
];
let expectedBackgroundApisTargetSpecific = [

Просмотреть файл

@ -13,7 +13,6 @@
"use strict";
/* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */
let expectedContentApisTargetSpecific = [
"userScripts.setScriptAPIs",
];
let expectedBackgroundApisTargetSpecific = [

Просмотреть файл

@ -1072,12 +1072,6 @@ class ChildAPIManager {
return false;
}
// Do not generate content_only APIs, unless explicitly allowed.
if (this.context.envType !== "content_child" &&
allowedContexts.includes("content_only")) {
return false;
}
return true;
}

Просмотреть файл

@ -424,25 +424,6 @@ class BaseContext {
this.messageManager = null;
this.contentWindow = null;
this.innerWindowID = 0;
// These two properties are assigned in ContentScriptContextChild subclass
// to keep a copy of the content script sandbox Error and Promise globals
// (which are used by the WebExtensions internals) before any extension
// content script code had any chance to redefine them.
this.cloneScopeError = null;
this.cloneScopePromise = null;
}
get Error() {
// Return the copy stored in the context instance (when the context is an instance of
// ContentScriptContextChild or the global from extension page window otherwise).
return this.cloneScopeError || this.cloneScope.Error;
}
get Promise() {
// Return the copy stored in the context instance (when the context is an instance of
// ContentScriptContextChild or the global from extension page window otherwise).
return this.cloneScopePromise || this.cloneScope.Promise;
}
setContentWindow(contentWindow) {
@ -616,7 +597,7 @@ class BaseContext {
* @returns {Error}
*/
normalizeError(error, caller) {
if (error instanceof this.Error) {
if (error instanceof this.cloneScope.Error) {
return error;
}
let message, fileName;
@ -642,7 +623,7 @@ class BaseContext {
Cu.reportError(error);
message = "An unexpected error occurred";
}
return new this.Error(message, fileName);
return new this.cloneScope.Error(message, fileName);
}
/**
@ -742,7 +723,7 @@ class BaseContext {
});
});
} else {
return new this.Promise((resolve, reject) => {
return new this.cloneScope.Promise((resolve, reject) => {
promise.then(
value => {
if (this.unloaded) {

Просмотреть файл

@ -42,7 +42,6 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
const {
DefaultMap,
DefaultWeakMap,
ExtensionError,
getInnerWindowID,
getWinUtils,
promiseDocumentIdle,
@ -472,7 +471,24 @@ class Script {
}
}
let scripts = await this.awaitCompiledScripts(context);
let scriptPromises = this.compileScripts();
let scripts = scriptPromises.map(promise => promise.script);
// If not all scripts are already available in the cache, block
// parsing and wait all promises to resolve.
if (!scripts.every(script => script)) {
let promise = Promise.all(scriptPromises);
// If we're supposed to inject at the start of the document load,
// and we haven't already missed that point, block further parsing
// until the scripts have been loaded.
let {document} = context.contentWindow;
if (this.runAt === "document_start" && document.readyState !== "complete") {
document.blockParsing(promise, {blockScriptCreated: false});
}
scripts = await promise;
}
let result;
@ -496,235 +512,10 @@ class Script {
await cssPromise;
return result;
}
async awaitCompiledScripts(context) {
let scriptPromises = this.compileScripts();
let scripts = scriptPromises.map(promise => promise.script);
// If not all scripts are already available in the cache, block
// parsing and wait all promises to resolve.
if (!scripts.every(script => script)) {
let promise = Promise.all(scriptPromises);
// If we're supposed to inject at the start of the document load,
// and we haven't already missed that point, block further parsing
// until the scripts have been loaded.
let {document} = context.contentWindow;
if (this.runAt === "document_start" && document.readyState !== "complete") {
document.blockParsing(promise, {blockScriptCreated: false});
}
scripts = await promise;
}
return scripts;
}
}
// Represents a user script.
class UserScript extends Script {
/**
* @param {BrowserExtensionContent} extension
* @param {WebExtensionContentScript|object} matcher
* An object with a "matchesWindow" method and content script execution
* details.
*/
constructor(extension, matcher) {
super(extension, matcher);
// This is an opaque object that the extension provides, it is associated to
// the particular userScript and it is passed as a parameter to the custom
// userScripts APIs defined by the extension.
this.scriptMetadata = matcher.userScriptOptions.scriptMetadata;
this.apiScriptURL = extension.manifest.userScripts && extension.manifest.userScripts.apiScript;
this.promiseAPIScript = null;
this.scriptPromises = null;
// WeakMap<ContentScriptContextChild, Sandbox>
this.sandboxes = new DefaultWeakMap((context) => {
return this.createSandbox(context);
});
}
compileScripts() {
if (this.apiScriptURL && !this.promiseAPIScript) {
this.promiseAPIScript = this.scriptCache.get(this.apiScriptURL);
}
if (!this.scriptPromises) {
this.scriptPromises = this.js.map(url => this.scriptCache.get(url));
}
if (this.promiseAPIScript) {
return [this.promiseAPIScript, ...this.scriptPromises];
}
return this.scriptPromises;
}
async inject(context) {
const {extension} = context;
DocumentManager.lazyInit();
let scripts = await this.awaitCompiledScripts(context);
let apiScript, sandboxScripts;
if (this.promiseAPIScript) {
[apiScript, ...sandboxScripts] = scripts;
} else {
sandboxScripts = scripts;
}
// Load and execute the API script once per context.
if (apiScript) {
context.executeAPIScript(apiScript);
}
// The evaluations below may throw, in which case the promise will be
// automatically rejected.
ExtensionTelemetry.userScriptInjection.stopwatchStart(extension, context);
try {
let userScriptSandbox = this.sandboxes.get(context);
context.callOnClose({
close: () => {
// Destroy the userScript sandbox when the related ContentScriptContextChild instance
// is being closed.
this.sandboxes.delete(context);
Cu.nukeSandbox(userScriptSandbox);
},
});
// Inject the custom API registered by the extension API script.
if (apiScript) {
this.injectUserScriptAPIs(userScriptSandbox, context);
}
for (let script of sandboxScripts) {
script.executeInGlobal(userScriptSandbox);
}
} finally {
ExtensionTelemetry.userScriptInjection.stopwatchFinish(extension, context);
}
}
createSandbox(context) {
const {contentWindow} = context;
const contentPrincipal = contentWindow.document.nodePrincipal;
const ssm = Services.scriptSecurityManager;
let principal;
if (ssm.isSystemPrincipal(contentPrincipal)) {
principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
} else {
principal = [contentPrincipal];
}
const sandbox = Cu.Sandbox(principal, {
sandboxName: `User Script registered by ${this.extension.policy.debugName}`,
sandboxPrototype: contentWindow,
sameZoneAs: contentWindow,
wantXrays: true,
wantGlobalProperties: ["XMLHttpRequest", "fetch"],
originAttributes: contentPrincipal.originAttributes,
metadata: {
"inner-window-id": context.innerWindowID,
addonId: this.extension.policy.id,
},
});
return sandbox;
}
injectUserScriptAPIs(userScriptScope, context) {
const {extension, scriptMetadata} = this;
const {userScriptAPIs, cloneScope: apiScope} = context;
if (!userScriptAPIs) {
return;
}
const clonedMetadata = scriptMetadata ?
Cu.cloneInto(scriptMetadata, apiScope) : undefined;
const UserScriptError = userScriptScope.Error;
const UserScriptPromise = userScriptScope.Promise;
const wrappedFnMap = new WeakMap();
function safeReturnCloned(res) {
try {
return Cu.cloneInto(res, userScriptScope);
} catch (err) {
Cu.reportError(
`userScripts API method wrapper for ${extension.policy.debugName}: ${err}`
);
throw new UserScriptError("Unable to clone object in the userScript sandbox");
}
}
function wrapUserScriptAPIMethod(fn, fnName) {
return Cu.exportFunction(function(...args) {
let fnArgs = Cu.cloneInto([], apiScope);
try {
for (let arg of args) {
if (typeof arg === "function") {
if (!wrappedFnMap.has(arg)) {
wrappedFnMap.set(arg, Cu.exportFunction(arg, apiScope));
}
fnArgs.push(wrappedFnMap.get(arg));
} else {
fnArgs.push(Cu.cloneInto(arg, apiScope));
}
}
} catch (err) {
Cu.reportError(`Error cloning userScriptAPIMethod parameters in ${fnName}: ${err}`);
throw new UserScriptError("Only serializable parameters are supported");
}
const res = runSafeSyncWithoutClone(fn, fnArgs, clonedMetadata, userScriptScope);
if (res instanceof context.Promise) {
return UserScriptPromise.resolve().then(async () => {
let value;
try {
value = await res;
} catch (err) {
if (err instanceof context.Error) {
throw new UserScriptError(err.message);
} else {
throw safeReturnCloned(err);
}
}
return safeReturnCloned(value);
});
}
return safeReturnCloned(res);
}, userScriptScope);
}
for (let key of Object.keys(userScriptAPIs)) {
Schemas.exportLazyGetter(userScriptScope, key, () => {
// Wrap the custom API methods exported to the userScript sandbox.
return wrapUserScriptAPIMethod(userScriptAPIs[key], key);
});
}
}
}
var contentScripts = new DefaultWeakMap(matcher => {
const extension = processScript.extensions.get(matcher.extension);
if ("userScriptOptions" in matcher) {
return new UserScript(extension, matcher);
}
return new Script(extension, matcher);
return new Script(processScript.extensions.get(matcher.extension), matcher);
});
/**
@ -796,12 +587,6 @@ class ContentScriptContextChild extends BaseContext {
originAttributes: attrs,
});
// Preserve a copy of the original Error and Promise globals from the sandbox object,
// which are used in the WebExtensions internals (before any content script code had
// any chance to redefine them).
this.cloneScopePromise = this.sandbox.Promise;
this.cloneScopeError = this.sandbox.Error;
// Preserve a copy of the original window's XMLHttpRequest and fetch
// in a content object (fetch is manually binded to the window
// to prevent it from raising a TypeError because content object is not
@ -835,14 +620,6 @@ class ContentScriptContextChild extends BaseContext {
Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
// A set of exported API methods provided by the extension to the userScripts sandboxes.
this.userScriptAPIs = null;
// Keep track if the userScript API script has been already executed in this context
// (e.g. because there are more then one UserScripts that match the related webpage
// and so the UserScript apiScript has already been executed).
this.hasUserScriptAPIs = false;
}
injectAPI() {
@ -861,23 +638,6 @@ class ContentScriptContextChild extends BaseContext {
return this.sandbox;
}
setUserScriptAPIs(extCustomAPIs) {
if (this.userScriptAPIs) {
throw new ExtensionError("userScripts APIs may only be set once");
}
this.userScriptAPIs = extCustomAPIs;
}
async executeAPIScript(apiScript) {
// Execute the UserScript apiScript only once per context (e.g. more then one UserScripts
// match the same webpage and the apiScript has already been executed).
if (apiScript && !this.hasUserScriptAPIs) {
this.hasUserScriptAPIs = true;
apiScript.executeInGlobal(this.cloneScope);
}
}
addScript(script) {
if (script.requiresCleanup) {
this.scripts.push(script);
@ -901,7 +661,6 @@ class ContentScriptContextChild extends BaseContext {
}
}
Cu.nukeSandbox(this.sandbox);
this.sandbox = null;
}
}

Просмотреть файл

@ -24,7 +24,6 @@ const histograms = {
"storageLocalSetJSON": "WEBEXT_STORAGE_LOCAL_SET_MS",
"storageLocalGetIDB": "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
"storageLocalSetIDB": "WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
"userScriptInjection": "WEBEXT_USER_SCRIPT_INJECTION_MS",
};
/**

Просмотреть файл

@ -75,7 +75,7 @@ this.test = class extends ExtensionAPI {
const {extension} = context;
function getStack() {
return new context.Error().stack.replace(/^/gm, " ");
return new context.cloneScope.Error().stack.replace(/^/gm, " ");
}
function assertTrue(value, msg) {

Просмотреть файл

@ -65,13 +65,6 @@ extensions.registerModules({
["test"],
],
},
userScripts: {
url: "chrome://extensions/content/child/ext-userScripts.js",
scopes: ["addon_child", "content_child"],
paths: [
["userScripts"],
],
},
webRequest: {
url: "chrome://extensions/content/child/ext-webRequest.js",
scopes: ["addon_child"],

Просмотреть файл

@ -1,159 +0,0 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.importGlobalProperties(["crypto", "TextEncoder"]);
var {
DefaultMap,
ExtensionError,
} = ExtensionUtils;
/**
* Represents a registered userScript in the child extension process.
*
* @param {ExtensionPageContextChild} context
* The extension context which has registered the user script.
* @param {string} scriptId
* An unique id that represents the registered user script
* (generated and used internally to identify it across the different processes).
*/
class UserScriptChild {
constructor({context, scriptId, onScriptUnregister}) {
this.context = context;
this.scriptId = scriptId;
this.onScriptUnregister = onScriptUnregister;
this.unregistered = false;
}
async unregister() {
if (this.unregistered) {
throw new ExtensionError("User script already unregistered");
}
this.unregistered = true;
await this.context.childManager.callParentAsyncFunction(
"userScripts.unregister", [this.scriptId]);
this.context = null;
this.onScriptUnregister();
}
api() {
const {context} = this;
// Returns the RegisteredUserScript API object.
return {
unregister: () => {
return context.wrapPromise(this.unregister());
},
};
}
}
this.userScripts = class extends ExtensionAPI {
getAPI(context) {
// Cache of the script code already converted into blob urls:
// Map<textHash, blobURLs>
const blobURLsByHash = new Map();
// Keep track of the userScript that are sharing the same blob urls,
// so that we can revoke any blob url that is not used by a registered
// userScripts:
// Map<blobURL, Set<scriptId>>
const userScriptsByBlobURL = new DefaultMap(() => new Set());
function trackBlobURLs(scriptId, options) {
for (let url of options.js) {
if (userScriptsByBlobURL.has(url)) {
userScriptsByBlobURL.get(url).add(scriptId);
}
}
}
function revokeBlobURLs(scriptId, options) {
for (let url of options.js) {
if (userScriptsByBlobURL.has(url)) {
let scriptIds = userScriptsByBlobURL.get(url);
scriptIds.delete(scriptId);
if (scriptIds.size === 0) {
userScriptsByBlobURL.delete(url);
context.cloneScope.URL.revokeObjectURL(url);
}
}
}
}
// Convert a script code string into a blob URL (and use a cached one
// if the script hash is already associated to a blob URL).
const getBlobURL = async (text) => {
// Compute the hash of the js code string and reuse the blob url if we already have
// for the same hash.
const buffer = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(text));
const hash = String.fromCharCode(...new Uint16Array(buffer));
let blobURL = blobURLsByHash.get(hash);
if (blobURL) {
return blobURL;
}
const blob = new context.cloneScope.Blob([text], {type: "text/javascript"});
blobURL = context.cloneScope.URL.createObjectURL(blob);
// Start to track this blob URL.
userScriptsByBlobURL.get(blobURL);
blobURLsByHash.set(hash, blobURL);
return blobURL;
};
function convertToAPIObject(scriptId, options) {
const registeredScript = new UserScriptChild({
context, scriptId,
onScriptUnregister: () => revokeBlobURLs(scriptId, options),
});
trackBlobURLs(scriptId, options);
const scriptAPI = Cu.cloneInto(registeredScript.api(), context.cloneScope,
{cloneFunctions: true});
return scriptAPI;
}
// Revoke all the created blob urls once the context is destroyed.
context.callOnClose({
close() {
if (!context.cloneScope) {
return;
}
for (let blobURL of blobURLsByHash.values()) {
context.cloneScope.URL.revokeObjectURL(blobURL);
}
},
});
return {
userScripts: {
register(options) {
return context.cloneScope.Promise.resolve().then(async () => {
options.js = await Promise.all(options.js.map(js => {
return js.file || getBlobURL(js.code);
}));
const scriptId = await context.childManager.callParentAsyncFunction(
"userScripts.register", [options]);
return convertToAPIObject(scriptId, options);
});
},
setScriptAPIs(exportedAPIMethods) {
context.setUserScriptAPIs(exportedAPIMethods);
},
},
};
}
};

Просмотреть файл

@ -189,14 +189,6 @@
["topSites"]
]
},
"userScripts": {
"url": "chrome://extensions/content/parent/ext-userScripts.js",
"schema": "chrome://extensions/content/schemas/user_scripts.json",
"scopes": ["addon_parent", "content_parent"],
"paths": [
["userScripts"]
]
},
"webNavigation": {
"url": "chrome://extensions/content/parent/ext-webNavigation.js",
"schema": "chrome://extensions/content/schemas/web_navigation.json",

Просмотреть файл

@ -165,12 +165,6 @@ ExtensionManager = {
const script = new WebExtensionContentScript(policy, options);
policy.registerContentScript(script);
registeredContentScripts.set(scriptId, script);
// If the script is a userScript, add the additional userScriptOptions
// property to the WebExtensionContentScript instance.
if ("userScriptOptions" in options) {
script.userScriptOptions = options.userScriptOptions;
}
}
policy.active = true;
@ -219,20 +213,12 @@ ExtensionManager = {
if (policy) {
const registeredContentScripts = this.registeredContentScripts.get(policy);
const type = "userScriptOptions" in data.options ? "userScript" : "contentScript";
if (registeredContentScripts.has(data.scriptId)) {
Cu.reportError(new Error(
`Registering ${type} ${data.scriptId} on ${data.id} more than once`));
`Registering content script ${data.scriptId} on ${data.id} more than once`));
} else {
const script = new WebExtensionContentScript(policy, data.options);
// If the script is a userScript, add the additional userScriptOptions
// property to the WebExtensionContentScript instance.
if (type === "userScript") {
script.userScriptOptions = data.options.userScriptOptions;
}
policy.registerContentScript(script);
registeredContentScripts.set(data.scriptId, script);
}

Просмотреть файл

@ -35,7 +35,6 @@ toolkit.jar:
content/extensions/parent/ext-theme.js (parent/ext-theme.js)
content/extensions/parent/ext-toolkit.js (parent/ext-toolkit.js)
content/extensions/parent/ext-topSites.js (parent/ext-topSites.js)
content/extensions/parent/ext-userScripts.js (parent/ext-userScripts.js)
content/extensions/parent/ext-webRequest.js (parent/ext-webRequest.js)
content/extensions/parent/ext-webNavigation.js (parent/ext-webNavigation.js)
content/extensions/child/ext-backgroundPage.js (child/ext-backgroundPage.js)
@ -48,5 +47,4 @@ toolkit.jar:
content/extensions/child/ext-storage.js (child/ext-storage.js)
content/extensions/child/ext-test.js (child/ext-test.js)
content/extensions/child/ext-toolkit.js (child/ext-toolkit.js)
content/extensions/child/ext-userScripts.js (child/ext-userScripts.js)
content/extensions/child/ext-webRequest.js (child/ext-webRequest.js)

Просмотреть файл

@ -1,136 +0,0 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
var {
ExtensionError,
getUniqueId,
} = ExtensionUtils;
/**
* Represents (in the main browser process) a user script.
*
* @param {UserScriptOptions} details
* The options object related to the user script
* (which has the properties described in the user_scripts.json
* JSON API schema file).
*/
class UserScriptParent {
constructor(details) {
this.scriptId = getUniqueId();
this.options = this._convertOptions(details);
}
destroy() {
if (this.destroyed) {
throw new Error("Unable to destroy UserScriptParent twice");
}
this.destroyed = true;
this.options = null;
}
_convertOptions(details) {
const options = {
matches: details.matches,
excludeMatches: details.excludeMatches,
includeGlobs: details.includeGlobs,
excludeGlobs: details.excludeGlobs,
allFrames: details.allFrames,
matchAboutBlank: details.matchAboutBlank,
runAt: details.runAt || "document_idle",
jsPaths: details.js,
userScriptOptions: {
scriptMetadata: details.scriptMetadata,
},
};
return options;
}
serialize() {
return this.options;
}
}
this.userScripts = class extends ExtensionAPI {
constructor(...args) {
super(...args);
// Map<scriptId -> UserScriptParent>
this.userScriptsMap = new Map();
}
getAPI(context) {
const {extension} = context;
// Set of the scriptIds registered from this context.
const registeredScriptIds = new Set();
function unregisterContentScripts(scriptIds) {
for (let scriptId of registeredScriptIds) {
extension.registeredContentScripts.delete(scriptId);
this.userScriptsMap.delete(scriptId);
}
return context.extension.broadcast("Extension:UnregisterContentScripts", {
id: context.extension.id,
scriptIds,
});
}
// Unregister all the scriptId related to a context when it is closed,
// and revoke all the created blob urls once the context is destroyed.
context.callOnClose({
close() {
unregisterContentScripts(Array.from(registeredScriptIds));
},
});
return {
userScripts: {
register: async (details) => {
for (let origin of details.matches) {
if (!extension.whiteListedHosts.subsumes(new MatchPattern(origin))) {
throw new ExtensionError(`Permission denied to register a user script for ${origin}`);
}
}
const userScript = new UserScriptParent(details);
const {scriptId} = userScript;
this.userScriptsMap.set(scriptId, userScript);
const scriptOptions = userScript.serialize();
await extension.broadcast("Extension:RegisterContentScript", {
id: extension.id,
options: scriptOptions,
scriptId,
});
extension.registeredContentScripts.set(scriptId, scriptOptions);
return scriptId;
},
// This method is not available to the extension code, the extension code
// doesn't have access to the internally used scriptId, on the contrary
// the extension code will call script.unregister on the script API object
// that is resolved from the register API method returned promise.
unregister: async (scriptId) => {
const userScript = this.userScriptsMap.get(scriptId);
if (!userScript) {
throw new Error(`No such user script ID: ${scriptId}`);
}
userScript.destroy();
await unregisterContentScripts([scriptId]);
},
},
};
}
};

Просмотреть файл

@ -6,6 +6,27 @@
{
"namespace": "contentScripts",
"types": [
{
"id": "ExtensionFileOrCode",
"choices": [
{
"type": "object",
"properties": {
"file": {
"$ref": "manifest.ExtensionURL"
}
}
},
{
"type": "object",
"properties": {
"code": {
"type": "string"
}
}
}
]
},
{
"id": "RegisteredContentScriptOptions",
"type": "object",
@ -37,13 +58,13 @@
"type": "array",
"optional": true,
"description": "The list of CSS files to inject",
"items": { "$ref": "extensionTypes.ExtensionFileOrCode" }
"items": { "$ref": "ExtensionFileOrCode" }
},
"js": {
"type": "array",
"optional": true,
"description": "The list of JS files to inject",
"items": { "$ref": "extensionTypes.ExtensionFileOrCode" }
"items": { "$ref": "ExtensionFileOrCode" }
},
"allFrames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."},
"matchAboutBlank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."},

Просмотреть файл

@ -89,38 +89,6 @@
"additionalProperties": { "type": "any" }
}
]
},
{
"id": "ExtensionFileOrCode",
"choices": [
{
"type": "object",
"properties": {
"file": {
"$ref": "manifest.ExtensionURL"
}
}
},
{
"type": "object",
"properties": {
"code": {
"type": "string"
}
}
}
]
},
{
"id": "PlainJSONValue",
"description": "A plain JSON value",
"choices": [
{ "type": "number" },
{ "type": "string" },
{ "type": "boolean" },
{ "type": "array", "items": {"$ref": "PlainJSONValue"} },
{ "type": "object", "additionalProperties": { "$ref": "PlainJSONValue" } }
]
}
]
}

Просмотреть файл

@ -36,6 +36,5 @@ toolkit.jar:
content/extensions/schemas/theme.json
content/extensions/schemas/top_sites.json
content/extensions/schemas/types.json
content/extensions/schemas/user_scripts.json
content/extensions/schemas/web_navigation.json
content/extensions/schemas/web_request.json

Просмотреть файл

@ -1,134 +0,0 @@
/* 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/. */
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"userScripts": {
"type": "object",
"optional": true,
"properties": {
"apiScript": { "$ref": "manifest.ExtensionURL"}
}
}
}
}
]
},
{
"namespace": "userScripts",
"allowedContexts": ["content"],
"types": [
{
"id": "ExportedAPIMethods",
"type": "object",
"description": "A set of API methods provided by the extensions to its userScripts",
"additionalProperties": { "type": "function" }
},
{
"id": "UserScriptOptions",
"type": "object",
"description": "Details of a user script",
"properties": {
"js": {
"type": "array",
"optional": true,
"description": "The list of JS files to inject",
"minItems": 1,
"items": { "$ref": "extensionTypes.ExtensionFileOrCode" }
},
"scriptMetadata": {
"description": "An opaque user script metadata value",
"$ref": "extensionTypes.PlainJSONValue",
"optional": true
},
"matches": {
"type": "array",
"optional": false,
"minItems": 1,
"items": { "$ref": "manifest.MatchPattern" }
},
"excludeMatches": {
"type": "array",
"optional": true,
"minItems": 1,
"items": { "$ref": "manifest.MatchPattern" }
},
"includeGlobs": {
"type": "array",
"optional": true,
"items": { "type": "string" }
},
"excludeGlobs": {
"type": "array",
"optional": true,
"items": { "type": "string" }
},
"allFrames": {
"type": "boolean",
"default": false,
"optional": true,
"description": "If allFrames is <code>true</code>, implies that the JavaScript should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."
},
"matchAboutBlank": {
"type": "boolean",
"default": false,
"optional": true,
"description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."
},
"runAt": {
"$ref": "extensionTypes.RunAt",
"default": "document_idle",
"optional": true,
"description": "The soonest that the JavaScript will be injected into the tab. Defaults to \"document_idle\"."
}
}
},
{
"id": "RegisteredUserScript",
"type": "object",
"description": "An object that represents a user script registered programmatically",
"functions": [
{
"name": "unregister",
"type": "function",
"description": "Unregister a user script registered programmatically",
"async": true,
"parameters": []
}
]
}
],
"functions": [
{
"name": "register",
"type": "function",
"description": "Register a user script programmatically given its $(ref:userScripts.UserScriptOptions), and resolves to a $(ref:userScripts.RegisteredUserScript) instance",
"async": true,
"parameters": [
{
"name": "userScriptOptions",
"$ref": "UserScriptOptions"
}
]
},
{
"name": "setScriptAPIs",
"allowedContexts": ["content", "content_only"],
"type": "function",
"description": "Provides a set of custom API methods available to the registered userScripts",
"parameters": [
{
"name": "exportedAPIMethods",
"$ref": "ExportedAPIMethods"
}
]
}
]
}
]

Просмотреть файл

@ -97,7 +97,6 @@ let expectedBackgroundApis = [
"theme.onUpdated",
"types.LevelOfControl",
"types.SettingScope",
"userScripts.register",
];
function sendAllApis() {

Просмотреть файл

@ -1,39 +0,0 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
/* exported withHandlingUserInput */
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
let extensionHandlers = new WeakSet();
function handlingUserInputFrameScript() {
/* globals content */
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
let handle;
MessageChannel.addListener(this, "ExtensionTest:HandleUserInput", {
receiveMessage({name, data}) {
if (data) {
handle = content.windowUtils.setHandlingUserInput(true);
} else if (handle) {
handle.destruct();
handle = null;
}
},
});
}
async function withHandlingUserInput(extension, fn) {
let {messageManager} = extension.extension.groupFrameLoader;
if (!extensionHandlers.has(extension)) {
messageManager.loadFrameScript(`data:,(${handlingUserInputFrameScript}).call(this)`, false, true);
extensionHandlers.add(extension);
}
await MessageChannel.sendMessage(messageManager, "ExtensionTest:HandleUserInput", true);
await fn();
await MessageChannel.sendMessage(messageManager, "ExtensionTest:HandleUserInput", false);
}

Просмотреть файл

@ -2,6 +2,7 @@
ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm");
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");
const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties";
@ -12,6 +13,38 @@ AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"
Services.prefs.setBoolPref("extensions.webextensions.background-delayed-startup", false);
let extensionHandlers = new WeakSet();
function frameScript() {
/* globals content */
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
let handle;
MessageChannel.addListener(this, "ExtensionTest:HandleUserInput", {
receiveMessage({name, data}) {
if (data) {
handle = content.windowUtils.setHandlingUserInput(true);
} else if (handle) {
handle.destruct();
handle = null;
}
},
});
}
async function withHandlingUserInput(extension, fn) {
let {messageManager} = extension.extension.groupFrameLoader;
if (!extensionHandlers.has(extension)) {
messageManager.loadFrameScript(`data:,(${frameScript}).call(this)`, false, true);
extensionHandlers.add(extension);
}
await MessageChannel.sendMessage(messageManager, "ExtensionTest:HandleUserInput", true);
await fn();
await MessageChannel.sendMessage(messageManager, "ExtensionTest:HandleUserInput", false);
}
let sawPrompt = false;
let acceptPrompt = false;
const observer = {

Просмотреть файл

@ -1,344 +0,0 @@
"use strict";
const {
createAppInfo,
} = AddonTestUtils;
AddonTestUtils.init(this);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
add_task(async function setup_optional_permission_observer() {
// Grant the optional permissions requested.
function permissionObserver(subject, topic, data) {
if (topic == "webextension-optional-permission-prompt") {
let {resolve} = subject.wrappedJSObject;
resolve(true);
}
}
Services.obs.addObserver(permissionObserver, "webextension-optional-permission-prompt");
registerCleanupFunction(() => {
Services.obs.removeObserver(permissionObserver, "webextension-optional-permission-prompt");
});
});
// Test that userScripts can only matches origins that are subsumed by the extension permissions,
// and that more origins can be allowed by requesting an optional permission.
add_task(async function test_userScripts_matches_denied() {
async function background() {
async function registerUserScriptWithMatches(matches) {
const scripts = await browser.userScripts.register({
js: [{code: ""}],
matches,
});
await scripts.unregister();
}
// These matches are supposed to be denied until the extension has been granted the
// <all_urls> origin permission.
const testMatches = [
"<all_urls>",
"file://*/*",
"https://localhost/*",
"http://example.com/*",
];
browser.test.onMessage.addListener(async msg => {
if (msg === "test-denied-matches") {
for (let testMatch of testMatches) {
await browser.test.assertRejects(
registerUserScriptWithMatches([testMatch]),
/Permission denied to register a user script for/,
"Got the expected rejection when the extension permission does not subsume the userScript matches");
}
} else if (msg === "grant-all-urls") {
await browser.permissions.request({origins: ["<all_urls>"]});
} else if (msg === "test-allowed-matches") {
for (let testMatch of testMatches) {
try {
await registerUserScriptWithMatches([testMatch]);
} catch (err) {
browser.test.fail(`Unexpected rejection ${err} on matching ${JSON.stringify(testMatch)}`);
}
}
}
browser.test.sendMessage(`${msg}:done`);
});
browser.test.sendMessage("background-ready");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["http://localhost/*"],
optional_permissions: ["<all_urls>"],
},
background,
});
await extension.startup();
await extension.awaitMessage("background-ready");
// Test that the matches not subsumed by the extension permissions are being denied.
extension.sendMessage("test-denied-matches");
await extension.awaitMessage("test-denied-matches:done");
// Grant the optional <all_urls> permission.
await withHandlingUserInput(extension, async () => {
extension.sendMessage("grant-all-urls");
await extension.awaitMessage("grant-all-urls:done");
});
// Test that all the matches are now subsumed by the extension permissions.
extension.sendMessage("test-allowed-matches");
await extension.awaitMessage("test-allowed-matches:done");
await extension.unload();
});
// Test that userScripts sandboxes:
// - can be registered/unregistered from an extension page
// - have no WebExtensions APIs available
// - are able to access the target window and document
add_task(async function test_userScripts_no_webext_apis() {
async function background() {
const matches = ["http://localhost/*/file_sample.html"];
const script = await browser.userScripts.register({
js: [{
code: `
const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
document.body.innerHTML = "userScript loaded - " + JSON.stringify(webextAPINamespaces);
`,
}],
runAt: "document_end",
matches,
scriptMetadata: {
name: "test-user-script",
arrayToMatch: ["el1"],
objectToMatch: {nestedProp: "nestedValue"},
},
});
const scriptToRemove = await browser.userScripts.register({
js: [{
code: 'document.body.innerHTML = "unexpected unregistered userScript loaded";',
}],
runAt: "document_end",
matches,
scriptMetadata: {
name: "user-script-to-remove",
},
});
browser.test.assertTrue("unregister" in script,
"Got an unregister method on the userScript API object");
// Remove the last registered user script.
await scriptToRemove.unregister();
await browser.contentScripts.register({
js: [{
code: `
browser.test.sendMessage("page-loaded", {
textContent: document.body.textContent,
url: window.location.href,
}); true;
`,
}],
matches,
});
browser.test.sendMessage("background-ready");
}
let extensionData = {
manifest: {
permissions: [
"http://localhost/*/file_sample.html",
],
},
background,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
await extension.awaitMessage("background-ready");
// Test in an existing process (where the registered userScripts has been received from the
// Extension:RegisterContentScript message sent to all the processes).
info("Test content script loaded in a process created before any registered userScript");
let url = `${BASE_URL}/file_sample.html#remote-false`;
let contentPage = await ExtensionTestUtils.loadContentPage(url, {remote: false});
const reply = await extension.awaitMessage("page-loaded");
Assert.deepEqual(reply, {
textContent: "userScript loaded - undefined",
url,
}, "The userScript executed on the expected url and no access to the WebExtensions APIs");
await contentPage.close();
// Test in a new process (where the registered userScripts has to be retrieved from the extension
// representation from the shared memory data).
// NOTE: this part is currently skipped on Android, where e10s content is not yet supported and
// the xpcshell test crash when we create contentPage2 with `remote = true`.
if (ExtensionTestUtils.remoteContentScripts) {
info("Test content script loaded in a process created after the userScript has been registered");
let url2 = `${BASE_URL}/file_sample.html#remote-true`;
let contentPage2 = await ExtensionTestUtils.loadContentPage(url2, {remote: true});
// Load an url that matches and check that the userScripts has been loaded.
const reply2 = await extension.awaitMessage("page-loaded");
Assert.deepEqual(reply2, {
textContent: "userScript loaded - undefined",
url: url2,
}, "The userScript executed on the expected url and no access to the WebExtensions APIs");
await contentPage2.close();
}
await extension.unload();
});
add_task(async function test_userScripts_exported_APIs() {
async function background() {
const matches = ["http://localhost/*/file_sample.html"];
await browser.runtime.onMessage.addListener(async (msg, sender) => {
return {bgPageReply: true};
});
async function userScript() {
// Explicitly retrieve the custom exported API methods
// to prevent eslint to raise a no-undef validation
// error for them.
const {
US_sync_api,
US_async_api_with_callback,
US_send_api_results,
} = this;
this.userScriptGlobalVar = "global-sandbox-value";
const syncAPIResult = US_sync_api("param1", "param2");
const cb = (cbParam) => {
return `callback param: ${JSON.stringify(cbParam)}`;
};
const cb2 = cb;
const asyncAPIResult = await US_async_api_with_callback("param3", cb, cb2);
let expectedError;
// This is expect to raise an exception due to the window parameter which can't
// be cloned.
try {
US_sync_api(window);
} catch (err) {
expectedError = err.message;
}
US_send_api_results({syncAPIResult, asyncAPIResult, expectedError});
}
await browser.userScripts.register({
js: [{
code: `(${userScript})();`,
}],
runAt: "document_end",
matches,
scriptMetadata: {
name: "test-user-script-exported-apis",
},
});
browser.test.sendMessage("background-ready");
}
function apiScript() {
// Redefine Promise and Error globals to verify that it doesn't break the WebExtensions internals
// that are going to use them.
this.Promise = {};
this.Error = {};
browser.userScripts.setScriptAPIs({
US_sync_api([param1, param2], scriptMetadata, scriptGlobal) {
browser.test.assertEq("test-user-script-exported-apis", scriptMetadata.name);
browser.test.assertEq("param1", param1, "Got the expected parameter value");
browser.test.assertEq("param2", param2, "Got the expected parameter value");
browser.test.sendMessage("US_sync_api", {param1, param2});
return "returned_value";
},
async US_async_api_with_callback([param, cb, cb2], scriptMetadata, scriptGlobal) {
browser.test.assertEq("function", typeof cb, "Got a callback function parameter");
browser.test.assertTrue(cb === cb2, "Got the same cloned function for the same function parameter");
browser.runtime.sendMessage({param}).then(bgPageRes => {
// eslint-disable-next-line no-undef
const cbResult = cb(cloneInto(bgPageRes, scriptGlobal));
browser.test.sendMessage("US_async_api_with_callback", cbResult);
});
return "resolved_value";
},
async US_send_api_results([results], scriptMetadata, scriptGlobal) {
browser.test.sendMessage("US_send_api_results", results);
},
});
}
let extensionData = {
manifest: {
permissions: [
"http://localhost/*/file_sample.html",
],
userScripts: {
apiScript: "api-script.js",
},
},
background,
files: {
"api-script.js": apiScript,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
// Ensure that a content page running in a content process and which has been
// already loaded when the content scripts has been registered, it has received
// and registered the expected content scripts.
let contentPage = await ExtensionTestUtils.loadContentPage(`about:blank`);
await extension.startup();
await extension.awaitMessage("background-ready");
await contentPage.loadURL(`${BASE_URL}/file_sample.html`);
info("Wait the userScript to call the exported US_sync_api method");
await extension.awaitMessage("US_sync_api");
info("Wait the userScript to call the exported US_async_api_with_callback method");
const userScriptCallbackResult = await extension.awaitMessage("US_async_api_with_callback");
equal(userScriptCallbackResult, `callback param: {"bgPageReply":true}`,
"Got the expected results when the userScript callback has been called");
info("Wait the userScript to call the exported US_send_api_results method");
const userScriptsAPIResults = await extension.awaitMessage("US_send_api_results");
Assert.deepEqual(userScriptsAPIResults, {
syncAPIResult: "returned_value",
asyncAPIResult: "resolved_value",
expectedError: "Only serializable parameters are supported",
}, "Got the expected userScript API results");
await extension.unload();
await contentPage.close();
});

Просмотреть файл

@ -1,123 +0,0 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const HISTOGRAM = "WEBEXT_USER_SCRIPT_INJECTION_MS";
const HISTOGRAM_KEYED = "WEBEXT_USER_SCRIPT_INJECTION_MS_BY_ADDONID";
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
add_task(async function test_userScripts_telemetry() {
function apiScript() {
browser.userScripts.setScriptAPIs({
US_test_sendMessage([msg, data], scriptMetadata, scriptGlobal) {
browser.test.sendMessage(msg, {data, scriptMetadata});
},
});
}
async function background() {
const code = `
US_test_sendMessage("userScript-run", {location: window.location.href});
`;
await browser.userScripts.register({
js: [{code}],
matches: ["http://*/*/file_sample.html"],
runAt: "document_end",
scriptMetadata: {
name: "test-user-script-telemetry",
},
});
browser.test.sendMessage("userScript-registered");
}
let testExtensionDef = {
manifest: {
permissions: [
"http://*/*/file_sample.html",
],
userScripts: {
apiScript: "api-script.js",
},
},
background,
files: {
"api-script.js": apiScript,
},
};
let extension = ExtensionTestUtils.loadExtension(testExtensionDef);
let extension2 = ExtensionTestUtils.loadExtension(testExtensionDef);
let contentPage = await ExtensionTestUtils.loadContentPage("about:blank");
clearHistograms();
let process = IS_OOP ? "content" : "parent";
ok(!(HISTOGRAM in getSnapshots(process)), `No data recorded for histogram: ${HISTOGRAM}.`);
ok(!(HISTOGRAM_KEYED in getKeyedSnapshots(process)),
`No data recorded for keyed histogram: ${HISTOGRAM_KEYED}.`);
await extension.startup();
await extension.awaitMessage("userScript-registered");
let extensionId = extension.extension.id;
ok(!(HISTOGRAM in getSnapshots(process)),
`No data recorded for histogram after startup: ${HISTOGRAM}.`);
ok(!(HISTOGRAM_KEYED in getKeyedSnapshots(process)),
`No data recorded for keyed histogram: ${HISTOGRAM_KEYED}.`);
let url = `${BASE_URL}/file_sample.html`;
contentPage.loadURL(url);
const res = await extension.awaitMessage("userScript-run");
Assert.deepEqual(res, {
data: {location: url},
scriptMetadata: {name: "test-user-script-telemetry"},
}, "The userScript has been executed on the content page as expected");
await promiseTelemetryRecorded(HISTOGRAM, process, 1);
await promiseKeyedTelemetryRecorded(HISTOGRAM_KEYED, process, extensionId, 1);
equal(arraySum(getSnapshots(process)[HISTOGRAM].counts), 1,
`Data recorded for histogram: ${HISTOGRAM}.`);
equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId].counts), 1,
`Data recorded for histogram: ${HISTOGRAM_KEYED} with key ${extensionId}.`);
await contentPage.close();
await extension.unload();
await extension2.startup();
await extension2.awaitMessage("userScript-registered");
let extensionId2 = extension2.extension.id;
equal(arraySum(getSnapshots(process)[HISTOGRAM].counts), 1,
`No data recorded for histogram after startup: ${HISTOGRAM}.`);
equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId].counts), 1,
`No new data recorded for histogram after extension2 startup: ${HISTOGRAM_KEYED} with key ${extensionId}.`);
ok(!(extensionId2 in getKeyedSnapshots(process)[HISTOGRAM_KEYED]),
`No data recorded for histogram after startup: ${HISTOGRAM_KEYED} with key ${extensionId2}.`);
contentPage = await ExtensionTestUtils.loadContentPage(url);
const res2 = await extension2.awaitMessage("userScript-run");
Assert.deepEqual(res2, {
data: {location: url},
scriptMetadata: {name: "test-user-script-telemetry"},
}, "The userScript has been executed on the content page as expected");
await promiseTelemetryRecorded(HISTOGRAM, process, 2);
await promiseKeyedTelemetryRecorded(HISTOGRAM_KEYED, process, extensionId2, 1);
equal(arraySum(getSnapshots(process)[HISTOGRAM].counts), 2,
`Data recorded for histogram: ${HISTOGRAM}.`);
equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId].counts), 1,
`No new data recorded for histogram: ${HISTOGRAM_KEYED} with key ${extensionId}.`);
equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId2].counts), 1,
`Data recorded for histogram: ${HISTOGRAM_KEYED} with key ${extensionId2}.`);
await contentPage.close();
await extension2.unload();
});

Просмотреть файл

@ -129,8 +129,6 @@ skip-if = os == 'android' # Bug 1258975 on android.
skip-if = os == "android"
[test_ext_unload_frame.js]
skip-if = true # Too frequent intermittent failures
[test_ext_userScripts.js]
[test_ext_userScripts_telemetry.js]
[test_ext_webRequest_auth.js]
skip-if = os == "android" && debug
[test_ext_webRequest_filterResponseData.js]

Просмотреть файл

@ -1,5 +1,5 @@
[DEFAULT]
head = head.js head_remote.js head_e10s.js head_telemetry.js head_storage.js head_handling_user_input.js
head = head.js head_remote.js head_e10s.js head_telemetry.js head_storage.js
tail =
firefox-appdir = browser
skip-if = appname == "thunderbird" || os == "android"

Просмотреть файл

@ -1,5 +1,5 @@
[DEFAULT]
head = head.js head_telemetry.js head_storage.js head_handling_user_input.js
head = head.js head_telemetry.js head_storage.js
firefox-appdir = browser
dupe-manifest =
support-files =

Просмотреть файл

@ -13718,29 +13718,6 @@
"releaseChannelCollection": "opt-out",
"description": "The number of times a storage.local backend data migration has been completed and results in one of the categories."
},
"WEBEXT_USER_SCRIPT_INJECTION_MS": {
"record_in_processes": ["main", "content"],
"alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
"bug_numbers": [1470466],
"expires_in_version": "67",
"kind": "exponential",
"releaseChannelCollection": "opt-out",
"high": 50000,
"n_buckets": 100,
"description": "The amount of time it takes for userScripts from a WebExtension to be injected into a window."
},
"WEBEXT_USER_SCRIPT_INJECTION_MS_BY_ADDONID": {
"record_in_processes": ["main", "content"],
"alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
"bug_numbers": [1470466],
"expires_in_version": "67",
"kind": "exponential",
"releaseChannelCollection": "opt-out",
"high": 50000,
"n_buckets": 100,
"description": "The amount of time it takes for userScripts from a WebExtension to be injected into a window, keyed by addon id.",
"keyed": true
},
"EXTENSION_UPDATE_TYPE": {
"record_in_processes": ["main"],
"alert_emails": ["addons-dev-internal@mozilla.com"],