diff options
Diffstat (limited to 'testing/specialpowers/content/SpecialPowersParent.sys.mjs')
-rw-r--r-- | testing/specialpowers/content/SpecialPowersParent.sys.mjs | 1481 |
1 files changed, 1481 insertions, 0 deletions
diff --git a/testing/specialpowers/content/SpecialPowersParent.sys.mjs b/testing/specialpowers/content/SpecialPowersParent.sys.mjs new file mode 100644 index 0000000000..2da4760c68 --- /dev/null +++ b/testing/specialpowers/content/SpecialPowersParent.sys.mjs @@ -0,0 +1,1481 @@ +/* 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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionData: "resource://gre/modules/Extension.sys.mjs", + ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs", + HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs", + PerTestCoverageUtils: + "resource://testing-common/PerTestCoverageUtils.sys.mjs", + ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs", + SpecialPowersSandbox: + "resource://testing-common/SpecialPowersSandbox.sys.mjs", +}); + +class SpecialPowersError extends Error { + get name() { + return "SpecialPowersError"; + } +} + +const PREF_TYPES = { + [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID", + [Ci.nsIPrefBranch.PREF_INT]: "INT", + [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL", + [Ci.nsIPrefBranch.PREF_STRING]: "STRING", + number: "INT", + boolean: "BOOL", + string: "STRING", +}; + +// We share a single preference environment stack between all +// SpecialPowers instances, across all processes. +let prefUndoStack = []; +let inPrefEnvOp = false; + +let permissionUndoStack = []; + +function doPrefEnvOp(fn) { + if (inPrefEnvOp) { + throw new Error( + "Reentrant preference environment operations not supported" + ); + } + inPrefEnvOp = true; + try { + return fn(); + } finally { + inPrefEnvOp = false; + } +} + +async function createWindowlessBrowser({ isPrivate = false } = {}) { + const { promiseDocumentLoaded, promiseEvent, promiseObserved } = + ChromeUtils.importESModule( + "resource://gre/modules/ExtensionUtils.sys.mjs" + ).ExtensionUtils; + + let windowlessBrowser = Services.appShell.createWindowlessBrowser(true); + + if (isPrivate) { + let loadContext = windowlessBrowser.docShell.QueryInterface( + Ci.nsILoadContext + ); + loadContext.usePrivateBrowsing = true; + } + + let chromeShell = windowlessBrowser.docShell.QueryInterface( + Ci.nsIWebNavigation + ); + + const system = Services.scriptSecurityManager.getSystemPrincipal(); + chromeShell.createAboutBlankContentViewer(system, system); + windowlessBrowser.browsingContext.useGlobalHistory = false; + chromeShell.loadURI( + Services.io.newURI("chrome://extensions/content/dummy.xhtml"), + { + triggeringPrincipal: system, + } + ); + + await promiseObserved( + "chrome-document-global-created", + win => win.document == chromeShell.document + ); + + let chromeDoc = await promiseDocumentLoaded(chromeShell.document); + + let browser = chromeDoc.createXULElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("remote", "true"); + + let promise = promiseEvent(browser, "XULFrameLoaderCreated"); + chromeDoc.documentElement.appendChild(browser); + + await promise; + + return { windowlessBrowser, browser }; +} + +// Supplies the unique IDs for tasks created by SpecialPowers.spawn(), +// used to bounce assertion messages back down to the correct child. +let nextTaskID = 1; + +// The default actor to send assertions to if a task originated in a +// window without a test harness. +let defaultAssertHandler; + +export class SpecialPowersParent extends JSWindowActorParent { + constructor() { + super(); + + this._messageManager = Services.mm; + this._serviceWorkerListener = null; + + this._observer = this.observe.bind(this); + + this.didDestroy = this.uninit.bind(this); + + this._registerObservers = { + _self: this, + _topics: [], + _add(topic) { + if (!this._topics.includes(topic)) { + this._topics.push(topic); + Services.obs.addObserver(this, topic); + } + }, + observe(aSubject, aTopic, aData) { + var msg = { aData }; + switch (aTopic) { + case "csp-on-violate-policy": + // the subject is either an nsIURI or an nsISupportsCString + let subject = null; + if (aSubject instanceof Ci.nsIURI) { + subject = aSubject.asciiSpec; + } else if (aSubject instanceof Ci.nsISupportsCString) { + subject = aSubject.data; + } else { + throw new Error("Subject must be nsIURI or nsISupportsCString"); + } + msg = { + subject, + data: aData, + }; + this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); + return; + case "xfo-on-violate-policy": + let uriSpec = null; + if (aSubject instanceof Ci.nsIURI) { + uriSpec = aSubject.asciiSpec; + } else { + throw new Error("Subject must be nsIURI"); + } + msg = { + subject: uriSpec, + data: aData, + }; + this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); + return; + default: + this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); + } + }, + }; + + this._basePrefs = null; + this.init(); + + this._crashDumpDir = null; + this._processCrashObserversRegistered = false; + this._chromeScriptListeners = []; + this._extensions = new Map(); + this._taskActors = new Map(); + } + + static registerActor() { + ChromeUtils.registerWindowActor("SpecialPowers", { + allFrames: true, + includeChrome: true, + child: { + esModuleURI: "resource://testing-common/SpecialPowersChild.sys.mjs", + observers: [ + "chrome-document-global-created", + "content-document-global-created", + ], + }, + parent: { + esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs", + }, + }); + } + + static unregisterActor() { + ChromeUtils.unregisterWindowActor("SpecialPowers"); + } + + init() { + Services.obs.addObserver(this._observer, "http-on-modify-request"); + + // We would like to check that tests don't leave service workers around + // after they finish, but that information lives in the parent process. + // Ideally, we'd be able to tell the child processes whenever service + // workers are registered or unregistered so they would know at all times, + // but service worker lifetimes are complicated enough to make that + // difficult. For the time being, let the child process know when a test + // registers a service worker so it can ask, synchronously, at the end if + // the service worker had unregister called on it. + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let self = this; + this._serviceWorkerListener = { + onRegister() { + self.onRegister(); + }, + + onUnregister() { + // no-op + }, + }; + swm.addListener(this._serviceWorkerListener); + + this.getBaselinePrefs(); + } + + uninit() { + if (defaultAssertHandler === this) { + defaultAssertHandler = null; + } + + var obs = Services.obs; + obs.removeObserver(this._observer, "http-on-modify-request"); + this._registerObservers._topics.splice(0).forEach(element => { + obs.removeObserver(this._registerObservers, element); + }); + this._removeProcessCrashObservers(); + + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + swm.removeListener(this._serviceWorkerListener); + } + + observe(aSubject, aTopic, aData) { + function addDumpIDToMessage(propertyName) { + try { + var id = aSubject.getPropertyAsAString(propertyName); + } catch (ex) { + id = null; + } + if (id) { + message.dumpIDs.push({ id, extension: "dmp" }); + message.dumpIDs.push({ id, extension: "extra" }); + } + } + + switch (aTopic) { + case "http-on-modify-request": + if (aSubject instanceof Ci.nsIChannel) { + let uri = aSubject.URI.spec; + this.sendAsyncMessage("specialpowers-http-notify-request", { uri }); + } + break; + + case "ipc:content-shutdown": + aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (!aSubject.hasKey("abnormal")) { + return; // This is a normal shutdown, ignore it + } + + var message = { type: "crash-observed", dumpIDs: [] }; + addDumpIDToMessage("dumpID"); + this.sendAsyncMessage("SPProcessCrashService", message); + break; + } + } + + _getCrashDumpDir() { + if (!this._crashDumpDir) { + this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + this._crashDumpDir.append("minidumps"); + } + return this._crashDumpDir; + } + + _getPendingCrashDumpDir() { + if (!this._pendingCrashDumpDir) { + this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile); + this._pendingCrashDumpDir.append("Crash Reports"); + this._pendingCrashDumpDir.append("pending"); + } + return this._pendingCrashDumpDir; + } + + _deleteCrashDumpFiles(aFilenames) { + var crashDumpDir = this._getCrashDumpDir(); + if (!crashDumpDir.exists()) { + return false; + } + + var success = !!aFilenames.length; + aFilenames.forEach(function (crashFilename) { + var file = crashDumpDir.clone(); + file.append(crashFilename); + if (file.exists()) { + file.remove(false); + } else { + success = false; + } + }); + return success; + } + + _findCrashDumpFiles(aToIgnore) { + var crashDumpDir = this._getCrashDumpDir(); + var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; + if (!entries) { + return []; + } + + var crashDumpFiles = []; + while (entries.hasMoreElements()) { + var file = entries.nextFile; + var path = String(file.path); + if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { + crashDumpFiles.push(path); + } + } + return crashDumpFiles.concat(); + } + + _deletePendingCrashDumpFiles() { + var crashDumpDir = this._getPendingCrashDumpDir(); + var removed = false; + if (crashDumpDir.exists()) { + let entries = crashDumpDir.directoryEntries; + while (entries.hasMoreElements()) { + let file = entries.nextFile; + if (file.isFile()) { + file.remove(false); + removed = true; + } + } + } + return removed; + } + + _addProcessCrashObservers() { + if (this._processCrashObserversRegistered) { + return; + } + + Services.obs.addObserver(this._observer, "ipc:content-shutdown"); + this._processCrashObserversRegistered = true; + } + + _removeProcessCrashObservers() { + if (!this._processCrashObserversRegistered) { + return; + } + + Services.obs.removeObserver(this._observer, "ipc:content-shutdown"); + this._processCrashObserversRegistered = false; + } + + onRegister() { + this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true }); + } + + _getURI(url) { + return Services.io.newURI(url); + } + _notifyCategoryAndObservers(subject, topic, data) { + const serviceMarker = "service,"; + + // First create observers from the category manager. + + let observers = []; + + for (let { value: contractID } of Services.catMan.enumerateCategory( + topic + )) { + let factoryFunction; + if (contractID.substring(0, serviceMarker.length) == serviceMarker) { + contractID = contractID.substring(serviceMarker.length); + factoryFunction = "getService"; + } else { + factoryFunction = "createInstance"; + } + + try { + let handler = Cc[contractID][factoryFunction](); + if (handler) { + let observer = handler.QueryInterface(Ci.nsIObserver); + observers.push(observer); + } + } catch (e) {} + } + + // Next enumerate the registered observers. + for (let observer of Services.obs.enumerateObservers(topic)) { + if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) { + observers.push(observer); + } + } + + observers.forEach(function (observer) { + try { + observer.observe(subject, topic, data); + } catch (e) {} + }); + } + + /* + Iterate through one atomic set of pref actions and perform sets/clears as appropriate. + All actions performed must modify the relevant pref. + + Returns whether we need to wait for a refresh driver tick for the pref to + have effect. This is only needed for ui. and font. prefs, which affect the + look and feel code and have some change-coalescing going on. + */ + _applyPrefs(actions) { + let requiresRefresh = false; + for (let pref of actions) { + // This logic should match PrefRequiresRefresh in reftest.jsm + requiresRefresh = + requiresRefresh || + pref.name == "layout.css.prefers-color-scheme.content-override" || + pref.name.startsWith("ui.") || + pref.name.startsWith("browser.display.") || + pref.name.startsWith("font."); + if (pref.action == "set") { + this._setPref(pref.name, pref.type, pref.value, pref.iid); + } else if (pref.action == "clear") { + Services.prefs.clearUserPref(pref.name); + } + } + return requiresRefresh; + } + + /** + * Take in a list of pref changes to make, pushes their current values + * onto the restore stack, and makes the changes. When the test + * finishes, these changes are reverted. + * + * |inPrefs| must be an object with up to two properties: "set" and "clear". + * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset + * the prefs indicated in |inPrefs.clear|. + * + * For example, you might pass |inPrefs| as: + * + * inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']], + * 'clear': [['clear.this'], ['also.this']] }; + * + * Notice that |set| and |clear| are both an array of arrays. In |set|, each + * of the inner arrays must have the form [pref_name, value] or [pref_name, + * value, iid]. (The latter form is used for prefs with "complex" values.) + * + * In |clear|, each inner array should have the form [pref_name]. + * + * If you set the same pref more than once (or both set and clear a pref), + * the behavior of this method is undefined. + */ + pushPrefEnv(inPrefs) { + return doPrefEnvOp(() => { + let pendingActions = []; + let cleanupActions = []; + + for (let [action, prefs] of Object.entries(inPrefs)) { + for (let pref of prefs) { + let name = pref[0]; + let value = null; + let iid = null; + let type = PREF_TYPES[Services.prefs.getPrefType(name)]; + let originalValue = null; + + if (pref.length == 3) { + value = pref[1]; + iid = pref[2]; + } else if (pref.length == 2) { + value = pref[1]; + } + + /* If pref is not found or invalid it doesn't exist. */ + if (type !== "INVALID") { + if ( + (Services.prefs.prefHasUserValue(name) && action == "clear") || + action == "set" + ) { + originalValue = this._getPref(name, type); + } + } else if (action == "set") { + /* name doesn't exist, so 'clear' is pointless */ + if (iid) { + type = "COMPLEX"; + } + } + + if (type === "INVALID") { + type = PREF_TYPES[typeof value]; + } + if (type === "INVALID") { + throw new Error("Unexpected preference type for " + name); + } + + pendingActions.push({ action, type, name, value, iid }); + + /* Push original preference value or clear into cleanup array */ + var cleanupTodo = { type, name, value: originalValue, iid }; + if (originalValue == null) { + cleanupTodo.action = "clear"; + } else { + cleanupTodo.action = "set"; + } + cleanupActions.push(cleanupTodo); + } + } + + prefUndoStack.push(cleanupActions); + let requiresRefresh = this._applyPrefs(pendingActions); + return { requiresRefresh }; + }); + } + + async popPrefEnv() { + return doPrefEnvOp(() => { + let env = prefUndoStack.pop(); + if (env) { + let requiresRefresh = this._applyPrefs(env); + return { popped: true, requiresRefresh }; + } + return { popped: false, requiresRefresh: false }; + }); + } + + flushPrefEnv() { + let requiresRefresh = false; + while (prefUndoStack.length) { + requiresRefresh |= this.popPrefEnv().requiresRefresh; + } + return { requiresRefresh }; + } + + _setPref(name, type, value, iid) { + switch (type) { + case "BOOL": + return Services.prefs.setBoolPref(name, value); + case "INT": + return Services.prefs.setIntPref(name, value); + case "CHAR": + return Services.prefs.setCharPref(name, value); + case "COMPLEX": + return Services.prefs.setComplexValue(name, iid, value); + case "STRING": + return Services.prefs.setStringPref(name, value); + } + switch (typeof value) { + case "boolean": + return Services.prefs.setBoolPref(name, value); + case "number": + return Services.prefs.setIntPref(name, value); + case "string": + return Services.prefs.setStringPref(name, value); + } + throw new Error( + `Unexpected preference type: ${type} for ${name} with value ${value} and type ${typeof value}` + ); + } + + _getPref(name, type, defaultValue, iid) { + switch (type) { + case "BOOL": + if (defaultValue !== undefined) { + return Services.prefs.getBoolPref(name, defaultValue); + } + return Services.prefs.getBoolPref(name); + case "INT": + if (defaultValue !== undefined) { + return Services.prefs.getIntPref(name, defaultValue); + } + return Services.prefs.getIntPref(name); + case "CHAR": + if (defaultValue !== undefined) { + return Services.prefs.getCharPref(name, defaultValue); + } + return Services.prefs.getCharPref(name); + case "COMPLEX": + return Services.prefs.getComplexValue(name, iid); + case "STRING": + if (defaultValue !== undefined) { + return Services.prefs.getStringPref(name, defaultValue); + } + return Services.prefs.getStringPref(name); + } + throw new Error( + `Unexpected preference type: ${type} for preference ${name}` + ); + } + + getBaselinePrefs() { + this._basePrefs = this._getAllPreferences(); + } + + _comparePrefs(base, target, ignorePrefs, partialMatches) { + let failures = []; + for (const [key, value] of base) { + if (ignorePrefs.includes(key)) { + continue; + } + let partialFind = false; + partialMatches.forEach(pm => { + if (key.startsWith(pm)) { + partialFind = true; + } + }); + if (partialFind) { + continue; + } + + if (value === target.get(key)) { + continue; + } + if (!failures.includes(key)) { + failures.push(key); + } + } + return failures; + } + + comparePrefsToBaseline(ignorePrefs) { + let newPrefs = this._getAllPreferences(); + + // find all items in ignorePrefs that end in *, add to partialMatch + let partialMatch = []; + if (ignorePrefs === undefined) { + ignorePrefs = []; + } + ignorePrefs.forEach(pref => { + if (pref.endsWith("*")) { + partialMatch.push(pref.split("*")[0]); + } + }); + + // find all new prefs different than old + let rv1 = this._comparePrefs( + newPrefs, + this._basePrefs, + ignorePrefs, + partialMatch + ); + + // find all old prefs different than new (in case we delete) + let rv2 = this._comparePrefs( + this._basePrefs, + newPrefs, + ignorePrefs, + partialMatch + ); + + let failures = [...new Set([...rv1, ...rv2])]; + + // reset failures + failures.forEach(f => { + if (this._basePrefs.get(f)) { + this._setPref( + f, + PREF_TYPES[Services.prefs.getPrefType(f)], + this._basePrefs.get(f) + ); + } else { + Services.prefs.clearUserPref(f); + } + }); + + if (ignorePrefs.length > 1) { + return failures; + } + return []; + } + + _getAllPreferences() { + let names = new Map(); + for (let prefName of Services.prefs.getChildList("")) { + let prefType = PREF_TYPES[Services.prefs.getPrefType(prefName)]; + let prefValue = this._getPref(prefName, prefType); + names.set(prefName, prefValue); + } + return names; + } + + _toggleMuteAudio(aMuted) { + let browser = this.browsingContext.top.embedderElement; + if (aMuted) { + browser.mute(); + } else { + browser.unmute(); + } + } + + _permOp(perm) { + switch (perm.op) { + case "add": + Services.perms.addFromPrincipal( + perm.principal, + perm.type, + perm.permission, + perm.expireType, + perm.expireTime + ); + break; + case "remove": + Services.perms.removeFromPrincipal(perm.principal, perm.type); + break; + default: + throw new Error(`Unexpected permission op: ${perm.op}`); + } + } + + pushPermissions(inPermissions) { + let pendingPermissions = []; + let cleanupPermissions = []; + + for (let permission of inPermissions) { + let { principal } = permission; + if (principal.isSystemPrincipal) { + continue; + } + + let originalValue = Services.perms.testPermissionFromPrincipal( + principal, + permission.type + ); + + let perm = permission.allow; + if (typeof perm === "boolean") { + perm = Ci.nsIPermissionManager[perm ? "ALLOW_ACTION" : "DENY_ACTION"]; + } + + if (permission.remove) { + perm = Ci.nsIPermissionManager.UNKNOWN_ACTION; + } + + if (originalValue == perm) { + continue; + } + + let todo = { + op: "add", + type: permission.type, + permission: perm, + value: perm, + principal, + expireType: + typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER + expireTime: + typeof permission.expireTime === "number" ? permission.expireTime : 0, + }; + + var cleanupTodo = Object.assign({}, todo); + + if (permission.remove) { + todo.op = "remove"; + } + + pendingPermissions.push(todo); + + if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) { + cleanupTodo.op = "remove"; + } else { + cleanupTodo.value = originalValue; + cleanupTodo.permission = originalValue; + } + cleanupPermissions.push(cleanupTodo); + } + + permissionUndoStack.push(cleanupPermissions); + + for (let perm of pendingPermissions) { + this._permOp(perm); + } + } + + popPermissions() { + if (permissionUndoStack.length) { + for (let perm of permissionUndoStack.pop()) { + this._permOp(perm); + } + } + } + + flushPermissions() { + while (permissionUndoStack.length) { + this.popPermissions(); + } + } + + _spawnChrome(task, args, caller, imports) { + let sb = new lazy.SpecialPowersSandbox( + null, + data => { + this.sendAsyncMessage("Assert", data); + }, + { imports } + ); + + for (let [global, prop] of Object.entries({ + windowGlobalParent: "manager", + browsingContext: "browsingContext", + })) { + Object.defineProperty(sb.sandbox, global, { + get: () => { + return this[prop]; + }, + enumerable: true, + }); + } + + return sb.execute(task, args, caller); + } + + /** + * messageManager callback function + * This will get requests from our API in the window and process them in chrome for it + **/ + // eslint-disable-next-line complexity + async receiveMessage(aMessage) { + let startTime = Cu.now(); + // Try block so we can use a finally statement to add a profiler marker + // despite all the return statements. + try { + // We explicitly return values in the below code so that this function + // doesn't trigger a flurry of warnings about "does not always return + // a value". + switch (aMessage.name) { + case "SPToggleMuteAudio": + return this._toggleMuteAudio(aMessage.data.mute); + + case "Ping": + return undefined; + + case "SpecialPowers.Quit": + if ( + !AppConstants.RELEASE_OR_BETA && + !AppConstants.DEBUG && + !AppConstants.MOZ_CODE_COVERAGE && + !AppConstants.ASAN && + !AppConstants.TSAN + ) { + if (Services.profiler.IsActive()) { + let filename = Services.env.get("MOZ_PROFILER_SHUTDOWN"); + if (filename) { + await Services.profiler.dumpProfileToFileAsync(filename); + await Services.profiler.StopProfiler(); + } + } + Cu.exitIfInAutomation(); + } else { + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + } + return undefined; + + case "EnsureFocus": + let bc = aMessage.data.browsingContext; + // Send a message to the child telling it to focus the window. + // If the message responds with a browsing context, then + // a child browsing context in a subframe should be focused. + // Iterate until nothing is returned and we get to the most + // deeply nested subframe that should be focused. + do { + let spParent = bc.currentWindowGlobal.getActor("SpecialPowers"); + if (spParent) { + bc = await spParent.sendQuery("EnsureFocus", { + blurSubframe: aMessage.data.blurSubframe, + }); + } + } while (bc && !aMessage.data.blurSubframe); + return undefined; + + case "SpecialPowers.Focus": + if (this.manager.rootFrameLoader) { + this.manager.rootFrameLoader.ownerElement.focus(); + } + return undefined; + + case "SpecialPowers.CreateFiles": + return (async () => { + let filePaths = []; + if (!this._createdFiles) { + this._createdFiles = []; + } + let createdFiles = this._createdFiles; + + let promises = []; + aMessage.data.forEach(function (request) { + const filePerms = 0o666; + let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + if (request.name) { + testFile.appendRelativePath(request.name); + } else { + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms); + } + let outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE + filePerms, + 0 + ); + if (request.data) { + outStream.write(request.data, request.data.length); + } + outStream.close(); + promises.push( + File.createFromFileName(testFile.path, request.options).then( + function (file) { + filePaths.push(file); + } + ) + ); + createdFiles.push(testFile); + }); + + await Promise.all(promises); + return filePaths; + })().catch(e => { + console.error(e); + return Promise.reject(String(e)); + }); + + case "SpecialPowers.RemoveFiles": + if (this._createdFiles) { + this._createdFiles.forEach(function (testFile) { + try { + testFile.remove(false); + } catch (e) {} + }); + this._createdFiles = null; + } + return undefined; + + case "Wakeup": + return undefined; + + case "EvictAllContentViewers": + this.browsingContext.top.sessionHistory.evictAllContentViewers(); + return undefined; + + case "getBaselinePrefs": + return this.getBaselinePrefs(); + + case "comparePrefsToBaseline": + return this.comparePrefsToBaseline(aMessage.data); + + case "PushPrefEnv": + return this.pushPrefEnv(aMessage.data); + + case "PopPrefEnv": + return this.popPrefEnv(); + + case "FlushPrefEnv": + return this.flushPrefEnv(); + + case "PushPermissions": + return this.pushPermissions(aMessage.data); + + case "PopPermissions": + return this.popPermissions(); + + case "FlushPermissions": + return this.flushPermissions(); + + case "SPPrefService": { + let prefs = Services.prefs; + let prefType = aMessage.json.prefType.toUpperCase(); + let { prefName, prefValue, iid, defaultValue } = aMessage.json; + + if (aMessage.json.op == "get") { + if (!prefName || !prefType) { + throw new SpecialPowersError( + "Invalid parameters for get in SPPrefService" + ); + } + + // return null if the pref doesn't exist + if ( + defaultValue === undefined && + prefs.getPrefType(prefName) == prefs.PREF_INVALID + ) { + return null; + } + return this._getPref(prefName, prefType, defaultValue, iid); + } else if (aMessage.json.op == "set") { + if (!prefName || !prefType || prefValue === undefined) { + throw new SpecialPowersError( + "Invalid parameters for set in SPPrefService" + ); + } + + return this._setPref(prefName, prefType, prefValue, iid); + } else if (aMessage.json.op == "clear") { + if (!prefName) { + throw new SpecialPowersError( + "Invalid parameters for clear in SPPrefService" + ); + } + + prefs.clearUserPref(prefName); + } else { + throw new SpecialPowersError("Invalid operation for SPPrefService"); + } + + return undefined; // See comment at the beginning of this function. + } + + case "SPProcessCrashService": { + switch (aMessage.json.op) { + case "register-observer": + this._addProcessCrashObservers(); + break; + case "unregister-observer": + this._removeProcessCrashObservers(); + break; + case "delete-crash-dump-files": + return this._deleteCrashDumpFiles(aMessage.json.filenames); + case "find-crash-dump-files": + return this._findCrashDumpFiles( + aMessage.json.crashDumpFilesToIgnore + ); + case "delete-pending-crash-dump-files": + return this._deletePendingCrashDumpFiles(); + default: + throw new SpecialPowersError( + "Invalid operation for SPProcessCrashService" + ); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPProcessCrashManagerWait": { + let promises = aMessage.json.crashIds.map(crashId => { + return Services.crashmanager.ensureCrashIsPresent(crashId); + }); + return Promise.all(promises); + } + + case "SPPermissionManager": { + let msg = aMessage.data; + switch (msg.op) { + case "add": + case "remove": + this._permOp(msg); + break; + case "has": + let hasPerm = Services.perms.testPermissionFromPrincipal( + msg.principal, + msg.type + ); + return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION; + case "test": + let testPerm = Services.perms.testPermissionFromPrincipal( + msg.principal, + msg.type + ); + return testPerm == msg.value; + default: + throw new SpecialPowersError( + "Invalid operation for SPPermissionManager" + ); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPObserverService": { + let topic = aMessage.json.observerTopic; + switch (aMessage.json.op) { + case "notify": + let data = aMessage.json.observerData; + Services.obs.notifyObservers(null, topic, data); + break; + case "add": + this._registerObservers._add(topic); + break; + default: + throw new SpecialPowersError( + "Invalid operation for SPObserverervice" + ); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPLoadChromeScript": { + let id = aMessage.json.id; + let scriptName; + + let jsScript = aMessage.json.function.body; + if (aMessage.json.url) { + scriptName = aMessage.json.url; + } else if (aMessage.json.function) { + scriptName = + aMessage.json.function.name || + "<loadChromeScript anonymous function>"; + } else { + throw new SpecialPowersError("SPLoadChromeScript: Invalid script"); + } + + // Setup a chrome sandbox that has access to sendAsyncMessage + // and {add,remove}MessageListener in order to communicate with + // the mochitest. + let sb = new lazy.SpecialPowersSandbox( + scriptName, + data => { + this.sendAsyncMessage("Assert", data); + }, + aMessage.data + ); + + Object.assign(sb.sandbox, { + createWindowlessBrowser, + sendAsyncMessage: (name, message) => { + this.sendAsyncMessage("SPChromeScriptMessage", { + id, + name, + message, + }); + }, + addMessageListener: (name, listener) => { + this._chromeScriptListeners.push({ id, name, listener }); + }, + removeMessageListener: (name, listener) => { + let index = this._chromeScriptListeners.findIndex(function (obj) { + return ( + obj.id == id && obj.name == name && obj.listener == listener + ); + }); + if (index >= 0) { + this._chromeScriptListeners.splice(index, 1); + } + }, + actorParent: this.manager, + }); + + // Evaluate the chrome script + try { + Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1); + } catch (e) { + throw new SpecialPowersError( + "Error while executing chrome script '" + + scriptName + + "':\n" + + e + + "\n" + + e.fileName + + ":" + + e.lineNumber + ); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPChromeScriptMessage": { + let id = aMessage.json.id; + let name = aMessage.json.name; + let message = aMessage.json.message; + let result; + for (let listener of this._chromeScriptListeners) { + if (listener.name == name && listener.id == id) { + result = listener.listener(message); + } + } + return result; + } + + case "SPImportInMainProcess": { + var message = { hadError: false, errorMessage: null }; + try { + ChromeUtils.import(aMessage.data); + } catch (e) { + message.hadError = true; + message.errorMessage = e.toString(); + } + return message; + } + + case "SPCleanUpSTSData": { + let origin = aMessage.data.origin; + let uri = Services.io.newURI(origin); + let sss = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + sss.resetState(uri); + return undefined; + } + + case "SPRequestDumpCoverageCounters": { + return lazy.PerTestCoverageUtils.afterTest(); + } + + case "SPRequestResetCoverageCounters": { + return lazy.PerTestCoverageUtils.beforeTest(); + } + + case "SPCheckServiceWorkers": { + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let regs = swm.getAllRegistrations(); + + // XXX This code is shared with specialpowers.js. + let workers = new Array(regs.length); + for (let i = 0; i < regs.length; ++i) { + let { scope, scriptSpec } = regs.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + workers[i] = { scope, scriptSpec }; + } + return { workers }; + } + + case "SPLoadExtension": { + let id = aMessage.data.id; + let ext = aMessage.data.ext; + if (AppConstants.platform === "android") { + // Some extension APIs are partially implemented in Java, and the + // interface between the JS and Java side (GeckoViewWebExtension) + // expects extensions to be registered with the AddonManager. + // + // For simplicity, default to using an Addon Manager (if not null). + if (ext.useAddonManager === undefined) { + ext.useAddonManager = "android-only"; + } + } + // delayedStartup is only supported in xpcshell + if (ext.delayedStartup !== undefined) { + throw new Error( + `delayedStartup is only supported in xpcshell, use "useAddonManager".` + ); + } + + let extension = lazy.ExtensionTestCommon.generate(ext); + + let resultListener = (...args) => { + this.sendAsyncMessage("SPExtensionMessage", { + id, + type: "testResult", + args, + }); + }; + + let messageListener = (...args) => { + args.shift(); + this.sendAsyncMessage("SPExtensionMessage", { + id, + type: "testMessage", + args, + }); + }; + + // Register pass/fail handlers. + extension.on("test-result", resultListener); + extension.on("test-eq", resultListener); + extension.on("test-log", resultListener); + extension.on("test-done", resultListener); + + extension.on("test-message", messageListener); + + this._extensions.set(id, extension); + return undefined; + } + + case "SPStartupExtension": { + let id = aMessage.data.id; + // This is either an Extension, or (if useAddonManager is set) a MockExtension. + let extension = this._extensions.get(id); + extension.on("startup", (eventName, ext) => { + if (AppConstants.platform === "android") { + // We need a way to notify the embedding layer that a new extension + // has been installed, so that the java layer can be updated too. + Services.obs.notifyObservers(null, "testing-installed-addon", id); + } + // ext is always the "real" Extension object, even when "extension" + // is a MockExtension. + this.sendAsyncMessage("SPExtensionMessage", { + id, + type: "extensionSetId", + args: [ext.id, ext.uuid], + }); + }); + + // Make sure the extension passes the packaging checks when + // they're run on a bare archive rather than a running instance, + // as the add-on manager runs them. + let extensionData = new lazy.ExtensionData(extension.rootURI); + return extensionData + .loadManifest() + .then( + () => { + return extensionData.initAllLocales().then(() => { + if (extensionData.errors.length) { + return Promise.reject( + "Extension contains packaging errors" + ); + } + return undefined; + }); + }, + () => { + // loadManifest() will throw if we're loading an embedded + // extension, so don't worry about locale errors in that + // case. + } + ) + .then(async () => { + // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension, + // in that case we have an ID here and we need to set the override. + if (extension.id) { + await lazy.ExtensionTestCommon.setIncognitoOverride(extension); + } + return extension.startup().then( + () => {}, + e => { + dump(`Extension startup failed: ${e}\n${e.stack}`); + throw e; + } + ); + }); + } + + case "SPExtensionMessage": { + let id = aMessage.data.id; + let extension = this._extensions.get(id); + extension.testMessage(...aMessage.data.args); + return undefined; + } + + case "SPExtensionGrantActiveTab": { + let { id, tabId } = aMessage.data; + let { tabManager } = this._extensions.get(id); + tabManager.addActiveTabPermission(tabManager.get(tabId).nativeTab); + return undefined; + } + + case "SPUnloadExtension": { + let id = aMessage.data.id; + let extension = this._extensions.get(id); + this._extensions.delete(id); + return extension.shutdown().then(() => { + return extension._uninstallPromise; + }); + } + + case "SPExtensionTerminateBackground": { + let id = aMessage.data.id; + let args = aMessage.data.args; + let extension = this._extensions.get(id); + return extension.terminateBackground(...args); + } + + case "SPExtensionWakeupBackground": { + let id = aMessage.data.id; + let extension = this._extensions.get(id); + return extension.wakeupBackground(); + } + + case "SetAsDefaultAssertHandler": { + defaultAssertHandler = this; + return undefined; + } + + case "Spawn": { + // Use a different variable for the profiler marker start time + // so that a marker isn't added when we return, but instead when + // our promise resolves. + let spawnStartTime = startTime; + startTime = undefined; + let { browsingContext, task, args, caller, hasHarness, imports } = + aMessage.data; + + let spParent = + browsingContext.currentWindowGlobal.getActor("SpecialPowers"); + + let taskId = nextTaskID++; + if (hasHarness) { + spParent._taskActors.set(taskId, this); + } + + return spParent + .sendQuery("Spawn", { task, args, caller, taskId, imports }) + .finally(() => { + ChromeUtils.addProfilerMarker( + "SpecialPowers", + { startTime: spawnStartTime, category: "Test" }, + aMessage.name + ); + return spParent._taskActors.delete(taskId); + }); + } + + case "SpawnChrome": { + let { task, args, caller, imports } = aMessage.data; + + return this._spawnChrome(task, args, caller, imports); + } + + case "Snapshot": { + let { browsingContext, rect, background, resetScrollPosition } = + aMessage.data; + + return browsingContext.currentWindowGlobal + .drawSnapshot(rect, 1.0, background, resetScrollPosition) + .then(async image => { + let hiddenFrame = new lazy.HiddenFrame(); + let win = await hiddenFrame.get(); + + let canvas = win.document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + + let data = ctx.getImageData(0, 0, image.width, image.height); + hiddenFrame.destroy(); + return data; + }); + } + + case "SecurityState": { + let { browsingContext } = aMessage.data; + return browsingContext.secureBrowserUI.state; + } + + case "ProxiedAssert": { + let { taskId, data } = aMessage.data; + + let actor = this._taskActors.get(taskId) || defaultAssertHandler; + actor.sendAsyncMessage("Assert", data); + + return undefined; + } + + case "SPRemoveAllServiceWorkers": { + return lazy.ServiceWorkerCleanUp.removeAll(); + } + + case "SPRemoveServiceWorkerDataForExampleDomain": { + return lazy.ServiceWorkerCleanUp.removeFromHost("example.com"); + } + + case "SPGenerateMediaControlKeyTestEvent": { + // eslint-disable-next-line no-undef + MediaControlService.generateMediaControlKey(aMessage.data.event); + return undefined; + } + + default: + throw new SpecialPowersError( + `Unrecognized Special Powers API: ${aMessage.name}` + ); + } + // This should be unreachable. If it ever becomes reachable, ESLint + // will produce an error about inconsistent return values. + } finally { + if (startTime) { + ChromeUtils.addProfilerMarker( + "SpecialPowers", + { startTime, category: "Test" }, + aMessage.name + ); + } + } + } +} |