gecko-dev/toolkit/components/extensions/ExtensionTestCommon.jsm
Luca Greco 665f72549b Bug 1710917 - Introduced internal:svgContextPropertiesAllowed webextension permission. r=dholbert,mixedpuppy
This patch does change the extension-related special case in SVGContextPaint::IsAllowedForImageFromURI
to check the "internal:svgContextPropertiesAllowed" extension permission, and move the existing
criteria in the Extension class (which takes care of adding the internal permission when those
criteria as met).

This patch does not contain yet a new explicit test case for the new internal permission (which is
part of the patch build on top of this one and attached to the same bugzilla issue), but it does
pass the existing mochitest-chrome (test_chrome_ext_svg_context_fill.html).

Differential Revision: https://phabricator.services.mozilla.com/D115835
2021-06-01 14:46:03 +00:00

490 lines
13 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/**
* This module contains extension testing helper logic which is common
* between all test suites.
*/
/* exported ExtensionTestCommon, MockExtension */
var EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["TextEncoder"]);
ChromeUtils.defineModuleGetter(
this,
"AddonManager",
"resource://gre/modules/AddonManager.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Extension",
"resource://gre/modules/Extension.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionPermissions",
"resource://gre/modules/ExtensionPermissions.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FileUtils",
"resource://gre/modules/FileUtils.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyGetter(
this,
"apiManager",
() => ExtensionParent.apiManager
);
const { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm"
);
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator"
);
const { flushJarCache } = ExtensionUtils;
const { instanceOf } = ExtensionCommon;
XPCOMUtils.defineLazyGetter(this, "console", () =>
ExtensionCommon.getConsole()
);
var ExtensionTestCommon;
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
* uninstalles it on shutdown().
*
* @param {string} id
* @param {nsIFile} file
* @param {nsIURI} rootURI
* @param {string} installType
*/
class MockExtension {
constructor(file, rootURI, addonData) {
this.id = null;
this.file = file;
this.rootURI = rootURI;
this.installType = addonData.useAddonManager;
this.addonData = addonData;
this.addon = null;
let promiseEvent = eventName =>
new Promise(resolve => {
let onstartup = async (msg, extension) => {
this.maybeSetID(extension.rootURI, extension.id);
if (!this.id && this.addonPromise) {
await this.addonPromise;
}
if (extension.id == this.id) {
apiManager.off(eventName, onstartup);
this._extension = extension;
resolve(extension);
}
};
apiManager.on(eventName, onstartup);
});
this._extension = null;
this._extensionPromise = promiseEvent("startup");
this._readyPromise = promiseEvent("ready");
this._uninstallPromise = promiseEvent("uninstall-complete");
}
maybeSetID(uri, id) {
if (
!this.id &&
uri instanceof Ci.nsIJARURI &&
uri.JARFile.QueryInterface(Ci.nsIFileURL).file.equals(this.file)
) {
this.id = id;
}
}
testMessage(...args) {
return this._extension.testMessage(...args);
}
get tabManager() {
return this._extension.tabManager;
}
on(...args) {
this._extensionPromise.then(extension => {
extension.on(...args);
});
// Extension.jsm emits a "startup" event on |extension| before emitting the
// "startup" event on |apiManager|. Trigger the "startup" event anyway, to
// make sure that the MockExtension behaves like an Extension with regards
// to the startup event.
if (args[0] === "startup" && !this._extension) {
this._extensionPromise.then(extension => {
args[1]("startup", extension);
});
}
}
off(...args) {
this._extensionPromise.then(extension => {
extension.off(...args);
});
}
_setIncognitoOverride() {
let { addonData } = this;
if (addonData && addonData.incognitoOverride) {
try {
let { id } = addonData.manifest.applications.gecko;
if (id) {
return ExtensionTestCommon.setIncognitoOverride({ id, addonData });
}
} catch (e) {}
throw new Error(
"Extension ID is required for setting incognito permission."
);
}
}
async startup() {
await this._setIncognitoOverride();
if (this.installType == "temporary") {
return AddonManager.installTemporaryAddon(this.file).then(async addon => {
this.addon = addon;
this.id = addon.id;
return this._readyPromise;
});
} else if (this.installType == "permanent") {
this.addonPromise = new Promise(resolve => {
this.resolveAddon = resolve;
});
let install = await AddonManager.getInstallForFile(this.file);
return new Promise((resolve, reject) => {
let listener = {
onInstallFailed: reject,
onInstallEnded: async (install, newAddon) => {
this.addon = newAddon;
this.id = newAddon.id;
this.resolveAddon(newAddon);
resolve(this._readyPromise);
},
};
install.addListener(listener);
install.install();
});
}
throw new Error("installType must be one of: temporary, permanent");
}
shutdown() {
this.addon.uninstall();
return this.cleanupGeneratedFile();
}
cleanupGeneratedFile() {
return this._extensionPromise
.then(extension => {
return extension.broadcast("Extension:FlushJarCache", {
path: this.file.path,
});
})
.then(() => {
return OS.File.remove(this.file.path);
});
}
}
function provide(obj, keys, value, override = false) {
if (keys.length == 1) {
if (!(keys[0] in obj) || override) {
obj[keys[0]] = value;
}
} else {
if (!(keys[0] in obj)) {
obj[keys[0]] = {};
}
provide(obj[keys[0]], keys.slice(1), value, override);
}
}
ExtensionTestCommon = class ExtensionTestCommon {
/**
* This code is designed to make it easy to test a WebExtension
* without creating a bunch of files. Everything is contained in a
* single JS object.
*
* Properties:
* "background": "<JS code>"
* A script to be loaded as the background script.
* The "background" section of the "manifest" property is overwritten
* if this is provided.
* "manifest": {...}
* Contents of manifest.json
* "files": {"filename1": "contents1", ...}
* Data to be included as files. Can be referenced from the manifest.
* If a manifest file is provided here, it takes precedence over
* a generated one. Always use "/" as a directory separator.
* Directories should appear here only implicitly (as a prefix
* to file names)
*
* To make things easier, the value of "background" and "files"[] can
* be a function, which is converted to source that is run.
*
* @param {object} data
* @returns {object}
*/
static generateFiles(data) {
let files = {};
Object.assign(files, data.files);
let manifest = data.manifest;
if (!manifest) {
manifest = {};
}
provide(manifest, ["name"], "Generated extension");
provide(manifest, ["manifest_version"], 2);
provide(manifest, ["version"], "1.0");
if (data.background) {
let bgScript = uuidGen.generateUUID().number + ".js";
provide(manifest, ["background", "scripts"], [bgScript], true);
files[bgScript] = data.background;
}
provide(files, ["manifest.json"], JSON.stringify(manifest));
for (let filename in files) {
let contents = files[filename];
if (typeof contents == "function") {
files[filename] = this.serializeScript(contents);
} else if (
typeof contents != "string" &&
!instanceOf(contents, "ArrayBuffer")
) {
files[filename] = JSON.stringify(contents);
}
}
return files;
}
/**
* Write an xpi file to disk for a webextension.
* The generated extension is stored in the system temporary directory,
* and an nsIFile object pointing to it is returned.
*
* @param {object} data In the format handled by generateFiles.
* @returns {nsIFile}
*/
static generateXPI(data) {
let files = this.generateFiles(data);
return this.generateZipFile(files);
}
static generateZipFile(files, baseName = "generated-extension.xpi") {
let ZipWriter = Components.Constructor(
"@mozilla.org/zipwriter;1",
"nsIZipWriter"
);
let zipW = new ZipWriter();
let file = FileUtils.getFile("TmpD", [baseName]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
const MODE_WRONLY = 0x02;
const MODE_TRUNCATE = 0x20;
zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
// Needs to be in microseconds for some reason.
let time = Date.now() * 1000;
function generateFile(filename) {
let components = filename.split("/");
let path = "";
for (let component of components.slice(0, -1)) {
path += component + "/";
if (!zipW.hasEntry(path)) {
zipW.addEntryDirectory(path, time, false);
}
}
}
for (let filename in files) {
let script = files[filename];
if (!instanceOf(script, "ArrayBuffer")) {
script = new TextEncoder("utf-8").encode(script).buffer;
}
let stream = Cc[
"@mozilla.org/io/arraybuffer-input-stream;1"
].createInstance(Ci.nsIArrayBufferInputStream);
stream.setData(script, 0, script.byteLength);
generateFile(filename);
zipW.addEntryStream(filename, time, 0, stream, false);
}
zipW.close();
return file;
}
/**
* Properly serialize a function into eval-able code string.
*
* @param {function} script
* @returns {string}
*/
static serializeFunction(script) {
// Serialization of object methods doesn't include `function` anymore.
const method = /^(async )?(?:(\w+)|"(\w+)\.js")\(/;
let code = script.toString();
let match = code.match(method);
if (match && match[2] !== "function") {
code = code.replace(method, "$1function $2$3(");
}
return code;
}
/**
* Properly serialize a script into eval-able code string.
*
* @param {string|function|Array} script
* @returns {string}
*/
static serializeScript(script) {
if (Array.isArray(script)) {
return Array.from(script, this.serializeScript, this).join(";");
}
if (typeof script !== "function") {
return script;
}
return `(${this.serializeFunction(script)})();`;
}
static setIncognitoOverride(extension) {
let { id, addonData } = extension;
if (!addonData || !addonData.incognitoOverride) {
return;
}
if (addonData.incognitoOverride == "not_allowed") {
return ExtensionPermissions.remove(id, {
permissions: ["internal:privateBrowsingAllowed"],
origins: [],
});
}
return ExtensionPermissions.add(id, {
permissions: ["internal:privateBrowsingAllowed"],
origins: [],
});
}
static setExtensionID(data) {
try {
if (data.manifest.applications.gecko.id) {
return;
}
} catch (e) {
// No ID is set.
}
provide(
data,
["manifest", "applications", "gecko", "id"],
uuidGen.generateUUID().number
);
}
/**
* Generates a new extension using |Extension.generateXPI|, and initializes a
* new |Extension| instance which will execute it.
*
* @param {object} data
* @returns {Extension}
*/
static generate(data) {
let file = this.generateXPI(data);
flushJarCache(file.path);
Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {
path: file.path,
});
let fileURI = Services.io.newFileURI(file);
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/");
// This may be "temporary" or "permanent".
if (data.useAddonManager) {
return new MockExtension(file, jarURI, data);
}
let id;
if (data.manifest) {
if (data.manifest.applications && data.manifest.applications.gecko) {
id = data.manifest.applications.gecko.id;
} else if (
data.manifest.browser_specific_settings &&
data.manifest.browser_specific_settings.gecko
) {
id = data.manifest.browser_specific_settings.gecko.id;
}
}
if (!id) {
id = uuidGen.generateUUID().number;
}
let signedState = AddonManager.SIGNEDSTATE_SIGNED;
if (data.isPrivileged) {
signedState = AddonManager.SIGNEDSTATE_PRIVILEGED;
}
if (data.isSystem) {
signedState = AddonManager.SIGNEDSTATE_SYSTEM;
}
return new Extension(
{
id,
resourceURI: jarURI,
cleanupFile: file,
signedState,
incognitoOverride: data.incognitoOverride,
temporarilyInstalled: !!data.temporarilyInstalled,
TEST_NO_ADDON_MANAGER: true,
},
data.startupReason
);
}
};