From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- testing/specialpowers/Makefile.in | 6 + testing/specialpowers/api.js | 68 + .../specialpowers/content/AppTestDelegate.sys.mjs | 52 + .../content/AppTestDelegateChild.sys.mjs | 18 + .../content/AppTestDelegateParent.sys.mjs | 85 + .../specialpowers/content/MockColorPicker.sys.mjs | 123 ++ .../specialpowers/content/MockFilePicker.sys.mjs | 315 +++ .../content/MockPermissionPrompt.sys.mjs | 75 + .../content/SpecialPowersChild.sys.mjs | 2329 ++++++++++++++++++++ .../content/SpecialPowersEventUtils.sys.mjs | 35 + .../content/SpecialPowersParent.sys.mjs | 1481 +++++++++++++ .../content/SpecialPowersSandbox.sys.mjs | 141 ++ .../specialpowers/content/WrapPrivileged.sys.mjs | 385 ++++ testing/specialpowers/manifest.json | 23 + testing/specialpowers/moz.build | 39 + testing/specialpowers/schema.json | 1 + 16 files changed, 5176 insertions(+) create mode 100644 testing/specialpowers/Makefile.in create mode 100644 testing/specialpowers/api.js create mode 100644 testing/specialpowers/content/AppTestDelegate.sys.mjs create mode 100644 testing/specialpowers/content/AppTestDelegateChild.sys.mjs create mode 100644 testing/specialpowers/content/AppTestDelegateParent.sys.mjs create mode 100644 testing/specialpowers/content/MockColorPicker.sys.mjs create mode 100644 testing/specialpowers/content/MockFilePicker.sys.mjs create mode 100644 testing/specialpowers/content/MockPermissionPrompt.sys.mjs create mode 100644 testing/specialpowers/content/SpecialPowersChild.sys.mjs create mode 100644 testing/specialpowers/content/SpecialPowersEventUtils.sys.mjs create mode 100644 testing/specialpowers/content/SpecialPowersParent.sys.mjs create mode 100644 testing/specialpowers/content/SpecialPowersSandbox.sys.mjs create mode 100644 testing/specialpowers/content/WrapPrivileged.sys.mjs create mode 100644 testing/specialpowers/manifest.json create mode 100644 testing/specialpowers/moz.build create mode 100644 testing/specialpowers/schema.json (limited to 'testing/specialpowers') diff --git a/testing/specialpowers/Makefile.in b/testing/specialpowers/Makefile.in new file mode 100644 index 0000000000..e42d0650ef --- /dev/null +++ b/testing/specialpowers/Makefile.in @@ -0,0 +1,6 @@ +# +# 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/. + +XPI_PKGNAME = specialpowers@mozilla.org diff --git a/testing/specialpowers/api.js b/testing/specialpowers/api.js new file mode 100644 index 0000000000..a0ffe42b32 --- /dev/null +++ b/testing/specialpowers/api.js @@ -0,0 +1,68 @@ +/* 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/. */ + +/* globals ExtensionAPI, Services, XPCOMUtils */ + +ChromeUtils.defineESModuleGetters(this, { + SpecialPowersParent: "resource://testing-common/SpecialPowersParent.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "resProto", + "@mozilla.org/network/protocol;1?name=resource", + "nsISubstitutingProtocolHandler" +); + +this.specialpowers = class extends ExtensionAPI { + onStartup() { + // Register special testing modules. + let manifest = Services.dirsvc.get("ProfD", Ci.nsIFile); + manifest.append("tests.manifest"); + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(manifest); + + { + let uri = Services.io.newURI("content/", null, this.extension.rootURI); + resProto.setSubstitutionWithFlags( + "specialpowers", + uri, + resProto.ALLOW_CONTENT_ACCESS + ); + } + + if (!resProto.hasSubstitution("testing-common")) { + let uri = Services.io.newURI("modules/", null, this.extension.rootURI); + resProto.setSubstitution( + "testing-common", + uri, + resProto.ALLOW_CONTENT_ACCESS + ); + } + + SpecialPowersParent.registerActor(); + + ChromeUtils.registerWindowActor("AppTestDelegate", { + parent: { + esModuleURI: "resource://specialpowers/AppTestDelegateParent.sys.mjs", + }, + child: { + esModuleURI: "resource://specialpowers/AppTestDelegateChild.sys.mjs", + events: { + DOMContentLoaded: { capture: true }, + load: { capture: true }, + }, + }, + allFrames: true, + includeChrome: true, + }); + } + + onShutdown() { + SpecialPowersParent.unregisterActor(); + ChromeUtils.unregisterWindowActor("AppTestDelegate"); + resProto.setSubstitution("specialpowers", null); + } +}; diff --git a/testing/specialpowers/content/AppTestDelegate.sys.mjs b/testing/specialpowers/content/AppTestDelegate.sys.mjs new file mode 100644 index 0000000000..97fe60341b --- /dev/null +++ b/testing/specialpowers/content/AppTestDelegate.sys.mjs @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +class Delegate { + actor(window) { + return window.windowGlobalChild.getActor("AppTestDelegate"); + } + + clickPageAction(window, extension) { + return this.actor(window).sendQuery("clickPageAction", { + extensionId: extension.id, + }); + } + + clickBrowserAction(window, extension) { + return this.actor(window).sendQuery("clickBrowserAction", { + extensionId: extension.id, + }); + } + + closeBrowserAction(window, extension) { + return this.actor(window).sendQuery("closeBrowserAction", { + extensionId: extension.id, + }); + } + + closePageAction(window, extension) { + return this.actor(window).sendQuery("closePageAction", { + extensionId: extension.id, + }); + } + + awaitExtensionPanel(window, extension) { + return this.actor(window).sendQuery("awaitExtensionPanel", { + extensionId: extension.id, + }); + } + + async openNewForegroundTab(window, url, waitForLoad = true) { + const tabId = await this.actor(window).sendQuery("openNewForegroundTab", { + url, + waitForLoad, + }); + return { id: tabId }; + } + + removeTab(window, tab) { + return this.actor(window).sendQuery("removeTab", { tabId: tab.id }); + } +} + +export var AppTestDelegate = new Delegate(); diff --git a/testing/specialpowers/content/AppTestDelegateChild.sys.mjs b/testing/specialpowers/content/AppTestDelegateChild.sys.mjs new file mode 100644 index 0000000000..1d78d41f5d --- /dev/null +++ b/testing/specialpowers/content/AppTestDelegateChild.sys.mjs @@ -0,0 +1,18 @@ +/* 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/. */ + +export class AppTestDelegateChild extends JSWindowActorChild { + handleEvent(event) { + switch (event.type) { + case "DOMContentLoaded": + case "load": { + this.sendAsyncMessage(event.type, { + internalURL: event.target.documentURI, + visibleURL: event.target.location?.href, + }); + break; + } + } + } +} diff --git a/testing/specialpowers/content/AppTestDelegateParent.sys.mjs b/testing/specialpowers/content/AppTestDelegateParent.sys.mjs new file mode 100644 index 0000000000..4935f3245a --- /dev/null +++ b/testing/specialpowers/content/AppTestDelegateParent.sys.mjs @@ -0,0 +1,85 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + // Each app needs to implement this + AppUiTestDelegate: "resource://testing-common/AppUiTestDelegate.sys.mjs", +}); + +export class AppTestDelegateParent extends JSWindowActorParent { + constructor() { + super(); + this._tabs = new Map(); + } + + get browser() { + return this.browsingContext.top.embedderElement; + } + + get window() { + return this.browser.ownerGlobal; + } + + async receiveMessage(message) { + const { extensionId, url, waitForLoad, tabId } = message.data; + switch (message.name) { + case "DOMContentLoaded": + case "load": { + return this.browser?.dispatchEvent( + new CustomEvent(`AppTestDelegate:${message.name}`, { + detail: { + browsingContext: this.browsingContext, + ...message.data, + }, + }) + ); + } + case "clickPageAction": + return lazy.AppUiTestDelegate.clickPageAction(this.window, extensionId); + case "clickBrowserAction": + return lazy.AppUiTestDelegate.clickBrowserAction( + this.window, + extensionId + ); + case "closePageAction": + return lazy.AppUiTestDelegate.closePageAction(this.window, extensionId); + case "closeBrowserAction": + return lazy.AppUiTestDelegate.closeBrowserAction( + this.window, + extensionId + ); + case "awaitExtensionPanel": + // The desktop delegate returns a , but that cannot be sent + // over IPC, so just ignore it. The promise resolves when the panel and + // its content is fully loaded. + await lazy.AppUiTestDelegate.awaitExtensionPanel( + this.window, + extensionId + ); + return null; + case "openNewForegroundTab": { + // We cannot send the tab object across process so let's store it with + // a unique ID here. + const uuid = Services.uuid.generateUUID().toString(); + const tab = await lazy.AppUiTestDelegate.openNewForegroundTab( + this.window, + url, + waitForLoad + ); + this._tabs.set(uuid, tab); + return uuid; + } + case "removeTab": { + const tab = this._tabs.get(tabId); + this._tabs.delete(tabId); + return lazy.AppUiTestDelegate.removeTab(tab); + } + + default: + throw new Error(`Unknown Test API: ${message.name}.`); + } + } +} diff --git a/testing/specialpowers/content/MockColorPicker.sys.mjs b/testing/specialpowers/content/MockColorPicker.sys.mjs new file mode 100644 index 0000000000..1b4c4d3b3e --- /dev/null +++ b/testing/specialpowers/content/MockColorPicker.sys.mjs @@ -0,0 +1,123 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs", +}); + +const Cm = Components.manager; + +const CONTRACT_ID = "@mozilla.org/colorpicker;1"; + +Cu.crashIfNotInAutomation(); + +var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID = ""; +var newClassID = Services.uuid.generateUUID(); +var newFactory = function (window) { + return { + createInstance(aIID) { + return new MockColorPickerInstance(window).QueryInterface(aIID); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), + }; +}; + +export var MockColorPicker = { + init(window) { + this.reset(); + this.factory = newFactory(window); + if (!registrar.isCIDRegistered(newClassID)) { + try { + oldClassID = registrar.contractIDToCID(CONTRACT_ID); + } catch (ex) { + oldClassID = ""; + dump( + "TEST-INFO | can't get colorpicker registered component, " + + "assuming there is none" + ); + } + registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory); + } + }, + + reset() { + this.returnColor = ""; + this.showCallback = null; + this.shown = false; + this.showing = false; + }, + + cleanup() { + var previousFactory = this.factory; + this.reset(); + this.factory = null; + + registrar.unregisterFactory(newClassID, previousFactory); + if (oldClassID != "") { + registrar.registerFactory(oldClassID, "", CONTRACT_ID, null); + } + }, +}; + +function MockColorPickerInstance(window) { + this.window = window; + this.showCallback = null; + this.showCallbackWrapped = null; +} +MockColorPickerInstance.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIColorPicker"]), + init(aParent, aTitle, aInitialColor, aDefaultColors) { + this.parent = aParent; + this.initialColor = aInitialColor; + this.defaultColors = aDefaultColors; + }, + initialColor: "", + parent: null, + open(aColorPickerShownCallback) { + MockColorPicker.showing = true; + MockColorPicker.shown = true; + + this.window.setTimeout(() => { + let result = ""; + try { + if (typeof MockColorPicker.showCallback == "function") { + if (MockColorPicker.showCallback != this.showCallback) { + this.showCallback = MockColorPicker.showCallback; + if (Cu.isXrayWrapper(this.window)) { + this.showCallbackWrapped = lazy.WrapPrivileged.wrapCallback( + MockColorPicker.showCallback, + this.window + ); + } else { + this.showCallbackWrapped = this.showCallback; + } + } + var updateCb = function (color) { + result = color; + aColorPickerShownCallback.update(color); + }; + let returnColor = this.showCallbackWrapped(this, updateCb); + if (typeof returnColor === "string") { + result = returnColor; + } + } else if (typeof MockColorPicker.returnColor === "string") { + result = MockColorPicker.returnColor; + } + } catch (ex) { + dump( + "TEST-UNEXPECTED-FAIL | Exception in MockColorPicker.sys.mjs open() " + + "method: " + + ex + + "\n" + ); + } + if (aColorPickerShownCallback) { + aColorPickerShownCallback.done(result); + } + }, 0); + }, +}; diff --git a/testing/specialpowers/content/MockFilePicker.sys.mjs b/testing/specialpowers/content/MockFilePicker.sys.mjs new file mode 100644 index 0000000000..9a7dab7c17 --- /dev/null +++ b/testing/specialpowers/content/MockFilePicker.sys.mjs @@ -0,0 +1,315 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs", +}); + +const Cm = Components.manager; + +const CONTRACT_ID = "@mozilla.org/filepicker;1"; + +if (import.meta.url.includes("specialpowers")) { + Cu.crashIfNotInAutomation(); +} + +var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID; +var newClassID = Services.uuid.generateUUID(); +var newFactory = function (window) { + return { + createInstance(aIID) { + return new MockFilePickerInstance(window).QueryInterface(aIID); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), + }; +}; + +export var MockFilePicker = { + returnOK: Ci.nsIFilePicker.returnOK, + returnCancel: Ci.nsIFilePicker.returnCancel, + returnReplace: Ci.nsIFilePicker.returnReplace, + + filterAll: Ci.nsIFilePicker.filterAll, + filterHTML: Ci.nsIFilePicker.filterHTML, + filterText: Ci.nsIFilePicker.filterText, + filterImages: Ci.nsIFilePicker.filterImages, + filterXML: Ci.nsIFilePicker.filterXML, + filterXUL: Ci.nsIFilePicker.filterXUL, + filterApps: Ci.nsIFilePicker.filterApps, + filterAllowURLs: Ci.nsIFilePicker.filterAllowURLs, + filterAudio: Ci.nsIFilePicker.filterAudio, + filterVideo: Ci.nsIFilePicker.filterVideo, + + window: null, + pendingPromises: [], + + init(window) { + this.window = window; + + this.reset(); + this.factory = newFactory(window); + if (!registrar.isCIDRegistered(newClassID)) { + oldClassID = registrar.contractIDToCID(CONTRACT_ID); + registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory); + } + }, + + reset() { + this.appendFilterCallback = null; + this.appendFiltersCallback = null; + this.displayDirectory = null; + this.displaySpecialDirectory = ""; + this.filterIndex = 0; + this.mode = null; + this.returnData = []; + this.returnValue = null; + this.showCallback = null; + this.afterOpenCallback = null; + this.shown = false; + this.showing = false; + }, + + cleanup() { + var previousFactory = this.factory; + this.reset(); + this.factory = null; + if (oldClassID) { + registrar.unregisterFactory(newClassID, previousFactory); + registrar.registerFactory(oldClassID, "", CONTRACT_ID, null); + } + }, + + internalFileData(obj) { + return { + nsIFile: "nsIFile" in obj ? obj.nsIFile : null, + domFile: "domFile" in obj ? obj.domFile : null, + domDirectory: "domDirectory" in obj ? obj.domDirectory : null, + }; + }, + + useAnyFile() { + var file = lazy.FileUtils.getDir("TmpD", [], false); + file.append("testfile"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + let promise = this.window.File.createFromNsIFile(file) + .then( + domFile => domFile, + () => null + ) + // domFile can be null. + .then(domFile => { + this.returnData = [this.internalFileData({ nsIFile: file, domFile })]; + }) + .then(() => file); + + this.pendingPromises = [promise]; + + // We return a promise in order to support some existing mochitests. + return promise; + }, + + useBlobFile() { + var blob = new this.window.Blob([]); + var file = new this.window.File([blob], "helloworld.txt", { + type: "plain/text", + }); + this.returnData = [this.internalFileData({ domFile: file })]; + this.pendingPromises = []; + }, + + useDirectory(aPath) { + var directory = new this.window.Directory(aPath); + this.returnData = [this.internalFileData({ domDirectory: directory })]; + this.pendingPromises = []; + }, + + setFiles(files) { + this.returnData = []; + this.pendingPromises = []; + + for (let file of files) { + if (this.window.File.isInstance(file)) { + this.returnData.push(this.internalFileData({ domFile: file })); + } else { + let promise = this.window.File.createFromNsIFile(file, { + existenceCheck: false, + }); + + promise.then(domFile => { + this.returnData.push( + this.internalFileData({ nsIFile: file, domFile }) + ); + }); + this.pendingPromises.push(promise); + } + } + }, + + getNsIFile() { + if (this.returnData.length >= 1) { + return this.returnData[0].nsIFile; + } + return null; + }, +}; + +function MockFilePickerInstance(window) { + this.window = window; + this.showCallback = null; + this.showCallbackWrapped = null; +} +MockFilePickerInstance.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIFilePicker"]), + init(aParent, aTitle, aMode) { + this.mode = aMode; + this.filterIndex = MockFilePicker.filterIndex; + this.parent = aParent; + }, + appendFilter(aTitle, aFilter) { + if (typeof MockFilePicker.appendFilterCallback == "function") { + MockFilePicker.appendFilterCallback(this, aTitle, aFilter); + } + }, + appendFilters(aFilterMask) { + if (typeof MockFilePicker.appendFiltersCallback == "function") { + MockFilePicker.appendFiltersCallback(this, aFilterMask); + } + }, + defaultString: "", + defaultExtension: "", + parent: null, + filterIndex: 0, + displayDirectory: null, + displaySpecialDirectory: "", + get file() { + if (MockFilePicker.returnData.length >= 1) { + return MockFilePicker.returnData[0].nsIFile; + } + + return null; + }, + + // We don't support directories here. + get domFileOrDirectory() { + if (MockFilePicker.returnData.length < 1) { + return null; + } + + if (MockFilePicker.returnData[0].domFile) { + return MockFilePicker.returnData[0].domFile; + } + + if (MockFilePicker.returnData[0].domDirectory) { + return MockFilePicker.returnData[0].domDirectory; + } + + return null; + }, + get fileURL() { + if ( + MockFilePicker.returnData.length >= 1 && + MockFilePicker.returnData[0].nsIFile + ) { + return Services.io.newFileURI(MockFilePicker.returnData[0].nsIFile); + } + + return null; + }, + *getFiles(asDOM) { + for (let d of MockFilePicker.returnData) { + if (asDOM) { + yield d.domFile || d.domDirectory; + } else if (d.nsIFile) { + yield d.nsIFile; + } else { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + } + }, + get files() { + return this.getFiles(false); + }, + get domFileOrDirectoryEnumerator() { + return this.getFiles(true); + }, + open(aFilePickerShownCallback) { + MockFilePicker.showing = true; + Services.tm.dispatchToMainThread(() => { + // Maybe all the pending promises are already resolved, but we want to be sure. + Promise.all(MockFilePicker.pendingPromises) + .then( + () => { + return Ci.nsIFilePicker.returnOK; + }, + () => { + return Ci.nsIFilePicker.returnCancel; + } + ) + .then(result => { + // Nothing else has to be done. + MockFilePicker.pendingPromises = []; + + if (result == Ci.nsIFilePicker.returnCancel) { + return result; + } + + MockFilePicker.displayDirectory = this.displayDirectory; + MockFilePicker.displaySpecialDirectory = this.displaySpecialDirectory; + MockFilePicker.shown = true; + if (typeof MockFilePicker.showCallback == "function") { + if (MockFilePicker.showCallback != this.showCallback) { + this.showCallback = MockFilePicker.showCallback; + if (Cu.isXrayWrapper(this.window)) { + this.showCallbackWrapped = lazy.WrapPrivileged.wrapCallback( + MockFilePicker.showCallback, + this.window + ); + } else { + this.showCallbackWrapped = this.showCallback; + } + } + try { + var returnValue = this.showCallbackWrapped(this); + if (typeof returnValue != "undefined") { + return returnValue; + } + } catch (ex) { + return Ci.nsIFilePicker.returnCancel; + } + } + + return MockFilePicker.returnValue; + }) + .then(result => { + // Some additional result file can be set by the callback. Let's + // resolve the pending promises again. + return Promise.all(MockFilePicker.pendingPromises).then( + () => { + return result; + }, + () => { + return Ci.nsIFilePicker.returnCancel; + } + ); + }) + .then(result => { + MockFilePicker.pendingPromises = []; + + if (aFilePickerShownCallback) { + aFilePickerShownCallback.done(result); + } + + if (typeof MockFilePicker.afterOpenCallback == "function") { + Services.tm.dispatchToMainThread(() => { + MockFilePicker.afterOpenCallback(this); + }); + } + }); + }); + }, +}; diff --git a/testing/specialpowers/content/MockPermissionPrompt.sys.mjs b/testing/specialpowers/content/MockPermissionPrompt.sys.mjs new file mode 100644 index 0000000000..615c786f2c --- /dev/null +++ b/testing/specialpowers/content/MockPermissionPrompt.sys.mjs @@ -0,0 +1,75 @@ +/* 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/. */ + +const Cm = Components.manager; + +const CONTRACT_ID = "@mozilla.org/content-permission/prompt;1"; + +var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID, oldFactory; +var newClassID = Services.uuid.generateUUID(); +var newFactory = { + createInstance(aIID) { + return new MockPermissionPromptInstance().QueryInterface(aIID); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), +}; + +export var MockPermissionPrompt = { + init() { + this.reset(); + if (!registrar.isCIDRegistered(newClassID)) { + try { + oldClassID = registrar.contractIDToCID(CONTRACT_ID); + oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory); + } catch (ex) { + oldClassID = ""; + oldFactory = null; + dump( + "TEST-INFO | can't get permission prompt registered component, " + + "assuming there is none" + ); + } + if (oldFactory) { + registrar.unregisterFactory(oldClassID, oldFactory); + } + registrar.registerFactory(newClassID, "", CONTRACT_ID, newFactory); + } + }, + + reset() {}, + + cleanup() { + this.reset(); + if (oldFactory) { + registrar.unregisterFactory(newClassID, newFactory); + registrar.registerFactory(oldClassID, "", CONTRACT_ID, oldFactory); + } + }, +}; + +function MockPermissionPromptInstance() {} +MockPermissionPromptInstance.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionPrompt"]), + + promptResult: Ci.nsIPermissionManager.UNKNOWN_ACTION, + + prompt(request) { + let perms = request.types.QueryInterface(Ci.nsIArray); + for (let idx = 0; idx < perms.length; idx++) { + let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType); + if ( + Services.perms.testExactPermissionFromPrincipal( + request.principal, + perm.type + ) != Ci.nsIPermissionManager.ALLOW_ACTION + ) { + request.cancel(); + return; + } + } + + request.allow(); + }, +}; 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 + // 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); + } + 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 + // 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 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