diff options
Diffstat (limited to 'testing/specialpowers/content/SpecialPowersChild.sys.mjs')
-rw-r--r-- | testing/specialpowers/content/SpecialPowersChild.sys.mjs | 2329 |
1 files changed, 2329 insertions, 0 deletions
diff --git a/testing/specialpowers/content/SpecialPowersChild.sys.mjs b/testing/specialpowers/content/SpecialPowersChild.sys.mjs new file mode 100644 index 0000000000..7b40aab198 --- /dev/null +++ b/testing/specialpowers/content/SpecialPowersChild.sys.mjs @@ -0,0 +1,2329 @@ +/* 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. + */ + +import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ContentTaskUtils: "resource://testing-common/ContentTaskUtils.sys.mjs", + MockColorPicker: "resource://testing-common/MockColorPicker.sys.mjs", + MockFilePicker: "resource://testing-common/MockFilePicker.sys.mjs", + MockPermissionPrompt: + "resource://testing-common/MockPermissionPrompt.sys.mjs", + PerTestCoverageUtils: + "resource://testing-common/PerTestCoverageUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + SpecialPowersSandbox: + "resource://testing-common/SpecialPowersSandbox.sys.mjs", + WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs", +}); +ChromeUtils.defineModuleGetter( + lazy, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +Cu.crashIfNotInAutomation(); + +function bindDOMWindowUtils(aWindow) { + return aWindow && lazy.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 = lazy.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; + } + + 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.console.unregisterListener(this); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener", "nsIObserver"]), +}; + +export 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; + + lazy.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) { + console.error(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 "EnsureFocus": + // Ensure that the focus is in this child document. Returns a browsing + // context of a child frame if a subframe should be focused or undefined + // otherwise. + + // If a subframe node is focused, then the focus will actually + // be within that subframe's document. If blurSubframe is true, + // then blur the subframe so that this parent document is focused + // instead. If blurSubframe is false, then return the browsing + // context for that subframe. The parent process will then call back + // into this same code but in the process for that subframe. + let focusedNode = this.document.activeElement; + let subframeFocused = + ChromeUtils.getClassName(focusedNode) == "HTMLIFrameElement" || + ChromeUtils.getClassName(focusedNode) == "HTMLFrameElement" || + ChromeUtils.getClassName(focusedNode) == "XULFrameElement"; + if (subframeFocused) { + if (message.data.blurSubframe) { + Services.focus.clearFocus(this.contentWindow); + } else { + if (!this.document.hasFocus()) { + this.contentWindow.focus(); + } + return Promise.resolve(focusedNode.browsingContext); + } + } + + // A subframe is not focused, so if this document is + // not focused, focus it and wait for the focus event. + if (!this.document.hasFocus()) { + return new Promise(resolve => { + this.document.addEventListener( + "focus", + () => { + resolve(); + }, + { + capture: true, + once: true, + } + ); + this.contentWindow.focus(); + }); + } + break; + + case "Assert": + { + if ("info" in message.data) { + (this.xpcshellScope || 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 if (this.xpcshellScope) { + this.xpcshellScope.do_report_result(passed, name, stack); + } 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 lazy.WrapPrivileged.unwrap(obj); + } + isWrapper(val) { + return lazy.WrapPrivileged.isWrapper(val); + } + + unwrapIfWrapped(obj) { + return lazy.WrapPrivileged.isWrapper(obj) + ? lazy.WrapPrivileged.unwrap(obj) + : obj; + } + + /* + * Wrap objects on a specified global. + */ + wrapFor(obj, win) { + return lazy.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 lazy.WrapPrivileged.wrapCallback(func, this.contentWindow); + } + wrapCallbackObject(obj) { + return lazy.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 (!lazy.WrapPrivileged.isWrapper(obj)) { + throw new Error( + "You only need to use this for SpecialPowers wrapped objects" + ); + } + + obj = lazy.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 lazy.WrapPrivileged.unwrap(a) === lazy.WrapPrivileged.unwrap(b); + } + + get MockFilePicker() { + return lazy.MockFilePicker; + } + + get MockColorPicker() { + return lazy.MockColorPicker; + } + + get MockPermissionPrompt() { + return lazy.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() { + // 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 = lazy.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 id = Services.uuid.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( + `SpecialPowers/receiveMessage/${name}`, + null, + 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 ChromeUtils() { + return ChromeUtils; + } + + get isHeadless() { + return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; + } + + 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); + } + + async pushPrefEnv(inPrefs, callback = null) { + let { requiresRefresh } = await this.sendQuery("PushPrefEnv", inPrefs); + if (callback) { + await callback(); + } + if (requiresRefresh) { + await this._promiseEarlyRefresh(); + } + } + + async popPrefEnv(callback = null) { + let { popped, requiresRefresh } = await this.sendQuery("PopPrefEnv"); + if (callback) { + await callback(popped); + } + if (requiresRefresh) { + await this._promiseEarlyRefresh(); + } + } + + async flushPrefEnv(callback = null) { + let { requiresRefresh } = await this.sendQuery("FlushPrefEnv"); + if (callback) { + await callback(); + } + if (requiresRefresh) { + await this._promiseEarlyRefresh(); + } + } + + /* + Collect a snapshot of all preferences in Firefox (i.e. about:prefs). + From this, store the results within specialpowers for later reference. + */ + async getBaselinePrefs(callback = null) { + await this.sendQuery("getBaselinePrefs"); + if (callback) { + await callback(); + } + } + + /* + This uses the stored prefs from getBaselinePrefs, collects a new snapshot + of preferences, then compares the new vs the baseline. If there are differences + they are recorded and returned as an array of preferences, in addition + all the changed preferences are reset to the value found in the baseline. + + ignorePrefs: array of strings which are preferences. If they end in *, + we do a partial match + */ + async comparePrefsToBaseline(ignorePrefs, callback = null) { + let retVal = await this.sendQuery("comparePrefsToBaseline", ignorePrefs); + if (callback) { + callback(retVal); + } + return retVal; + } + + _promiseEarlyRefresh() { + return new Promise(r => { + // for mochitest-browser + if (typeof this.chromeWindow != "undefined") { + this.chromeWindow.requestAnimationFrame(r); + } + // for mochitest-plain + else { + this.contentWindow.requestAnimationFrame(r); + } + }); + } + + _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 = lazy.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 = lazy.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 = lazy.WrapPrivileged.unwrap(obj1); + obj2 = lazy.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 + // <object> and <embed> 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); + } + getStringPref(...args) { + return Services.prefs.getStringPref(...args); + } + + 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 }); + } + getParentStringPref(prefName, defaultValue) { + return this._getParentPref(prefName, "STRING", { 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); + } + setStringPref(prefName, value) { + return this._setPref(prefName, "STRING", value); + } + + // 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); + case "STRING": + return Services.prefs.getStringPref(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); + } + 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); + } + 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; + } + + emulateMedium(window, mediaType) { + BrowsingContext.getFromWindow(window).top.mediumOverride = mediaType; + } + + stopEmulatingMedium(window) { + BrowsingContext.getFromWindow(window).top.mediumOverride = ""; + } + + // Takes a snapshot of the given window and returns a <canvas> + // 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 (!Cu.isRemoteProxy(content) && 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 <canvas> 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.sys.mjs. 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 <iframe> elements and XUL <browser> elements) or a + * WindowProxy (either in-process or remote). + * @param {Array<any>} args + * An array of arguments to pass to the task. All arguments + * must be structured clone compatible, and will be cloned + * before being passed to the task. + * @param {function} task + * The function to run in the context of the target. The + * function will be stringified and re-evaluated in the context + * of the target's content window. It may return any structured + * clone compatible value, or a Promise which resolves to the + * same, which will be returned to the caller. + * + * @returns {Promise<any>} + * A promise which resolves to the return value of the task, or + * which rejects if the task raises an exception. As this is + * being written, the rejection value will always be undefined + * in the cases where the task throws an error, though that may + * change in the future. + */ + spawn(target, args, task) { + let browsingContext = this._browsingContextForTarget(target); + + return this.sendQuery("Spawn", { + browsingContext, + args, + task: String(task), + caller: Cu.getFunctionSourceLocation(task), + hasHarness: + typeof this.SimpleTest === "object" || + typeof this.xpcshellScope === "object", + imports: this._spawnTaskImports, + }); + } + + /** + * Like `spawn`, but spawns a chrome task in the parent process, + * instead. The task additionally has access to `windowGlobalParent` + * and `browsingContext` globals corresponding to the window from + * which the task was spawned. + */ + spawnChrome(args, task) { + return this.sendQuery("SpawnChrome", { + args, + task: String(task), + caller: Cu.getFunctionSourceLocation(task), + imports: this._spawnTaskImports, + }); + } + + snapshotContext(target, rect, background, resetScrollPosition = false) { + let browsingContext = this._browsingContextForTarget(target); + + return this.sendQuery("Snapshot", { + browsingContext, + rect, + background, + resetScrollPosition, + }).then(imageData => { + return this.contentWindow.createImageBitmap(imageData); + }); + } + + getSecurityState(target) { + let browsingContext = this._browsingContextForTarget(target); + + return this.sendQuery("SecurityState", { + browsingContext, + }); + } + + _spawnTask(task, args, caller, taskId, imports) { + let sb = new lazy.SpecialPowersSandbox( + null, + data => { + this.sendAsyncMessage("ProxiedAssert", { taskId, data }); + }, + { imports } + ); + + sb.sandbox.SpecialPowers = this; + sb.sandbox.ContentTaskUtils = lazy.ContentTaskUtils; + for (let [global, prop] of Object.entries({ + content: "contentWindow", + docShell: "docShell", + })) { + Object.defineProperty(sb.sandbox, global, { + get: () => { + return this[prop]; + }, + enumerable: true, + }); + } + + return sb.execute(task, args, caller); + } + + /** + * Automatically imports the given symbol from the given sys.mjs for any + * task spawned by this SpecialPowers instance. + */ + addTaskImport(symbol, url) { + this._spawnTaskImports[symbol] = url; + } + + get SimpleTest() { + return this._SimpleTest || this.contentWindow.wrappedJSObject.SimpleTest; + } + set SimpleTest(val) { + this._SimpleTest = val; + } + + get xpcshellScope() { + return this._xpcshellScope; + } + set xpcshellScope(val) { + this._xpcshellScope = val; + } + + async evictAllContentViewers() { + if (Services.appinfo.sessionHistoryInParent) { + await this.sendQuery("EvictAllContentViewers"); + } else { + this.browsingContext.top.childSessionHistory.legacySHistory.evictAllContentViewers(); + } + } + + /** + * Sets this actor as the default assertion result handler for tasks + * which originate in a window without a test harness. + */ + setAsDefaultAssertHandler() { + this.sendAsyncMessage("SetAsDefaultAssertHandler"); + } + + getFocusedElementForWindow(targetWindow, aDeep) { + var outParam = {}; + Services.focus.getFocusedElementForWindow(targetWindow, aDeep, outParam); + return outParam.value; + } + + get focusManager() { + return Services.focus; + } + + activeWindow() { + return Services.focus.activeWindow; + } + + focusedWindow() { + return Services.focus.focusedWindow; + } + + clearFocus(aWindow) { + Services.focus.clearFocus(aWindow); + } + + focus(aWindow) { + // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests + // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching + if (aWindow) { + aWindow.focus(); + } + + try { + let actor = aWindow + ? aWindow.windowGlobalChild.getActor("SpecialPowers") + : this; + actor.sendAsyncMessage("SpecialPowers.Focus", {}); + } catch (e) { + console.error(e); + } + } + + ensureFocus(aBrowsingContext, aBlurSubframe) { + return this.sendQuery("EnsureFocus", { + browsingContext: aBrowsingContext, + blurSubframe: aBlurSubframe, + }); + } + + getClipboardData(flavor, whichClipboard) { + if (whichClipboard === undefined) { + whichClipboard = Services.clipboard.kGlobalClipboard; + } + + var xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(this.docShell); + xferable.addDataFlavor(flavor); + Services.clipboard.getData(xferable, whichClipboard); + var data = {}; + try { + xferable.getTransferData(flavor, data); + } catch (e) {} + data = data.value || null; + if (data == null) { + return ""; + } + + return data.QueryInterface(Ci.nsISupportsString).data; + } + + clipboardCopyString(str) { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(str); + } + + supportsSelectionClipboard() { + return Services.clipboard.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard + ); + } + + swapFactoryRegistration(cid, contractID, newFactory) { + newFactory = Cu.waiveXrays(newFactory); + + var componentRegistrar = Components.manager.QueryInterface( + Ci.nsIComponentRegistrar + ); + + var currentCID = componentRegistrar.contractIDToCID(contractID); + var currentFactory = Components.manager.getClassObject( + Cc[contractID], + Ci.nsIFactory + ); + if (cid) { + componentRegistrar.unregisterFactory(currentCID, currentFactory); + } else { + cid = Services.uuid.generateUUID(); + } + + // Restore the original factory. + componentRegistrar.registerFactory(cid, "", contractID, newFactory); + return { originalCID: currentCID }; + } + + _getElement(aWindow, id) { + return typeof id == "string" ? aWindow.document.getElementById(id) : id; + } + + dispatchEvent(aWindow, target, event) { + var el = this._getElement(aWindow, target); + return el.dispatchEvent(event); + } + + get isDebugBuild() { + return Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2) + .isDebugBuild; + } + assertionCount() { + var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + return debugsvc.assertionCount; + } + + /** + * @param arg one of the following: + * - A URI string. + * - A document node. + * - A dictionary including a URL (`url`) and origin attributes (`attr`). + */ + _getPrincipalFromArg(arg) { + arg = lazy.WrapPrivileged.unwrap(Cu.unwaiveXrays(arg)); + + if (arg.nodePrincipal) { + // It's a document. + return arg.nodePrincipal; + } + + let secMan = Services.scriptSecurityManager; + if (typeof arg == "string") { + // It's a URL. + let uri = Services.io.newURI(arg); + return secMan.createContentPrincipal(uri, {}); + } + + let uri = Services.io.newURI(arg.url); + let attrs = arg.originAttributes || {}; + return secMan.createContentPrincipal(uri, attrs); + } + + async addPermission(type, allow, arg, expireType, expireTime) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return; // nothing to do + } + + let permission = allow; + if (typeof permission === "boolean") { + permission = + Ci.nsIPermissionManager[allow ? "ALLOW_ACTION" : "DENY_ACTION"]; + } + + var msg = { + op: "add", + type, + permission, + principal, + expireType: typeof expireType === "number" ? expireType : 0, + expireTime: typeof expireTime === "number" ? expireTime : 0, + }; + + await this.sendQuery("SPPermissionManager", msg); + } + + /** + * @param type see nsIPermissionsManager::testPermissionFromPrincipal. + * @param arg one of the following: + * - A URI string. + * - A document node. + * - A dictionary including a URL (`url`) and origin attributes (`attr`). + */ + async removePermission(type, arg) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return; // nothing to do + } + + var msg = { + op: "remove", + type, + principal, + }; + + await this.sendQuery("SPPermissionManager", msg); + } + + async hasPermission(type, arg) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return true; // system principals have all permissions + } + + var msg = { + op: "has", + type, + principal, + }; + + return this.sendQuery("SPPermissionManager", msg); + } + + async testPermission(type, value, arg) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return true; // system principals have all permissions + } + + var msg = { + op: "test", + type, + value, + principal, + }; + return this.sendQuery("SPPermissionManager", msg); + } + + isContentWindowPrivate(win) { + return lazy.PrivateBrowsingUtils.isContentWindowPrivate(win); + } + + async notifyObserversInParentProcess(subject, topic, data) { + if (subject) { + throw new Error("Can't send subject to another process!"); + } + if (this.isMainProcess()) { + this.notifyObservers(subject, topic, data); + return; + } + var msg = { + op: "notify", + observerTopic: topic, + observerData: data, + }; + await this.sendQuery("SPObserverService", msg); + } + + removeAllServiceWorkerData() { + return this.sendQuery("SPRemoveAllServiceWorkers", {}); + } + + removeServiceWorkerDataForExampleDomain() { + return this.sendQuery("SPRemoveServiceWorkerDataForExampleDomain", {}); + } + + cleanUpSTSData(origin, flags) { + return this.sendQuery("SPCleanUpSTSData", { origin }); + } + + async requestDumpCoverageCounters(cb) { + // We want to avoid a roundtrip between child and parent. + if (!lazy.PerTestCoverageUtils.enabled) { + return; + } + + await this.sendQuery("SPRequestDumpCoverageCounters", {}); + } + + async requestResetCoverageCounters(cb) { + // We want to avoid a roundtrip between child and parent. + if (!lazy.PerTestCoverageUtils.enabled) { + return; + } + await this.sendQuery("SPRequestResetCoverageCounters", {}); + } + + loadExtension(ext, handler) { + if (this._extensionListeners == null) { + this._extensionListeners = new Set(); + + this._addMessageListener("SPExtensionMessage", msg => { + for (let listener of this._extensionListeners) { + try { + listener(msg); + } catch (e) { + console.error(e); + } + } + }); + } + + // Note, this is not the addon is as used by the AddonManager etc, + // this is just an identifier used for specialpowers messaging + // between this content process and the chrome process. + let id = this._nextExtensionID++; + + handler = Cu.waiveXrays(handler); + ext = Cu.waiveXrays(ext); + + let sp = this; + let state = "uninitialized"; + let extension = { + get state() { + return state; + }, + + startup() { + state = "pending"; + return sp.sendQuery("SPStartupExtension", { id }).then( + () => { + state = "running"; + }, + () => { + state = "failed"; + sp._extensionListeners.delete(listener); + return Promise.reject("startup failed"); + } + ); + }, + + unload() { + state = "unloading"; + return sp.sendQuery("SPUnloadExtension", { id }).finally(() => { + sp._extensionListeners.delete(listener); + state = "unloaded"; + }); + }, + + sendMessage(...args) { + sp.sendAsyncMessage("SPExtensionMessage", { id, args }); + }, + + grantActiveTab(tabId) { + sp.sendAsyncMessage("SPExtensionGrantActiveTab", { id, tabId }); + }, + + terminateBackground(...args) { + return sp.sendQuery("SPExtensionTerminateBackground", { id, args }); + }, + + wakeupBackground() { + return sp.sendQuery("SPExtensionWakeupBackground", { id }); + }, + }; + + this.sendAsyncMessage("SPLoadExtension", { ext, id }); + + let listener = msg => { + if (msg.data.id == id) { + if (msg.data.type == "extensionSetId") { + extension.id = msg.data.args[0]; + extension.uuid = msg.data.args[1]; + } else if (msg.data.type in handler) { + handler[msg.data.type]( + ...Cu.cloneInto(msg.data.args, this.contentWindow) + ); + } else { + dump(`Unexpected: ${msg.data.type}\n`); + } + } + }; + + this._extensionListeners.add(listener); + return extension; + } + + invalidateExtensionStorageCache() { + this.notifyObserversInParentProcess( + null, + "extension-invalidate-storage-cache", + "" + ); + } + + allowMedia(window, enable) { + window.docShell.allowMedia = enable; + } + + createChromeCache(name, url) { + let principal = this._getPrincipalFromArg(url); + return new this.contentWindow.CacheStorage(name, principal); + } + + loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) { + const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" + ); + + return new Promise(function (resolve) { + let listener = { + httpStatus: 0, + + onStartRequest(request) { + request.QueryInterface(Ci.nsIHttpChannel); + this.httpStatus = request.responseStatus; + }, + + onDataAvailable(request, stream, offset, count) { + new BinaryInputStream(stream).readByteArray(count); + }, + + onStopRequest(request, status) { + /* testing here that the redirect was not followed. If it was followed + we would see a http status of 200 and status of NS_OK */ + + let httpStatus = this.httpStatus; + resolve({ status, httpStatus }); + }, + }; + let uri = lazy.NetUtil.newURI(url); + let channel = lazy.NetUtil.newChannel({ uri, loadUsingSystemPrincipal }); + + channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI; + channel.QueryInterface(Ci.nsIHttpChannelInternal); + channel.documentURI = uri; + channel.asyncOpen(listener); + }); + } + + get ParserUtils() { + if (this._pu != null) { + return this._pu; + } + + let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); + // We need to create and return our own wrapper. + this._pu = { + sanitize(src, flags) { + return pu.sanitize(src, flags); + }, + convertToPlainText(src, flags, wrapCol) { + return pu.convertToPlainText(src, flags, wrapCol); + }, + parseFragment(fragment, flags, isXML, baseURL, element) { + let baseURI = baseURL ? lazy.NetUtil.newURI(baseURL) : null; + return pu.parseFragment( + lazy.WrapPrivileged.unwrap(fragment), + flags, + isXML, + baseURI, + lazy.WrapPrivileged.unwrap(element) + ); + }, + }; + return this._pu; + } + + createDOMWalker(node, showAnonymousContent) { + node = lazy.WrapPrivileged.unwrap(node); + let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance( + Ci.inIDeepTreeWalker + ); + walker.showAnonymousContent = showAnonymousContent; + walker.init(node.ownerDocument, NodeFilter.SHOW_ALL); + walker.currentNode = node; + let contentWindow = this.contentWindow; + return { + get firstChild() { + return lazy.WrapPrivileged.wrap(walker.firstChild(), contentWindow); + }, + get lastChild() { + return lazy.WrapPrivileged.wrap(walker.lastChild(), contentWindow); + }, + }; + } + + /** + * Which commands are available can be determined by checking which commands + * are registered. See \ref + * nsIControllerCommandTable.registerCommand(in String, in nsIControllerCommand). + */ + doCommand(window, cmd, param) { + switch (cmd) { + case "cmd_align": + case "cmd_backgroundColor": + case "cmd_fontColor": + case "cmd_fontFace": + case "cmd_fontSize": + case "cmd_highlight": + case "cmd_insertImageNoUI": + case "cmd_insertLinkNoUI": + case "cmd_paragraphState": { + const params = Cu.createCommandParams(); + params.setStringValue("state_attribute", param); + return window.docShell.doCommandWithParams(cmd, params); + } + case "cmd_pasteTransferable": { + const params = Cu.createCommandParams(); + params.setISupportsValue("transferable", param); + return window.docShell.doCommandWithParams(cmd, params); + } + default: + return window.docShell.doCommand(cmd); + } + } + + isCommandEnabled(window, cmd) { + return window.docShell.isCommandEnabled(cmd); + } + + /** + * See \ref nsIContentViewerEdit.setCommandNode(in Node). + */ + setCommandNode(window, node) { + return window.docShell.contentViewer + .QueryInterface(Ci.nsIContentViewerEdit) + .setCommandNode(node); + } + + /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by + * SystemGroup, but some test cases may run as web content. That would assert + * when trying to enter web content from a runnable labeled by the + * SystemGroup. To avoid that, we run classify from SpecialPowers which is + * chrome-privileged and allowed to run inside SystemGroup + */ + + doUrlClassify(principal, callback) { + let classifierService = Cc[ + "@mozilla.org/url-classifier/dbservice;1" + ].getService(Ci.nsIURIClassifier); + + let wrapCallback = (...args) => { + Services.tm.dispatchToMainThread(() => { + if (typeof callback == "function") { + callback(...args); + } else { + callback.onClassifyComplete.call(undefined, ...args); + } + }); + }; + + return classifierService.classify( + lazy.WrapPrivileged.unwrap(principal), + wrapCallback + ); + } + + // TODO: Bug 1353701 - Supports custom event target for labelling. + doUrlClassifyLocal(uri, tables, callback) { + let classifierService = Cc[ + "@mozilla.org/url-classifier/dbservice;1" + ].getService(Ci.nsIURIClassifier); + + let wrapCallback = results => { + Services.tm.dispatchToMainThread(() => { + if (typeof callback == "function") { + callback(lazy.WrapPrivileged.wrap(results, this.contentWindow)); + } else { + callback.onClassifyComplete.call( + undefined, + lazy.WrapPrivileged.wrap(results, this.contentWindow) + ); + } + }); + }; + + let feature = classifierService.createFeatureWithTables( + "test", + tables.split(","), + [] + ); + return classifierService.asyncClassifyLocalWithFeatures( + lazy.WrapPrivileged.unwrap(uri), + [feature], + Ci.nsIUrlClassifierFeature.blocklist, + wrapCallback + ); + } + + /* Content processes asynchronously receive child-to-parent transformations + * when they are launched. Until they are received, screen coordinates + * reported to JS are wrong. This is generally ok. It behaves as if the + * user repositioned the window. But if we want to test screen coordinates, + * we need to wait for the updated data. + */ + contentTransformsReceived(win) { + while (win) { + try { + return win.docShell.browserChild.contentTransformsReceived(); + } catch (ex) { + // browserChild getter throws on non-e10s rather than returning null. + } + if (win == win.parent) { + break; + } + win = win.parent; + } + return Promise.resolve(); + } +} + +SpecialPowersChild.prototype._proxiedObservers = { + "specialpowers-http-notify-request": function (aMessage) { + let uri = aMessage.json.uri; + Services.obs.notifyObservers( + null, + "specialpowers-http-notify-request", + uri + ); + }, + + "specialpowers-service-worker-shutdown": function (aMessage) { + Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown"); + }, + + "specialpowers-csp-on-violate-policy": function (aMessage) { + let subject = null; + + try { + subject = Services.io.newURI(aMessage.data.subject); + } catch (ex) { + // if it's not a valid URI it must be an nsISupportsCString + subject = Cc["@mozilla.org/supports-cstring;1"].createInstance( + Ci.nsISupportsCString + ); + subject.data = aMessage.data.subject; + } + Services.obs.notifyObservers( + subject, + "specialpowers-csp-on-violate-policy", + aMessage.data.data + ); + }, + + "specialpowers-xfo-on-violate-policy": function (aMessage) { + let subject = Services.io.newURI(aMessage.data.subject); + Services.obs.notifyObservers( + subject, + "specialpowers-xfo-on-violate-policy", + aMessage.data.data + ); + }, +}; + +SpecialPowersChild.prototype.EARLY_BETA_OR_EARLIER = + AppConstants.EARLY_BETA_OR_EARLIER; |