From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- .../specialpowers/content/SpecialPowersChild.jsm | 2232 ++++++++++++++++++++ 1 file changed, 2232 insertions(+) create mode 100644 testing/specialpowers/content/SpecialPowersChild.jsm (limited to 'testing/specialpowers/content/SpecialPowersChild.jsm') diff --git a/testing/specialpowers/content/SpecialPowersChild.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm new file mode 100644 index 0000000000..203495dc71 --- /dev/null +++ b/testing/specialpowers/content/SpecialPowersChild.jsm @@ -0,0 +1,2232 @@ +/* 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/. */ +/* This code is loaded in every child process that is started by mochitest. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["SpecialPowersChild"]; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "MockFilePicker", + "resource://specialpowers/MockFilePicker.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MockColorPicker", + "resource://specialpowers/MockColorPicker.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MockPermissionPrompt", + "resource://specialpowers/MockPermissionPrompt.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "SpecialPowersSandbox", + "resource://specialpowers/SpecialPowersSandbox.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "WrapPrivileged", + "resource://specialpowers/WrapPrivileged.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PerTestCoverageUtils", + "resource://testing-common/PerTestCoverageUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ContentTaskUtils", + "resource://testing-common/ContentTaskUtils.jsm" +); + +Cu.crashIfNotInAutomation(); + +function bindDOMWindowUtils(aWindow) { + return aWindow && WrapPrivileged.wrap(aWindow.windowUtils, aWindow); +} + +function defineSpecialPowers(sp) { + let window = sp.contentWindow; + window.SpecialPowers = sp; + if (window === window.wrappedJSObject) { + return; + } + // We can't use a generic |defineLazyGetter| because it does not + // allow customizing the re-definition behavior. + Object.defineProperty(window.wrappedJSObject, "SpecialPowers", { + get() { + let value = WrapPrivileged.wrap(sp, window); + // If we bind |window.wrappedJSObject| when defining the getter + // and use it here, it might become a dead wrapper. + // We have to retrieve |wrappedJSObject| again. + Object.defineProperty(window.wrappedJSObject, "SpecialPowers", { + configurable: true, + enumerable: true, + value, + writable: true, + }); + return value; + }, + configurable: true, + enumerable: true, + }); +} + +// SPConsoleListener reflects nsIConsoleMessage objects into JS in a +// tidy, XPCOM-hiding way. Messages that are nsIScriptError objects +// have their properties exposed in detail. It also auto-unregisters +// itself when it receives a "sentinel" message. +function SPConsoleListener(callback, contentWindow) { + this.callback = callback; + this.contentWindow = contentWindow; +} + +SPConsoleListener.prototype = { + // Overload the observe method for both nsIConsoleListener and nsIObserver. + // The topic will be null for nsIConsoleListener. + observe(msg, topic) { + let m = { + message: msg.message, + errorMessage: null, + cssSelectors: null, + sourceName: null, + sourceLine: null, + lineNumber: null, + columnNumber: null, + category: null, + windowID: null, + isScriptError: false, + isConsoleEvent: false, + isWarning: false, + }; + if (msg instanceof Ci.nsIScriptError) { + m.errorMessage = msg.errorMessage; + m.cssSelectors = msg.cssSelectors; + m.sourceName = msg.sourceName; + m.sourceLine = msg.sourceLine; + m.lineNumber = msg.lineNumber; + m.columnNumber = msg.columnNumber; + m.category = msg.category; + m.windowID = msg.outerWindowID; + m.innerWindowID = msg.innerWindowID; + m.isScriptError = true; + m.isWarning = (msg.flags & Ci.nsIScriptError.warningFlag) === 1; + } else if (topic === "console-api-log-event") { + // This is a dom/console event. + let unwrapped = msg.wrappedJSObject; + m.errorMessage = unwrapped.arguments[0]; + m.sourceName = unwrapped.filename; + m.lineNumber = unwrapped.lineNumber; + m.columnNumber = unwrapped.columnNumber; + m.windowID = unwrapped.ID; + m.innerWindowID = unwrapped.innerID; + m.isConsoleEvent = true; + m.isWarning = unwrapped.level === "warning"; + } + + Object.freeze(m); + + // Run in a separate runnable since console listeners aren't + // supposed to touch content and this one might. + Services.tm.dispatchToMainThread(() => { + this.callback.call(undefined, Cu.cloneInto(m, this.contentWindow)); + }); + + if (!m.isScriptError && !m.isConsoleEvent && m.message === "SENTINEL") { + Services.obs.removeObserver(this, "console-api-log-event"); + Services.console.unregisterListener(this); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener", "nsIObserver"]), +}; + +class SpecialPowersChild extends JSWindowActorChild { + constructor() { + super(); + + this._windowID = null; + + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = {}; + this._crashDumpDir = null; + this._serviceWorkerRegistered = false; + this._serviceWorkerCleanUpRequests = new Map(); + Object.defineProperty(this, "Components", { + configurable: true, + enumerable: true, + value: Components, + }); + this._createFilesOnError = null; + this._createFilesOnSuccess = null; + + this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set()); + + this._consoleListeners = []; + this._spawnTaskImports = {}; + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = {}; + this._crashDumpDir = null; + this._mfl = null; + this._asyncObservers = new WeakMap(); + this._xpcomabi = null; + this._os = null; + this._pu = null; + + this._nextExtensionID = 0; + this._extensionListeners = null; + + WrapPrivileged.disableAutoWrap( + this.unwrap, + this.isWrapper, + this.wrapCallback, + this.wrapCallbackObject, + this.setWrapped, + this.nondeterministicGetWeakMapKeys, + this.snapshotWindowWithOptions, + this.snapshotWindow, + this.snapshotRect, + this.getDOMRequestService + ); + } + + observe(aSubject, aTopic, aData) { + // Ignore the "{chrome/content}-document-global-created" event. It + // is only observed to force creation of the actor. + } + + actorCreated() { + this.attachToWindow(); + } + + attachToWindow() { + let window = this.contentWindow; + // We should not invoke the getter. + if (!("SpecialPowers" in window.wrappedJSObject)) { + this._windowID = window.windowGlobalChild.innerWindowId; + + defineSpecialPowers(this); + } + } + + get window() { + return this.contentWindow; + } + + // Hack around devtools sometimes trying to JSON stringify us. + toJSON() { + return {}; + } + + toString() { + return "[SpecialPowers]"; + } + sanityCheck() { + return "foo"; + } + + _addMessageListener(msgname, listener) { + this._messageListeners.get(msgname).add(listener); + } + + _removeMessageListener(msgname, listener) { + this._messageListeners.get(msgname).delete(listener); + } + + receiveMessage(message) { + if (this._messageListeners.has(message.name)) { + for (let listener of this._messageListeners.get(message.name)) { + try { + if (typeof listener === "function") { + listener(message); + } else { + listener.receiveMessage(message); + } + } catch (e) { + Cu.reportError(e); + } + } + } + + switch (message.name) { + case "SPProcessCrashService": + if (message.json.type == "crash-observed") { + for (let e of message.json.dumpIDs) { + this._encounteredCrashDumpFiles.push(e.id + "." + e.extension); + } + } + break; + + case "SPServiceWorkerRegistered": + this._serviceWorkerRegistered = message.data.registered; + break; + + case "SpecialPowers.FilesCreated": + var createdHandler = this._createFilesOnSuccess; + this._createFilesOnSuccess = null; + this._createFilesOnError = null; + if (createdHandler) { + createdHandler(Cu.cloneInto(message.data, this.contentWindow)); + } + break; + + case "SpecialPowers.FilesError": + var errorHandler = this._createFilesOnError; + this._createFilesOnSuccess = null; + this._createFilesOnError = null; + if (errorHandler) { + errorHandler(message.data); + } + break; + + case "Spawn": + let { task, args, caller, taskId, imports } = message.data; + return this._spawnTask(task, args, caller, taskId, imports); + + case "Assert": + { + if ("info" in message.data) { + this.SimpleTest.info(message.data.info); + break; + } + + // An assertion has been done in a mochitest chrome script + let { name, passed, stack, diag, expectFail } = message.data; + + let { SimpleTest } = this; + if (SimpleTest) { + let expected = expectFail ? "fail" : "pass"; + SimpleTest.record(passed, name, diag, stack, expected); + } else { + // Well, this is unexpected. + dump(name + "\n"); + } + } + break; + } + return undefined; + } + + registerProcessCrashObservers() { + this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" }); + } + + unregisterProcessCrashObservers() { + this.sendAsyncMessage("SPProcessCrashService", { + op: "unregister-observer", + }); + } + + /* + * Privileged object wrapping API + * + * Usage: + * var wrapper = SpecialPowers.wrap(obj); + * wrapper.privilegedMethod(); wrapper.privilegedProperty; + * obj === SpecialPowers.unwrap(wrapper); + * + * These functions provide transparent access to privileged objects using + * various pieces of deep SpiderMagic. Conceptually, a wrapper is just an + * object containing a reference to the underlying object, where all method + * calls and property accesses are transparently performed with the System + * Principal. Moreover, objects obtained from the wrapper (including properties + * and method return values) are wrapped automatically. Thus, after a single + * call to SpecialPowers.wrap(), the wrapper layer is transitively maintained. + * + * Known Issues: + * + * - The wrapping function does not preserve identity, so + * SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543. + * + * - The wrapper cannot see expando properties on unprivileged DOM objects. + * That is to say, the wrapper uses Xray delegation. + * + * - The wrapper sometimes guesses certain ES5 attributes for returned + * properties. This is explained in a comment in the wrapper code above, + * and shouldn't be a problem. + */ + wrap(obj) { + return obj; + } + unwrap(obj) { + return WrapPrivileged.unwrap(obj); + } + isWrapper(val) { + return WrapPrivileged.isWrapper(val); + } + + /* + * Wrap objects on a specified global. + */ + wrapFor(obj, win) { + return WrapPrivileged.wrap(obj, win); + } + + /* + * When content needs to pass a callback or a callback object to an API + * accessed over SpecialPowers, that API may sometimes receive arguments for + * whom it is forbidden to create a wrapper in content scopes. As such, we + * need a layer to wrap the values in SpecialPowers wrappers before they ever + * reach content. + */ + wrapCallback(func) { + return WrapPrivileged.wrapCallback(func, this.contentWindow); + } + wrapCallbackObject(obj) { + return WrapPrivileged.wrapCallbackObject(obj, this.contentWindow); + } + + /* + * Used for assigning a property to a SpecialPowers wrapper, without unwrapping + * the value that is assigned. + */ + setWrapped(obj, prop, val) { + if (!WrapPrivileged.isWrapper(obj)) { + throw new Error( + "You only need to use this for SpecialPowers wrapped objects" + ); + } + + obj = WrapPrivileged.unwrap(obj); + return Reflect.set(obj, prop, val); + } + + /* + * Create blank privileged objects to use as out-params for privileged functions. + */ + createBlankObject() { + return {}; + } + + /* + * Because SpecialPowers wrappers don't preserve identity, comparing with == + * can be hazardous. Sometimes we can just unwrap to compare, but sometimes + * wrapping the underlying object into a content scope is forbidden. This + * function strips any wrappers if they exist and compare the underlying + * values. + */ + compare(a, b) { + return WrapPrivileged.unwrap(a) === WrapPrivileged.unwrap(b); + } + + get MockFilePicker() { + return MockFilePicker; + } + + get MockColorPicker() { + return MockColorPicker; + } + + get MockPermissionPrompt() { + return MockPermissionPrompt; + } + + quit() { + this.sendAsyncMessage("SpecialPowers.Quit", {}); + } + + // fileRequests is an array of file requests. Each file request is an object. + // A request must have a field |name|, which gives the base of the name of the + // file to be created in the profile directory. If the request has a |data| field + // then that data will be written to the file. + createFiles(fileRequests, onCreation, onError) { + return this.sendQuery("SpecialPowers.CreateFiles", fileRequests).then( + files => onCreation(Cu.cloneInto(files, this.contentWindow)), + onError + ); + } + + // Remove the files that were created using |SpecialPowers.createFiles()|. + // This will be automatically called by |SimpleTest.finish()|. + removeFiles() { + this.sendAsyncMessage("SpecialPowers.RemoveFiles", {}); + } + + executeAfterFlushingMessageQueue(aCallback) { + return this.sendQuery("Ping").then(aCallback); + } + + async registeredServiceWorkers() { + // For the time being, if parent_intercept is false, we can assume that + // ServiceWorkers registered by the current test are all known to the SWM in + // this process. + if ( + !Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false) + ) { + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let regs = swm.getAllRegistrations(); + + // XXX This is shared with SpecialPowersAPIParent.jsm + let workers = new Array(regs.length); + for (let i = 0; i < workers.length; ++i) { + let { scope, scriptSpec } = regs.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + workers[i] = { scope, scriptSpec }; + } + + return workers; + } + + // Please see the comment in SpecialPowersObserver.jsm above + // this._serviceWorkerListener's assignment for what this returns. + if (this._serviceWorkerRegistered) { + // This test registered at least one service worker. Send a synchronous + // call to the parent to make sure that it called unregister on all of its + // service workers. + let { workers } = await this.sendQuery("SPCheckServiceWorkers"); + return workers; + } + + return []; + } + + /* + * Load a privileged script that runs same-process. This is different from + * |loadChromeScript|, which will run in the parent process in e10s mode. + */ + loadPrivilegedScript(aFunction) { + var str = "(" + aFunction.toString() + ")();"; + let gGlobalObject = Cu.getGlobalForObject(this); + let sb = Cu.Sandbox(gGlobalObject); + var window = this.contentWindow; + var mc = new window.MessageChannel(); + sb.port = mc.port1; + let blob = new Blob([str], { type: "application/javascript" }); + let blobUrl = URL.createObjectURL(blob); + Services.scriptloader.loadSubScript(blobUrl, sb); + + return mc.port2; + } + + _readUrlAsString(aUrl) { + // Fetch script content as we can't use scriptloader's loadSubScript + // to evaluate http:// urls... + var scriptableStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].getService(Ci.nsIScriptableInputStream); + + var channel = NetUtil.newChannel({ + uri: aUrl, + loadUsingSystemPrincipal: true, + }); + var input = channel.open(); + scriptableStream.init(input); + + var str; + var buffer = []; + + while ((str = scriptableStream.read(4096))) { + buffer.push(str); + } + + var output = buffer.join(""); + + scriptableStream.close(); + input.close(); + + var status; + if (channel instanceof Ci.nsIHttpChannel) { + status = channel.responseStatus; + } + + if (status == 404) { + throw new Error( + `Error while executing chrome script '${aUrl}':\n` + + "The script doesn't exist. Ensure you have registered it in " + + "'support-files' in your mochitest.ini." + ); + } + + return output; + } + + loadChromeScript(urlOrFunction, sandboxOptions) { + // Create a unique id for this chrome script + let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator + ); + let id = uuidGenerator.generateUUID().toString(); + + // Tells chrome code to evaluate this chrome script + let scriptArgs = { id, sandboxOptions }; + if (typeof urlOrFunction == "function") { + scriptArgs.function = { + body: "(" + urlOrFunction.toString() + ")();", + name: urlOrFunction.name, + }; + } else { + // Note: We need to do this in the child since, even though + // `_readUrlAsString` pretends to be synchronous, its channel + // winds up spinning the event loop when loading HTTP URLs. That + // leads to unexpected out-of-order operations if the child sends + // a message immediately after loading the script. + scriptArgs.function = { + body: this._readUrlAsString(urlOrFunction), + }; + scriptArgs.url = urlOrFunction; + } + this.sendAsyncMessage("SPLoadChromeScript", scriptArgs); + + // Returns a MessageManager like API in order to be + // able to communicate with this chrome script + let listeners = []; + let chromeScript = { + addMessageListener: (name, listener) => { + listeners.push({ name, listener }); + }, + + promiseOneMessage: name => + new Promise(resolve => { + chromeScript.addMessageListener(name, function listener(message) { + chromeScript.removeMessageListener(name, listener); + resolve(message); + }); + }), + + removeMessageListener: (name, listener) => { + listeners = listeners.filter( + o => o.name != name || o.listener != listener + ); + }, + + sendAsyncMessage: (name, message) => { + this.sendAsyncMessage("SPChromeScriptMessage", { id, name, message }); + }, + + sendQuery: (name, message) => { + return this.sendQuery("SPChromeScriptMessage", { id, name, message }); + }, + + destroy: () => { + listeners = []; + this._removeMessageListener("SPChromeScriptMessage", chromeScript); + }, + + receiveMessage: aMessage => { + let messageId = aMessage.json.id; + let name = aMessage.json.name; + let message = aMessage.json.message; + if (this.contentWindow) { + message = new StructuredCloneHolder(message).deserialize( + this.contentWindow + ); + } + // Ignore message from other chrome script + if (messageId != id) { + return null; + } + + let result; + if (aMessage.name == "SPChromeScriptMessage") { + for (let listener of listeners.filter(o => o.name == name)) { + result = listener.listener(message); + } + } + return result; + }, + }; + this._addMessageListener("SPChromeScriptMessage", chromeScript); + + return chromeScript; + } + + async importInMainProcess(importString) { + var message = await this.sendQuery("SPImportInMainProcess", importString); + if (message.hadError) { + throw new Error( + "SpecialPowers.importInMainProcess failed with error " + + message.errorMessage + ); + } + } + + get Services() { + return Services; + } + + /* + * Convenient shortcuts to the standard Components abbreviations. + */ + get Cc() { + return Cc; + } + get Ci() { + return Ci; + } + get Cu() { + return Cu; + } + get Cr() { + return Cr; + } + + get addProfilerMarker() { + return ChromeUtils.addProfilerMarker; + } + + get DOMWindowUtils() { + return this.contentWindow.windowUtils; + } + + getDOMWindowUtils(aWindow) { + if (aWindow == this.contentWindow) { + return aWindow.windowUtils; + } + + return bindDOMWindowUtils(Cu.unwaiveXrays(aWindow)); + } + + async toggleMuteState(aMuted, aWindow) { + let actor = aWindow + ? aWindow.windowGlobalChild.getActor("SpecialPowers") + : this; + return actor.sendQuery("SPToggleMuteAudio", { mute: aMuted }); + } + + /* + * A method to get a DOMParser that can't parse XUL. + */ + getNoXULDOMParser() { + // If we create it with a system subject principal (so it gets a + // nullprincipal), it won't be able to parse XUL by default. + return new DOMParser(); + } + + get InspectorUtils() { + return InspectorUtils; + } + + get PromiseDebugging() { + return PromiseDebugging; + } + + async waitForCrashes(aExpectingProcessCrash) { + if (!aExpectingProcessCrash) { + return; + } + + var crashIds = this._encounteredCrashDumpFiles + .filter(filename => { + return filename.length === 40 && filename.endsWith(".dmp"); + }) + .map(id => { + return id.slice(0, -4); // Strip the .dmp extension to get the ID + }); + + await this.sendQuery("SPProcessCrashManagerWait", { + crashIds, + }); + } + + async removeExpectedCrashDumpFiles(aExpectingProcessCrash) { + var success = true; + if (aExpectingProcessCrash) { + var message = { + op: "delete-crash-dump-files", + filenames: this._encounteredCrashDumpFiles, + }; + if (!(await this.sendQuery("SPProcessCrashService", message))) { + success = false; + } + } + this._encounteredCrashDumpFiles.length = 0; + return success; + } + + async findUnexpectedCrashDumpFiles() { + var self = this; + var message = { + op: "find-crash-dump-files", + crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles, + }; + var crashDumpFiles = await this.sendQuery("SPProcessCrashService", message); + crashDumpFiles.forEach(function(aFilename) { + self._unexpectedCrashDumpFiles[aFilename] = true; + }); + return crashDumpFiles; + } + + removePendingCrashDumpFiles() { + var message = { + op: "delete-pending-crash-dump-files", + }; + return this.sendQuery("SPProcessCrashService", message); + } + + _setTimeout(callback, delay = 0) { + // for mochitest-browser + if (typeof this.chromeWindow != "undefined") { + this.chromeWindow.setTimeout(callback, delay); + } + // for mochitest-plain + else { + this.contentWindow.setTimeout(callback, delay); + } + } + + promiseTimeout(delay) { + return new Promise(resolve => { + this._setTimeout(resolve, delay); + }); + } + + _delayCallbackTwice(callback) { + let delayedCallback = () => { + let delayAgain = aCallback => { + // Using this._setTimeout doesn't work here + // It causes failures in mochtests that use + // multiple pushPrefEnv calls + // For chrome/browser-chrome mochitests + this._setTimeout(aCallback); + }; + delayAgain(delayAgain.bind(this, callback)); + }; + return delayedCallback; + } + + /* apply permissions to the system and when the test case is finished (SimpleTest.finish()) + we will revert the permission back to the original. + + inPermissions is an array of objects where each object has a type, action, context, ex: + [{'type': 'SystemXHR', 'allow': 1, 'context': document}, + {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}] + + Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION + */ + async pushPermissions(inPermissions, callback) { + let permissions = []; + for (let perm of inPermissions) { + let principal = this._getPrincipalFromArg(perm.context); + permissions.push({ + ...perm, + context: null, + principal, + }); + } + + await this.sendQuery("PushPermissions", permissions).then(callback); + await this.promiseTimeout(0); + } + + async popPermissions(callback = null) { + await this.sendQuery("PopPermissions").then(callback); + await this.promiseTimeout(0); + } + + async flushPermissions(callback = null) { + await this.sendQuery("FlushPermissions").then(callback); + await this.promiseTimeout(0); + } + + /* + * This function should be used when specialpowers is in content process but + * it want to get the notification from chrome space. + * + * This function will call Services.obs.addObserver in SpecialPowersObserver + * (that is in chrome process) and forward the data received to SpecialPowers + * via messageManager. + * You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire + * the callback. + * + * To get the expected data, you should modify + * SpecialPowersObserver.prototype._registerObservers.observe. Or the message + * you received from messageManager will only contain 'aData' from Service.obs. + */ + registerObservers(topic) { + var msg = { + op: "add", + observerTopic: topic, + }; + return this.sendQuery("SPObserverService", msg); + } + + setTestPluginEnabledState(newEnabledState, pluginName) { + return this.sendQuery("SPSetTestPluginEnabledState", { + newEnabledState, + pluginName, + }); + } + + async pushPrefEnv(inPrefs, callback = null) { + await this.sendQuery("PushPrefEnv", inPrefs).then(callback); + await this.promiseTimeout(0); + } + + async popPrefEnv(callback = null) { + await this.sendQuery("PopPrefEnv").then(callback); + await this.promiseTimeout(0); + } + + async flushPrefEnv(callback = null) { + await this.sendQuery("FlushPrefEnv").then(callback); + await this.promiseTimeout(0); + } + + _addObserverProxy(notification) { + if (notification in this._proxiedObservers) { + this._addMessageListener( + notification, + this._proxiedObservers[notification] + ); + } + } + _removeObserverProxy(notification) { + if (notification in this._proxiedObservers) { + this._removeMessageListener( + notification, + this._proxiedObservers[notification] + ); + } + } + + addObserver(obs, notification, weak) { + // Make sure the parent side exists, or we won't get any notifications. + this.sendAsyncMessage("Wakeup"); + + this._addObserverProxy(notification); + obs = Cu.waiveXrays(obs); + if ( + typeof obs == "object" && + obs.observe.name != "SpecialPowersCallbackWrapper" + ) { + obs.observe = WrapPrivileged.wrapCallback( + Cu.unwaiveXrays(obs.observe), + this.contentWindow + ); + } + Services.obs.addObserver(obs, notification, weak); + } + removeObserver(obs, notification) { + this._removeObserverProxy(notification); + Services.obs.removeObserver(Cu.waiveXrays(obs), notification); + } + notifyObservers(subject, topic, data) { + Services.obs.notifyObservers(subject, topic, data); + } + + /** + * An async observer is useful if you're listening for a + * notification that normally is only used by C++ code or chrome + * code (so it runs in the SystemGroup), but we need to know about + * it for a test (which runs as web content). If we used + * addObserver, we would assert when trying to enter web content + * from a runnabled labeled by the SystemGroup. An async observer + * avoids this problem. + */ + addAsyncObserver(obs, notification, weak) { + obs = Cu.waiveXrays(obs); + if ( + typeof obs == "object" && + obs.observe.name != "SpecialPowersCallbackWrapper" + ) { + obs.observe = WrapPrivileged.wrapCallback( + Cu.unwaiveXrays(obs.observe), + this.contentWindow + ); + } + let asyncObs = (...args) => { + Services.tm.dispatchToMainThread(() => { + if (typeof obs == "function") { + obs(...args); + } else { + obs.observe.call(undefined, ...args); + } + }); + }; + this._asyncObservers.set(obs, asyncObs); + Services.obs.addObserver(asyncObs, notification, weak); + } + removeAsyncObserver(obs, notification) { + let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs)); + Services.obs.removeObserver(asyncObs, notification); + } + + can_QI(obj) { + return obj.QueryInterface !== undefined; + } + do_QueryInterface(obj, iface) { + return obj.QueryInterface(Ci[iface]); + } + + call_Instanceof(obj1, obj2) { + obj1 = WrapPrivileged.unwrap(obj1); + obj2 = WrapPrivileged.unwrap(obj2); + return obj1 instanceof obj2; + } + + // Returns a privileged getter from an object. GetOwnPropertyDescriptor does + // not work here because xray wrappers don't properly implement it. + // + // This terribleness is used by dom/base/test/test_object.html because + // and tags will spawn plugins if their prototype is touched, + // so we need to get and cache the getter of |hasRunningPlugin| if we want to + // call it without paradoxically spawning the plugin. + do_lookupGetter(obj, name) { + return Object.prototype.__lookupGetter__.call(obj, name); + } + + // Mimic the get*Pref API + getBoolPref(...args) { + return Services.prefs.getBoolPref(...args); + } + getIntPref(...args) { + return Services.prefs.getIntPref(...args); + } + getCharPref(...args) { + return Services.prefs.getCharPref(...args); + } + getComplexValue(prefName, iid) { + return Services.prefs.getComplexValue(prefName, iid); + } + + getParentBoolPref(prefName, defaultValue) { + return this._getParentPref(prefName, "BOOL", { defaultValue }); + } + getParentIntPref(prefName, defaultValue) { + return this._getParentPref(prefName, "INT", { defaultValue }); + } + getParentCharPref(prefName, defaultValue) { + return this._getParentPref(prefName, "CHAR", { defaultValue }); + } + + // Mimic the set*Pref API + setBoolPref(prefName, value) { + return this._setPref(prefName, "BOOL", value); + } + setIntPref(prefName, value) { + return this._setPref(prefName, "INT", value); + } + setCharPref(prefName, value) { + return this._setPref(prefName, "CHAR", value); + } + setComplexValue(prefName, iid, value) { + return this._setPref(prefName, "COMPLEX", value, iid); + } + + // Mimic the clearUserPref API + clearUserPref(prefName) { + let msg = { + op: "clear", + prefName, + prefType: "", + }; + return this.sendQuery("SPPrefService", msg); + } + + // Private pref functions to communicate to chrome + async _getParentPref(prefName, prefType, { defaultValue, iid }) { + let msg = { + op: "get", + prefName, + prefType, + iid, // Only used with complex prefs + defaultValue, // Optional default value + }; + let val = await this.sendQuery("SPPrefService", msg); + if (val == null) { + throw new Error(`Error getting pref '${prefName}'`); + } + return val; + } + _getPref(prefName, prefType, { defaultValue }) { + switch (prefType) { + case "BOOL": + return Services.prefs.getBoolPref(prefName); + case "INT": + return Services.prefs.getIntPref(prefName); + case "CHAR": + return Services.prefs.getCharPref(prefName); + } + return undefined; + } + _setPref(prefName, prefType, prefValue, iid) { + let msg = { + op: "set", + prefName, + prefType, + iid, // Only used with complex prefs + prefValue, + }; + return this.sendQuery("SPPrefService", msg); + } + + _getMUDV(window) { + return window.docShell.contentViewer; + } + // XXX: these APIs really ought to be removed, they're not e10s-safe. + // (also they're pretty Firefox-specific) + _getTopChromeWindow(window) { + return window.browsingContext.topChromeWindow; + } + _getAutoCompletePopup(window) { + return this._getTopChromeWindow(window).document.getElementById( + "PopupAutoComplete" + ); + } + addAutoCompletePopupEventListener(window, eventname, listener) { + this._getAutoCompletePopup(window).addEventListener(eventname, listener); + } + removeAutoCompletePopupEventListener(window, eventname, listener) { + this._getAutoCompletePopup(window).removeEventListener(eventname, listener); + } + get formHistory() { + let tmp = {}; + ChromeUtils.import("resource://gre/modules/FormHistory.jsm", tmp); + return tmp.FormHistory; + } + getFormFillController(window) { + return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService( + Ci.nsIFormFillController + ); + } + attachFormFillControllerTo(window) { + this.getFormFillController().attachPopupElementToDocument( + window.document, + this._getAutoCompletePopup(window) + ); + } + detachFormFillControllerFrom(window) { + this.getFormFillController().detachFromDocument(window.document); + } + isBackButtonEnabled(window) { + return !this._getTopChromeWindow(window) + .document.getElementById("Browser:Back") + .hasAttribute("disabled"); + } + // XXX end of problematic APIs + + addChromeEventListener(type, listener, capture, allowUntrusted) { + this.docShell.chromeEventHandler.addEventListener( + type, + listener, + capture, + allowUntrusted + ); + } + removeChromeEventListener(type, listener, capture) { + this.docShell.chromeEventHandler.removeEventListener( + type, + listener, + capture + ); + } + + async generateMediaControlKeyTestEvent(event) { + await this.sendQuery("SPGenerateMediaControlKeyTestEvent", { event }); + } + + // Note: each call to registerConsoleListener MUST be paired with a + // call to postConsoleSentinel; when the callback receives the + // sentinel it will unregister itself (_after_ calling the + // callback). SimpleTest.expectConsoleMessages does this for you. + // If you register more than one console listener, a call to + // postConsoleSentinel will zap all of them. + registerConsoleListener(callback) { + let listener = new SPConsoleListener(callback, this.contentWindow); + Services.console.registerListener(listener); + + // listen for dom/console events as well + Services.obs.addObserver(listener, "console-api-log-event"); + } + postConsoleSentinel() { + Services.console.logStringMessage("SENTINEL"); + } + resetConsole() { + Services.console.reset(); + } + + getFullZoom(window) { + return BrowsingContext.getFromWindow(window).fullZoom; + } + + getDeviceFullZoom(window) { + return this._getMUDV(window).deviceFullZoomForTest; + } + setFullZoom(window, zoom) { + BrowsingContext.getFromWindow(window).fullZoom = zoom; + } + getTextZoom(window) { + return BrowsingContext.getFromWindow(window).textZoom; + } + setTextZoom(window, zoom) { + BrowsingContext.getFromWindow(window).textZoom = zoom; + } + + getOverrideDPPX(window) { + return this._getMUDV(window).overrideDPPX; + } + setOverrideDPPX(window, dppx) { + this._getMUDV(window).overrideDPPX = dppx; + } + + emulateMedium(window, mediaType) { + this._getMUDV(window).emulateMedium(mediaType); + } + stopEmulatingMedium(window) { + this._getMUDV(window).stopEmulatingMedium(); + } + + // Takes a snapshot of the given window and returns a + // containing the image. When the window is same-process, the canvas + // is returned synchronously. When it is out-of-process (or when a + // BrowsingContext or FrameLoaderOwner is passed instead of a Window), + // a promise which resolves to such a canvas is returned instead. + snapshotWindowWithOptions(content, rect, bgcolor, options) { + function getImageData(rect, bgcolor, options) { + let el = content.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + if (rect === undefined) { + rect = { + top: content.scrollY, + left: content.scrollX, + width: content.innerWidth, + height: content.innerHeight, + }; + } + if (bgcolor === undefined) { + bgcolor = "rgb(255,255,255)"; + } + if (options === undefined) { + options = {}; + } + + el.width = rect.width; + el.height = rect.height; + let ctx = el.getContext("2d"); + + let flags = 0; + for (let option in options) { + flags |= options[option] && ctx[option]; + } + + ctx.drawWindow( + content, + rect.left, + rect.top, + rect.width, + rect.height, + bgcolor, + flags + ); + + return ctx.getImageData(0, 0, el.width, el.height); + } + + let toCanvas = imageData => { + let el = this.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + el.width = imageData.width; + el.height = imageData.height; + + if (ImageData.isInstance(imageData)) { + let ctx = el.getContext("2d"); + ctx.putImageData(imageData, 0, 0); + } + + return el; + }; + + if (Window.isInstance(content)) { + // Hack around tests that try to snapshot 0 width or height + // elements. + if (rect && !(rect.width && rect.height)) { + return toCanvas(rect); + } + + // This is an in-process window. Snapshot it synchronously. + return toCanvas(getImageData(rect, bgcolor, options)); + } + + // This is a remote window or frame. Snapshot it asynchronously and + // return a promise for the result. Alas, consumers expect us to + // return a element rather than an ImageData object, so we + // need to convert the result from the remote snapshot to a local + // canvas. + let promise = this.spawn( + content, + [rect, bgcolor, options], + getImageData + ).then(toCanvas); + if (Cu.isXrayWrapper(this.contentWindow)) { + return new this.contentWindow.Promise((resolve, reject) => { + promise.then(resolve, reject); + }); + } + return promise; + } + + snapshotWindow(win, withCaret, rect, bgcolor) { + return this.snapshotWindowWithOptions(win, rect, bgcolor, { + DRAWWINDOW_DRAW_CARET: withCaret, + }); + } + + snapshotRect(win, rect, bgcolor) { + return this.snapshotWindowWithOptions(win, rect, bgcolor); + } + + gc() { + this.contentWindow.windowUtils.garbageCollect(); + } + + forceGC() { + Cu.forceGC(); + } + + forceShrinkingGC() { + Cu.forceShrinkingGC(); + } + + forceCC() { + Cu.forceCC(); + } + + finishCC() { + Cu.finishCC(); + } + + ccSlice(budget) { + Cu.ccSlice(budget); + } + + // Due to various dependencies between JS objects and C++ objects, an ordinary + // forceGC doesn't necessarily clear all unused objects, thus the GC and CC + // needs to run several times and when no other JS is running. + // The current number of iterations has been determined according to massive + // cross platform testing. + exactGC(callback) { + let count = 0; + + function genGCCallback(cb) { + return function() { + Cu.forceCC(); + if (++count < 3) { + Cu.schedulePreciseGC(genGCCallback(cb)); + } else if (cb) { + cb(); + } + }; + } + + Cu.schedulePreciseGC(genGCCallback(callback)); + } + + nondeterministicGetWeakMapKeys(m) { + let keys = ChromeUtils.nondeterministicGetWeakMapKeys(m); + if (!keys) { + return undefined; + } + return this.contentWindow.Array.from(keys); + } + + getMemoryReports() { + try { + Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager) + .getReports( + () => {}, + null, + () => {}, + null, + false + ); + } catch (e) {} + } + + setGCZeal(zeal) { + Cu.setGCZeal(zeal); + } + + isMainProcess() { + try { + return ( + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); + } catch (e) {} + return true; + } + + get XPCOMABI() { + if (this._xpcomabi != null) { + return this._xpcomabi; + } + + var xulRuntime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime); + + this._xpcomabi = xulRuntime.XPCOMABI; + return this._xpcomabi; + } + + // The optional aWin parameter allows the caller to specify a given window in + // whose scope the runnable should be dispatched. If aFun throws, the + // exception will be reported to aWin. + executeSoon(aFun, aWin) { + // Create the runnable in the scope of aWin to avoid running into COWs. + var runnable = {}; + if (aWin) { + runnable = Cu.createObjectIn(aWin); + } + runnable.run = aFun; + Cu.dispatch(runnable, aWin); + } + + get OS() { + if (this._os != null) { + return this._os; + } + + this._os = Services.appinfo.OS; + return this._os; + } + + get useRemoteSubframes() { + return this.docShell.nsILoadContext.useRemoteSubframes; + } + + addSystemEventListener(target, type, listener, useCapture) { + Services.els.addSystemEventListener(target, type, listener, useCapture); + } + removeSystemEventListener(target, type, listener, useCapture) { + Services.els.removeSystemEventListener(target, type, listener, useCapture); + } + + // helper method to check if the event is consumed by either default group's + // event listener or system group's event listener. + defaultPreventedInAnyGroup(event) { + // FYI: Event.defaultPrevented returns false in content context if the + // event is consumed only by system group's event listeners. + return event.defaultPrevented; + } + + getDOMRequestService() { + var serv = Services.DOMRequest; + var res = {}; + var props = [ + "createRequest", + "createCursor", + "fireError", + "fireSuccess", + "fireDone", + "fireDetailedError", + ]; + for (var i in props) { + let prop = props[i]; + res[prop] = function() { + return serv[prop].apply(serv, arguments); + }; + } + return Cu.cloneInto(res, this.contentWindow, { cloneFunctions: true }); + } + + addCategoryEntry(category, entry, value, persists, replace) { + Services.catMan.addCategoryEntry(category, entry, value, persists, replace); + } + + deleteCategoryEntry(category, entry, persists) { + Services.catMan.deleteCategoryEntry(category, entry, persists); + } + openDialog(win, args) { + return win.openDialog.apply(win, args); + } + // This is a blocking call which creates and spins a native event loop + spinEventLoop(win) { + // simply do a sync XHR back to our windows location. + var syncXHR = new win.XMLHttpRequest(); + syncXHR.open("GET", win.location, false); + syncXHR.send(); + } + + // :jdm gets credit for this. ex: getPrivilegedProps(window, 'location.href'); + getPrivilegedProps(obj, props) { + var parts = props.split("."); + for (var i = 0; i < parts.length; i++) { + var p = parts[i]; + if (obj[p] != undefined) { + obj = obj[p]; + } else { + return null; + } + } + return obj; + } + + _browsingContextForTarget(target) { + if (BrowsingContext.isInstance(target)) { + return target; + } + if (Element.isInstance(target)) { + return target.browsingContext; + } + + return BrowsingContext.getFromWindow(target); + } + + getBrowsingContextID(target) { + return this._browsingContextForTarget(target).id; + } + + *getGroupTopLevelWindows(target) { + let { group } = this._browsingContextForTarget(target); + for (let bc of group.getToplevels()) { + yield bc.window; + } + } + + /** + * Runs a task in the context of the given frame, and returns a + * promise which resolves to the return value of that task. + * + * The given frame may be in-process or out-of-process. Either way, + * the task will run asynchronously, in a sandbox with access to the + * frame's content window via its `content` global. Any arguments + * passed will be copied via structured clone, as will its return + * value. + * + * The sandbox also has access to an Assert object, as provided by + * Assert.jsm. Any assertion methods called before the task resolves + * will be relayed back to the test environment of the caller. + * + * @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target + * The target in which to run the task. This may be any element + * which implements the FrameLoaderOwner interface (including + * HTML