Bug 1691666: part 1) Add some documentation to `SpecialPowersChild.setCommandNode...
[gecko.git] / testing / specialpowers / content / SpecialPowersChild.jsm
blobcd70d3a074b8b1e62b3b5c825bdf44d04932348d
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 /* This code is loaded in every child process that is started by mochitest.
5  */
7 "use strict";
9 var EXPORTED_SYMBOLS = ["SpecialPowersChild"];
11 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
13 const { ExtensionUtils } = ChromeUtils.import(
14   "resource://gre/modules/ExtensionUtils.jsm"
17 ChromeUtils.defineModuleGetter(
18   this,
19   "MockFilePicker",
20   "resource://specialpowers/MockFilePicker.jsm"
22 ChromeUtils.defineModuleGetter(
23   this,
24   "MockColorPicker",
25   "resource://specialpowers/MockColorPicker.jsm"
27 ChromeUtils.defineModuleGetter(
28   this,
29   "MockPermissionPrompt",
30   "resource://specialpowers/MockPermissionPrompt.jsm"
32 ChromeUtils.defineModuleGetter(
33   this,
34   "SpecialPowersSandbox",
35   "resource://specialpowers/SpecialPowersSandbox.jsm"
37 ChromeUtils.defineModuleGetter(
38   this,
39   "WrapPrivileged",
40   "resource://specialpowers/WrapPrivileged.jsm"
42 ChromeUtils.defineModuleGetter(
43   this,
44   "PrivateBrowsingUtils",
45   "resource://gre/modules/PrivateBrowsingUtils.jsm"
47 ChromeUtils.defineModuleGetter(
48   this,
49   "NetUtil",
50   "resource://gre/modules/NetUtil.jsm"
52 ChromeUtils.defineModuleGetter(
53   this,
54   "AppConstants",
55   "resource://gre/modules/AppConstants.jsm"
57 ChromeUtils.defineModuleGetter(
58   this,
59   "PerTestCoverageUtils",
60   "resource://testing-common/PerTestCoverageUtils.jsm"
62 ChromeUtils.defineModuleGetter(
63   this,
64   "ContentTaskUtils",
65   "resource://testing-common/ContentTaskUtils.jsm"
68 Cu.crashIfNotInAutomation();
70 function bindDOMWindowUtils(aWindow) {
71   return aWindow && WrapPrivileged.wrap(aWindow.windowUtils, aWindow);
74 function defineSpecialPowers(sp) {
75   let window = sp.contentWindow;
76   window.SpecialPowers = sp;
77   if (window === window.wrappedJSObject) {
78     return;
79   }
80   // We can't use a generic |defineLazyGetter| because it does not
81   // allow customizing the re-definition behavior.
82   Object.defineProperty(window.wrappedJSObject, "SpecialPowers", {
83     get() {
84       let value = WrapPrivileged.wrap(sp, window);
85       // If we bind |window.wrappedJSObject| when defining the getter
86       // and use it here, it might become a dead wrapper.
87       // We have to retrieve |wrappedJSObject| again.
88       Object.defineProperty(window.wrappedJSObject, "SpecialPowers", {
89         configurable: true,
90         enumerable: true,
91         value,
92         writable: true,
93       });
94       return value;
95     },
96     configurable: true,
97     enumerable: true,
98   });
101 // SPConsoleListener reflects nsIConsoleMessage objects into JS in a
102 // tidy, XPCOM-hiding way.  Messages that are nsIScriptError objects
103 // have their properties exposed in detail.  It also auto-unregisters
104 // itself when it receives a "sentinel" message.
105 function SPConsoleListener(callback, contentWindow) {
106   this.callback = callback;
107   this.contentWindow = contentWindow;
110 SPConsoleListener.prototype = {
111   // Overload the observe method for both nsIConsoleListener and nsIObserver.
112   // The topic will be null for nsIConsoleListener.
113   observe(msg, topic) {
114     let m = {
115       message: msg.message,
116       errorMessage: null,
117       cssSelectors: null,
118       sourceName: null,
119       sourceLine: null,
120       lineNumber: null,
121       columnNumber: null,
122       category: null,
123       windowID: null,
124       isScriptError: false,
125       isConsoleEvent: false,
126       isWarning: false,
127     };
128     if (msg instanceof Ci.nsIScriptError) {
129       m.errorMessage = msg.errorMessage;
130       m.cssSelectors = msg.cssSelectors;
131       m.sourceName = msg.sourceName;
132       m.sourceLine = msg.sourceLine;
133       m.lineNumber = msg.lineNumber;
134       m.columnNumber = msg.columnNumber;
135       m.category = msg.category;
136       m.windowID = msg.outerWindowID;
137       m.innerWindowID = msg.innerWindowID;
138       m.isScriptError = true;
139       m.isWarning = (msg.flags & Ci.nsIScriptError.warningFlag) === 1;
140     } else if (topic === "console-api-log-event") {
141       // This is a dom/console event.
142       let unwrapped = msg.wrappedJSObject;
143       m.errorMessage = unwrapped.arguments[0];
144       m.sourceName = unwrapped.filename;
145       m.lineNumber = unwrapped.lineNumber;
146       m.columnNumber = unwrapped.columnNumber;
147       m.windowID = unwrapped.ID;
148       m.innerWindowID = unwrapped.innerID;
149       m.isConsoleEvent = true;
150       m.isWarning = unwrapped.level === "warning";
151     }
153     Object.freeze(m);
155     // Run in a separate runnable since console listeners aren't
156     // supposed to touch content and this one might.
157     Services.tm.dispatchToMainThread(() => {
158       this.callback.call(undefined, Cu.cloneInto(m, this.contentWindow));
159     });
161     if (!m.isScriptError && !m.isConsoleEvent && m.message === "SENTINEL") {
162       Services.obs.removeObserver(this, "console-api-log-event");
163       Services.console.unregisterListener(this);
164     }
165   },
167   QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener", "nsIObserver"]),
170 class SpecialPowersChild extends JSWindowActorChild {
171   constructor() {
172     super();
174     this._windowID = null;
176     this._encounteredCrashDumpFiles = [];
177     this._unexpectedCrashDumpFiles = {};
178     this._crashDumpDir = null;
179     this._serviceWorkerRegistered = false;
180     this._serviceWorkerCleanUpRequests = new Map();
181     Object.defineProperty(this, "Components", {
182       configurable: true,
183       enumerable: true,
184       value: Components,
185     });
186     this._createFilesOnError = null;
187     this._createFilesOnSuccess = null;
189     this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set());
191     this._consoleListeners = [];
192     this._spawnTaskImports = {};
193     this._encounteredCrashDumpFiles = [];
194     this._unexpectedCrashDumpFiles = {};
195     this._crashDumpDir = null;
196     this._mfl = null;
197     this._asyncObservers = new WeakMap();
198     this._xpcomabi = null;
199     this._os = null;
200     this._pu = null;
202     this._nextExtensionID = 0;
203     this._extensionListeners = null;
205     WrapPrivileged.disableAutoWrap(
206       this.unwrap,
207       this.isWrapper,
208       this.wrapCallback,
209       this.wrapCallbackObject,
210       this.setWrapped,
211       this.nondeterministicGetWeakMapKeys,
212       this.snapshotWindowWithOptions,
213       this.snapshotWindow,
214       this.snapshotRect,
215       this.getDOMRequestService
216     );
217   }
219   observe(aSubject, aTopic, aData) {
220     // Ignore the "{chrome/content}-document-global-created" event. It
221     // is only observed to force creation of the actor.
222   }
224   actorCreated() {
225     this.attachToWindow();
226   }
228   attachToWindow() {
229     let window = this.contentWindow;
230     // We should not invoke the getter.
231     if (!("SpecialPowers" in window.wrappedJSObject)) {
232       this._windowID = window.windowGlobalChild.innerWindowId;
234       defineSpecialPowers(this);
235     }
236   }
238   get window() {
239     return this.contentWindow;
240   }
242   // Hack around devtools sometimes trying to JSON stringify us.
243   toJSON() {
244     return {};
245   }
247   toString() {
248     return "[SpecialPowers]";
249   }
250   sanityCheck() {
251     return "foo";
252   }
254   _addMessageListener(msgname, listener) {
255     this._messageListeners.get(msgname).add(listener);
256   }
258   _removeMessageListener(msgname, listener) {
259     this._messageListeners.get(msgname).delete(listener);
260   }
262   receiveMessage(message) {
263     if (this._messageListeners.has(message.name)) {
264       for (let listener of this._messageListeners.get(message.name)) {
265         try {
266           if (typeof listener === "function") {
267             listener(message);
268           } else {
269             listener.receiveMessage(message);
270           }
271         } catch (e) {
272           Cu.reportError(e);
273         }
274       }
275     }
277     switch (message.name) {
278       case "SPProcessCrashService":
279         if (message.json.type == "crash-observed") {
280           for (let e of message.json.dumpIDs) {
281             this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
282           }
283         }
284         break;
286       case "SPServiceWorkerRegistered":
287         this._serviceWorkerRegistered = message.data.registered;
288         break;
290       case "SpecialPowers.FilesCreated":
291         var createdHandler = this._createFilesOnSuccess;
292         this._createFilesOnSuccess = null;
293         this._createFilesOnError = null;
294         if (createdHandler) {
295           createdHandler(Cu.cloneInto(message.data, this.contentWindow));
296         }
297         break;
299       case "SpecialPowers.FilesError":
300         var errorHandler = this._createFilesOnError;
301         this._createFilesOnSuccess = null;
302         this._createFilesOnError = null;
303         if (errorHandler) {
304           errorHandler(message.data);
305         }
306         break;
308       case "Spawn":
309         let { task, args, caller, taskId, imports } = message.data;
310         return this._spawnTask(task, args, caller, taskId, imports);
312       case "Assert":
313         {
314           if ("info" in message.data) {
315             this.SimpleTest.info(message.data.info);
316             break;
317           }
319           // An assertion has been done in a mochitest chrome script
320           let { name, passed, stack, diag, expectFail } = message.data;
322           let { SimpleTest } = this;
323           if (SimpleTest) {
324             let expected = expectFail ? "fail" : "pass";
325             SimpleTest.record(passed, name, diag, stack, expected);
326           } else {
327             // Well, this is unexpected.
328             dump(name + "\n");
329           }
330         }
331         break;
332     }
333     return undefined;
334   }
336   registerProcessCrashObservers() {
337     this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" });
338   }
340   unregisterProcessCrashObservers() {
341     this.sendAsyncMessage("SPProcessCrashService", {
342       op: "unregister-observer",
343     });
344   }
346   /*
347    * Privileged object wrapping API
348    *
349    * Usage:
350    *   var wrapper = SpecialPowers.wrap(obj);
351    *   wrapper.privilegedMethod(); wrapper.privilegedProperty;
352    *   obj === SpecialPowers.unwrap(wrapper);
353    *
354    * These functions provide transparent access to privileged objects using
355    * various pieces of deep SpiderMagic. Conceptually, a wrapper is just an
356    * object containing a reference to the underlying object, where all method
357    * calls and property accesses are transparently performed with the System
358    * Principal. Moreover, objects obtained from the wrapper (including properties
359    * and method return values) are wrapped automatically. Thus, after a single
360    * call to SpecialPowers.wrap(), the wrapper layer is transitively maintained.
361    *
362    * Known Issues:
363    *
364    *  - The wrapping function does not preserve identity, so
365    *    SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543.
366    *
367    *  - The wrapper cannot see expando properties on unprivileged DOM objects.
368    *    That is to say, the wrapper uses Xray delegation.
369    *
370    *  - The wrapper sometimes guesses certain ES5 attributes for returned
371    *    properties. This is explained in a comment in the wrapper code above,
372    *    and shouldn't be a problem.
373    */
374   wrap(obj) {
375     return obj;
376   }
377   unwrap(obj) {
378     return WrapPrivileged.unwrap(obj);
379   }
380   isWrapper(val) {
381     return WrapPrivileged.isWrapper(val);
382   }
384   /*
385    * Wrap objects on a specified global.
386    */
387   wrapFor(obj, win) {
388     return WrapPrivileged.wrap(obj, win);
389   }
391   /*
392    * When content needs to pass a callback or a callback object to an API
393    * accessed over SpecialPowers, that API may sometimes receive arguments for
394    * whom it is forbidden to create a wrapper in content scopes. As such, we
395    * need a layer to wrap the values in SpecialPowers wrappers before they ever
396    * reach content.
397    */
398   wrapCallback(func) {
399     return WrapPrivileged.wrapCallback(func, this.contentWindow);
400   }
401   wrapCallbackObject(obj) {
402     return WrapPrivileged.wrapCallbackObject(obj, this.contentWindow);
403   }
405   /*
406    * Used for assigning a property to a SpecialPowers wrapper, without unwrapping
407    * the value that is assigned.
408    */
409   setWrapped(obj, prop, val) {
410     if (!WrapPrivileged.isWrapper(obj)) {
411       throw new Error(
412         "You only need to use this for SpecialPowers wrapped objects"
413       );
414     }
416     obj = WrapPrivileged.unwrap(obj);
417     return Reflect.set(obj, prop, val);
418   }
420   /*
421    * Create blank privileged objects to use as out-params for privileged functions.
422    */
423   createBlankObject() {
424     return {};
425   }
427   /*
428    * Because SpecialPowers wrappers don't preserve identity, comparing with ==
429    * can be hazardous. Sometimes we can just unwrap to compare, but sometimes
430    * wrapping the underlying object into a content scope is forbidden. This
431    * function strips any wrappers if they exist and compare the underlying
432    * values.
433    */
434   compare(a, b) {
435     return WrapPrivileged.unwrap(a) === WrapPrivileged.unwrap(b);
436   }
438   get MockFilePicker() {
439     return MockFilePicker;
440   }
442   get MockColorPicker() {
443     return MockColorPicker;
444   }
446   get MockPermissionPrompt() {
447     return MockPermissionPrompt;
448   }
450   quit() {
451     this.sendAsyncMessage("SpecialPowers.Quit", {});
452   }
454   // fileRequests is an array of file requests. Each file request is an object.
455   // A request must have a field |name|, which gives the base of the name of the
456   // file to be created in the profile directory. If the request has a |data| field
457   // then that data will be written to the file.
458   createFiles(fileRequests, onCreation, onError) {
459     return this.sendQuery("SpecialPowers.CreateFiles", fileRequests).then(
460       files => onCreation(Cu.cloneInto(files, this.contentWindow)),
461       onError
462     );
463   }
465   // Remove the files that were created using |SpecialPowers.createFiles()|.
466   // This will be automatically called by |SimpleTest.finish()|.
467   removeFiles() {
468     this.sendAsyncMessage("SpecialPowers.RemoveFiles", {});
469   }
471   executeAfterFlushingMessageQueue(aCallback) {
472     return this.sendQuery("Ping").then(aCallback);
473   }
475   async registeredServiceWorkers() {
476     // For the time being, if parent_intercept is false, we can assume that
477     // ServiceWorkers registered by the current test are all known to the SWM in
478     // this process.
479     if (
480       !Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false)
481     ) {
482       let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
483         Ci.nsIServiceWorkerManager
484       );
485       let regs = swm.getAllRegistrations();
487       // XXX This is shared with SpecialPowersAPIParent.jsm
488       let workers = new Array(regs.length);
489       for (let i = 0; i < workers.length; ++i) {
490         let { scope, scriptSpec } = regs.queryElementAt(
491           i,
492           Ci.nsIServiceWorkerRegistrationInfo
493         );
494         workers[i] = { scope, scriptSpec };
495       }
497       return workers;
498     }
500     // Please see the comment in SpecialPowersObserver.jsm above
501     // this._serviceWorkerListener's assignment for what this returns.
502     if (this._serviceWorkerRegistered) {
503       // This test registered at least one service worker. Send a synchronous
504       // call to the parent to make sure that it called unregister on all of its
505       // service workers.
506       let { workers } = await this.sendQuery("SPCheckServiceWorkers");
507       return workers;
508     }
510     return [];
511   }
513   /*
514    * Load a privileged script that runs same-process. This is different from
515    * |loadChromeScript|, which will run in the parent process in e10s mode.
516    */
517   loadPrivilegedScript(aFunction) {
518     var str = "(" + aFunction.toString() + ")();";
519     let gGlobalObject = Cu.getGlobalForObject(this);
520     let sb = Cu.Sandbox(gGlobalObject);
521     var window = this.contentWindow;
522     var mc = new window.MessageChannel();
523     sb.port = mc.port1;
524     let blob = new Blob([str], { type: "application/javascript" });
525     let blobUrl = URL.createObjectURL(blob);
526     Services.scriptloader.loadSubScript(blobUrl, sb);
528     return mc.port2;
529   }
531   _readUrlAsString(aUrl) {
532     // Fetch script content as we can't use scriptloader's loadSubScript
533     // to evaluate http:// urls...
534     var scriptableStream = Cc[
535       "@mozilla.org/scriptableinputstream;1"
536     ].getService(Ci.nsIScriptableInputStream);
538     var channel = NetUtil.newChannel({
539       uri: aUrl,
540       loadUsingSystemPrincipal: true,
541     });
542     var input = channel.open();
543     scriptableStream.init(input);
545     var str;
546     var buffer = [];
548     while ((str = scriptableStream.read(4096))) {
549       buffer.push(str);
550     }
552     var output = buffer.join("");
554     scriptableStream.close();
555     input.close();
557     var status;
558     if (channel instanceof Ci.nsIHttpChannel) {
559       status = channel.responseStatus;
560     }
562     if (status == 404) {
563       throw new Error(
564         `Error while executing chrome script '${aUrl}':\n` +
565           "The script doesn't exist. Ensure you have registered it in " +
566           "'support-files' in your mochitest.ini."
567       );
568     }
570     return output;
571   }
573   loadChromeScript(urlOrFunction, sandboxOptions) {
574     // Create a unique id for this chrome script
575     let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
576       Ci.nsIUUIDGenerator
577     );
578     let id = uuidGenerator.generateUUID().toString();
580     // Tells chrome code to evaluate this chrome script
581     let scriptArgs = { id, sandboxOptions };
582     if (typeof urlOrFunction == "function") {
583       scriptArgs.function = {
584         body: "(" + urlOrFunction.toString() + ")();",
585         name: urlOrFunction.name,
586       };
587     } else {
588       // Note: We need to do this in the child since, even though
589       // `_readUrlAsString` pretends to be synchronous, its channel
590       // winds up spinning the event loop when loading HTTP URLs. That
591       // leads to unexpected out-of-order operations if the child sends
592       // a message immediately after loading the script.
593       scriptArgs.function = {
594         body: this._readUrlAsString(urlOrFunction),
595       };
596       scriptArgs.url = urlOrFunction;
597     }
598     this.sendAsyncMessage("SPLoadChromeScript", scriptArgs);
600     // Returns a MessageManager like API in order to be
601     // able to communicate with this chrome script
602     let listeners = [];
603     let chromeScript = {
604       addMessageListener: (name, listener) => {
605         listeners.push({ name, listener });
606       },
608       promiseOneMessage: name =>
609         new Promise(resolve => {
610           chromeScript.addMessageListener(name, function listener(message) {
611             chromeScript.removeMessageListener(name, listener);
612             resolve(message);
613           });
614         }),
616       removeMessageListener: (name, listener) => {
617         listeners = listeners.filter(
618           o => o.name != name || o.listener != listener
619         );
620       },
622       sendAsyncMessage: (name, message) => {
623         this.sendAsyncMessage("SPChromeScriptMessage", { id, name, message });
624       },
626       sendQuery: (name, message) => {
627         return this.sendQuery("SPChromeScriptMessage", { id, name, message });
628       },
630       destroy: () => {
631         listeners = [];
632         this._removeMessageListener("SPChromeScriptMessage", chromeScript);
633       },
635       receiveMessage: aMessage => {
636         let messageId = aMessage.json.id;
637         let name = aMessage.json.name;
638         let message = aMessage.json.message;
639         if (this.contentWindow) {
640           message = new StructuredCloneHolder(message).deserialize(
641             this.contentWindow
642           );
643         }
644         // Ignore message from other chrome script
645         if (messageId != id) {
646           return null;
647         }
649         let result;
650         if (aMessage.name == "SPChromeScriptMessage") {
651           for (let listener of listeners.filter(o => o.name == name)) {
652             result = listener.listener(message);
653           }
654         }
655         return result;
656       },
657     };
658     this._addMessageListener("SPChromeScriptMessage", chromeScript);
660     return chromeScript;
661   }
663   async importInMainProcess(importString) {
664     var message = await this.sendQuery("SPImportInMainProcess", importString);
665     if (message.hadError) {
666       throw new Error(
667         "SpecialPowers.importInMainProcess failed with error " +
668           message.errorMessage
669       );
670     }
671   }
673   get Services() {
674     return Services;
675   }
677   /*
678    * Convenient shortcuts to the standard Components abbreviations.
679    */
680   get Cc() {
681     return Cc;
682   }
683   get Ci() {
684     return Ci;
685   }
686   get Cu() {
687     return Cu;
688   }
689   get Cr() {
690     return Cr;
691   }
693   get addProfilerMarker() {
694     return ChromeUtils.addProfilerMarker;
695   }
697   get DOMWindowUtils() {
698     return this.contentWindow.windowUtils;
699   }
701   getDOMWindowUtils(aWindow) {
702     if (aWindow == this.contentWindow) {
703       return aWindow.windowUtils;
704     }
706     return bindDOMWindowUtils(Cu.unwaiveXrays(aWindow));
707   }
709   async toggleMuteState(aMuted, aWindow) {
710     let actor = aWindow
711       ? aWindow.windowGlobalChild.getActor("SpecialPowers")
712       : this;
713     return actor.sendQuery("SPToggleMuteAudio", { mute: aMuted });
714   }
716   /*
717    * A method to get a DOMParser that can't parse XUL.
718    */
719   getNoXULDOMParser() {
720     // If we create it with a system subject principal (so it gets a
721     // nullprincipal), it won't be able to parse XUL by default.
722     return new DOMParser();
723   }
725   get InspectorUtils() {
726     return InspectorUtils;
727   }
729   get PromiseDebugging() {
730     return PromiseDebugging;
731   }
733   async waitForCrashes(aExpectingProcessCrash) {
734     if (!aExpectingProcessCrash) {
735       return;
736     }
738     var crashIds = this._encounteredCrashDumpFiles
739       .filter(filename => {
740         return filename.length === 40 && filename.endsWith(".dmp");
741       })
742       .map(id => {
743         return id.slice(0, -4); // Strip the .dmp extension to get the ID
744       });
746     await this.sendQuery("SPProcessCrashManagerWait", {
747       crashIds,
748     });
749   }
751   async removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
752     var success = true;
753     if (aExpectingProcessCrash) {
754       var message = {
755         op: "delete-crash-dump-files",
756         filenames: this._encounteredCrashDumpFiles,
757       };
758       if (!(await this.sendQuery("SPProcessCrashService", message))) {
759         success = false;
760       }
761     }
762     this._encounteredCrashDumpFiles.length = 0;
763     return success;
764   }
766   async findUnexpectedCrashDumpFiles() {
767     var self = this;
768     var message = {
769       op: "find-crash-dump-files",
770       crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles,
771     };
772     var crashDumpFiles = await this.sendQuery("SPProcessCrashService", message);
773     crashDumpFiles.forEach(function(aFilename) {
774       self._unexpectedCrashDumpFiles[aFilename] = true;
775     });
776     return crashDumpFiles;
777   }
779   removePendingCrashDumpFiles() {
780     var message = {
781       op: "delete-pending-crash-dump-files",
782     };
783     return this.sendQuery("SPProcessCrashService", message);
784   }
786   _setTimeout(callback, delay = 0) {
787     // for mochitest-browser
788     if (typeof this.chromeWindow != "undefined") {
789       this.chromeWindow.setTimeout(callback, delay);
790     }
791     // for mochitest-plain
792     else {
793       this.contentWindow.setTimeout(callback, delay);
794     }
795   }
797   promiseTimeout(delay) {
798     return new Promise(resolve => {
799       this._setTimeout(resolve, delay);
800     });
801   }
803   _delayCallbackTwice(callback) {
804     let delayedCallback = () => {
805       let delayAgain = aCallback => {
806         // Using this._setTimeout doesn't work here
807         // It causes failures in mochtests that use
808         // multiple pushPrefEnv calls
809         // For chrome/browser-chrome mochitests
810         this._setTimeout(aCallback);
811       };
812       delayAgain(delayAgain.bind(this, callback));
813     };
814     return delayedCallback;
815   }
817   /* apply permissions to the system and when the test case is finished (SimpleTest.finish())
818      we will revert the permission back to the original.
820      inPermissions is an array of objects where each object has a type, action, context, ex:
821      [{'type': 'SystemXHR', 'allow': 1, 'context': document},
822       {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}]
824      Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION
825   */
826   async pushPermissions(inPermissions, callback) {
827     let permissions = [];
828     for (let perm of inPermissions) {
829       let principal = this._getPrincipalFromArg(perm.context);
830       permissions.push({
831         ...perm,
832         context: null,
833         principal,
834       });
835     }
837     await this.sendQuery("PushPermissions", permissions).then(callback);
838     await this.promiseTimeout(0);
839   }
841   async popPermissions(callback = null) {
842     await this.sendQuery("PopPermissions").then(callback);
843     await this.promiseTimeout(0);
844   }
846   async flushPermissions(callback = null) {
847     await this.sendQuery("FlushPermissions").then(callback);
848     await this.promiseTimeout(0);
849   }
851   /*
852    * This function should be used when specialpowers is in content process but
853    * it want to get the notification from chrome space.
854    *
855    * This function will call Services.obs.addObserver in SpecialPowersObserver
856    * (that is in chrome process) and forward the data received to SpecialPowers
857    * via messageManager.
858    * You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire
859    * the callback.
860    *
861    * To get the expected data, you should modify
862    * SpecialPowersObserver.prototype._registerObservers.observe. Or the message
863    * you received from messageManager will only contain 'aData' from Service.obs.
864    */
865   registerObservers(topic) {
866     var msg = {
867       op: "add",
868       observerTopic: topic,
869     };
870     return this.sendQuery("SPObserverService", msg);
871   }
873   setTestPluginEnabledState(newEnabledState, pluginName) {
874     return this.sendQuery("SPSetTestPluginEnabledState", {
875       newEnabledState,
876       pluginName,
877     });
878   }
880   async pushPrefEnv(inPrefs, callback = null) {
881     await this.sendQuery("PushPrefEnv", inPrefs).then(callback);
882     await this.promiseTimeout(0);
883   }
885   async popPrefEnv(callback = null) {
886     await this.sendQuery("PopPrefEnv").then(callback);
887     await this.promiseTimeout(0);
888   }
890   async flushPrefEnv(callback = null) {
891     await this.sendQuery("FlushPrefEnv").then(callback);
892     await this.promiseTimeout(0);
893   }
895   _addObserverProxy(notification) {
896     if (notification in this._proxiedObservers) {
897       this._addMessageListener(
898         notification,
899         this._proxiedObservers[notification]
900       );
901     }
902   }
903   _removeObserverProxy(notification) {
904     if (notification in this._proxiedObservers) {
905       this._removeMessageListener(
906         notification,
907         this._proxiedObservers[notification]
908       );
909     }
910   }
912   addObserver(obs, notification, weak) {
913     // Make sure the parent side exists, or we won't get any notifications.
914     this.sendAsyncMessage("Wakeup");
916     this._addObserverProxy(notification);
917     obs = Cu.waiveXrays(obs);
918     if (
919       typeof obs == "object" &&
920       obs.observe.name != "SpecialPowersCallbackWrapper"
921     ) {
922       obs.observe = WrapPrivileged.wrapCallback(
923         Cu.unwaiveXrays(obs.observe),
924         this.contentWindow
925       );
926     }
927     Services.obs.addObserver(obs, notification, weak);
928   }
929   removeObserver(obs, notification) {
930     this._removeObserverProxy(notification);
931     Services.obs.removeObserver(Cu.waiveXrays(obs), notification);
932   }
933   notifyObservers(subject, topic, data) {
934     Services.obs.notifyObservers(subject, topic, data);
935   }
937   /**
938    * An async observer is useful if you're listening for a
939    * notification that normally is only used by C++ code or chrome
940    * code (so it runs in the SystemGroup), but we need to know about
941    * it for a test (which runs as web content). If we used
942    * addObserver, we would assert when trying to enter web content
943    * from a runnabled labeled by the SystemGroup. An async observer
944    * avoids this problem.
945    */
946   addAsyncObserver(obs, notification, weak) {
947     obs = Cu.waiveXrays(obs);
948     if (
949       typeof obs == "object" &&
950       obs.observe.name != "SpecialPowersCallbackWrapper"
951     ) {
952       obs.observe = WrapPrivileged.wrapCallback(
953         Cu.unwaiveXrays(obs.observe),
954         this.contentWindow
955       );
956     }
957     let asyncObs = (...args) => {
958       Services.tm.dispatchToMainThread(() => {
959         if (typeof obs == "function") {
960           obs(...args);
961         } else {
962           obs.observe.call(undefined, ...args);
963         }
964       });
965     };
966     this._asyncObservers.set(obs, asyncObs);
967     Services.obs.addObserver(asyncObs, notification, weak);
968   }
969   removeAsyncObserver(obs, notification) {
970     let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs));
971     Services.obs.removeObserver(asyncObs, notification);
972   }
974   can_QI(obj) {
975     return obj.QueryInterface !== undefined;
976   }
977   do_QueryInterface(obj, iface) {
978     return obj.QueryInterface(Ci[iface]);
979   }
981   call_Instanceof(obj1, obj2) {
982     obj1 = WrapPrivileged.unwrap(obj1);
983     obj2 = WrapPrivileged.unwrap(obj2);
984     return obj1 instanceof obj2;
985   }
987   // Returns a privileged getter from an object. GetOwnPropertyDescriptor does
988   // not work here because xray wrappers don't properly implement it.
989   //
990   // This terribleness is used by dom/base/test/test_object.html because
991   // <object> and <embed> tags will spawn plugins if their prototype is touched,
992   // so we need to get and cache the getter of |hasRunningPlugin| if we want to
993   // call it without paradoxically spawning the plugin.
994   do_lookupGetter(obj, name) {
995     return Object.prototype.__lookupGetter__.call(obj, name);
996   }
998   // Mimic the get*Pref API
999   getBoolPref(...args) {
1000     return Services.prefs.getBoolPref(...args);
1001   }
1002   getIntPref(...args) {
1003     return Services.prefs.getIntPref(...args);
1004   }
1005   getCharPref(...args) {
1006     return Services.prefs.getCharPref(...args);
1007   }
1008   getComplexValue(prefName, iid) {
1009     return Services.prefs.getComplexValue(prefName, iid);
1010   }
1012   getParentBoolPref(prefName, defaultValue) {
1013     return this._getParentPref(prefName, "BOOL", { defaultValue });
1014   }
1015   getParentIntPref(prefName, defaultValue) {
1016     return this._getParentPref(prefName, "INT", { defaultValue });
1017   }
1018   getParentCharPref(prefName, defaultValue) {
1019     return this._getParentPref(prefName, "CHAR", { defaultValue });
1020   }
1022   // Mimic the set*Pref API
1023   setBoolPref(prefName, value) {
1024     return this._setPref(prefName, "BOOL", value);
1025   }
1026   setIntPref(prefName, value) {
1027     return this._setPref(prefName, "INT", value);
1028   }
1029   setCharPref(prefName, value) {
1030     return this._setPref(prefName, "CHAR", value);
1031   }
1032   setComplexValue(prefName, iid, value) {
1033     return this._setPref(prefName, "COMPLEX", value, iid);
1034   }
1036   // Mimic the clearUserPref API
1037   clearUserPref(prefName) {
1038     let msg = {
1039       op: "clear",
1040       prefName,
1041       prefType: "",
1042     };
1043     return this.sendQuery("SPPrefService", msg);
1044   }
1046   // Private pref functions to communicate to chrome
1047   async _getParentPref(prefName, prefType, { defaultValue, iid }) {
1048     let msg = {
1049       op: "get",
1050       prefName,
1051       prefType,
1052       iid, // Only used with complex prefs
1053       defaultValue, // Optional default value
1054     };
1055     let val = await this.sendQuery("SPPrefService", msg);
1056     if (val == null) {
1057       throw new Error(`Error getting pref '${prefName}'`);
1058     }
1059     return val;
1060   }
1061   _getPref(prefName, prefType, { defaultValue }) {
1062     switch (prefType) {
1063       case "BOOL":
1064         return Services.prefs.getBoolPref(prefName);
1065       case "INT":
1066         return Services.prefs.getIntPref(prefName);
1067       case "CHAR":
1068         return Services.prefs.getCharPref(prefName);
1069     }
1070     return undefined;
1071   }
1072   _setPref(prefName, prefType, prefValue, iid) {
1073     let msg = {
1074       op: "set",
1075       prefName,
1076       prefType,
1077       iid, // Only used with complex prefs
1078       prefValue,
1079     };
1080     return this.sendQuery("SPPrefService", msg);
1081   }
1083   _getMUDV(window) {
1084     return window.docShell.contentViewer;
1085   }
1086   // XXX: these APIs really ought to be removed, they're not e10s-safe.
1087   // (also they're pretty Firefox-specific)
1088   _getTopChromeWindow(window) {
1089     return window.browsingContext.topChromeWindow;
1090   }
1091   _getAutoCompletePopup(window) {
1092     return this._getTopChromeWindow(window).document.getElementById(
1093       "PopupAutoComplete"
1094     );
1095   }
1096   addAutoCompletePopupEventListener(window, eventname, listener) {
1097     this._getAutoCompletePopup(window).addEventListener(eventname, listener);
1098   }
1099   removeAutoCompletePopupEventListener(window, eventname, listener) {
1100     this._getAutoCompletePopup(window).removeEventListener(eventname, listener);
1101   }
1102   get formHistory() {
1103     let tmp = {};
1104     ChromeUtils.import("resource://gre/modules/FormHistory.jsm", tmp);
1105     return tmp.FormHistory;
1106   }
1107   getFormFillController(window) {
1108     return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService(
1109       Ci.nsIFormFillController
1110     );
1111   }
1112   attachFormFillControllerTo(window) {
1113     this.getFormFillController().attachPopupElementToDocument(
1114       window.document,
1115       this._getAutoCompletePopup(window)
1116     );
1117   }
1118   detachFormFillControllerFrom(window) {
1119     this.getFormFillController().detachFromDocument(window.document);
1120   }
1121   isBackButtonEnabled(window) {
1122     return !this._getTopChromeWindow(window)
1123       .document.getElementById("Browser:Back")
1124       .hasAttribute("disabled");
1125   }
1126   // XXX end of problematic APIs
1128   addChromeEventListener(type, listener, capture, allowUntrusted) {
1129     this.docShell.chromeEventHandler.addEventListener(
1130       type,
1131       listener,
1132       capture,
1133       allowUntrusted
1134     );
1135   }
1136   removeChromeEventListener(type, listener, capture) {
1137     this.docShell.chromeEventHandler.removeEventListener(
1138       type,
1139       listener,
1140       capture
1141     );
1142   }
1144   async generateMediaControlKeyTestEvent(event) {
1145     await this.sendQuery("SPGenerateMediaControlKeyTestEvent", { event });
1146   }
1148   // Note: each call to registerConsoleListener MUST be paired with a
1149   // call to postConsoleSentinel; when the callback receives the
1150   // sentinel it will unregister itself (_after_ calling the
1151   // callback).  SimpleTest.expectConsoleMessages does this for you.
1152   // If you register more than one console listener, a call to
1153   // postConsoleSentinel will zap all of them.
1154   registerConsoleListener(callback) {
1155     let listener = new SPConsoleListener(callback, this.contentWindow);
1156     Services.console.registerListener(listener);
1158     // listen for dom/console events as well
1159     Services.obs.addObserver(listener, "console-api-log-event");
1160   }
1161   postConsoleSentinel() {
1162     Services.console.logStringMessage("SENTINEL");
1163   }
1164   resetConsole() {
1165     Services.console.reset();
1166   }
1168   getFullZoom(window) {
1169     return BrowsingContext.getFromWindow(window).fullZoom;
1170   }
1172   getDeviceFullZoom(window) {
1173     return this._getMUDV(window).deviceFullZoomForTest;
1174   }
1175   setFullZoom(window, zoom) {
1176     BrowsingContext.getFromWindow(window).fullZoom = zoom;
1177   }
1178   getTextZoom(window) {
1179     return BrowsingContext.getFromWindow(window).textZoom;
1180   }
1181   setTextZoom(window, zoom) {
1182     BrowsingContext.getFromWindow(window).textZoom = zoom;
1183   }
1185   getOverrideDPPX(window) {
1186     return this._getMUDV(window).overrideDPPX;
1187   }
1188   setOverrideDPPX(window, dppx) {
1189     this._getMUDV(window).overrideDPPX = dppx;
1190   }
1192   emulateMedium(window, mediaType) {
1193     BrowsingContext.getFromWindow(window).top.mediumOverride = mediaType;
1194   }
1196   stopEmulatingMedium(window) {
1197     BrowsingContext.getFromWindow(window).top.mediumOverride = "";
1198   }
1200   // Takes a snapshot of the given window and returns a <canvas>
1201   // containing the image. When the window is same-process, the canvas
1202   // is returned synchronously. When it is out-of-process (or when a
1203   // BrowsingContext or FrameLoaderOwner is passed instead of a Window),
1204   // a promise which resolves to such a canvas is returned instead.
1205   snapshotWindowWithOptions(content, rect, bgcolor, options) {
1206     function getImageData(rect, bgcolor, options) {
1207       let el = content.document.createElementNS(
1208         "http://www.w3.org/1999/xhtml",
1209         "canvas"
1210       );
1211       if (rect === undefined) {
1212         rect = {
1213           top: content.scrollY,
1214           left: content.scrollX,
1215           width: content.innerWidth,
1216           height: content.innerHeight,
1217         };
1218       }
1219       if (bgcolor === undefined) {
1220         bgcolor = "rgb(255,255,255)";
1221       }
1222       if (options === undefined) {
1223         options = {};
1224       }
1226       el.width = rect.width;
1227       el.height = rect.height;
1228       let ctx = el.getContext("2d");
1230       let flags = 0;
1231       for (let option in options) {
1232         flags |= options[option] && ctx[option];
1233       }
1235       ctx.drawWindow(
1236         content,
1237         rect.left,
1238         rect.top,
1239         rect.width,
1240         rect.height,
1241         bgcolor,
1242         flags
1243       );
1245       return ctx.getImageData(0, 0, el.width, el.height);
1246     }
1248     let toCanvas = imageData => {
1249       let el = this.document.createElementNS(
1250         "http://www.w3.org/1999/xhtml",
1251         "canvas"
1252       );
1253       el.width = imageData.width;
1254       el.height = imageData.height;
1256       if (ImageData.isInstance(imageData)) {
1257         let ctx = el.getContext("2d");
1258         ctx.putImageData(imageData, 0, 0);
1259       }
1261       return el;
1262     };
1264     if (Window.isInstance(content)) {
1265       // Hack around tests that try to snapshot 0 width or height
1266       // elements.
1267       if (rect && !(rect.width && rect.height)) {
1268         return toCanvas(rect);
1269       }
1271       // This is an in-process window. Snapshot it synchronously.
1272       return toCanvas(getImageData(rect, bgcolor, options));
1273     }
1275     // This is a remote window or frame. Snapshot it asynchronously and
1276     // return a promise for the result. Alas, consumers expect us to
1277     // return a <canvas> element rather than an ImageData object, so we
1278     // need to convert the result from the remote snapshot to a local
1279     // canvas.
1280     let promise = this.spawn(
1281       content,
1282       [rect, bgcolor, options],
1283       getImageData
1284     ).then(toCanvas);
1285     if (Cu.isXrayWrapper(this.contentWindow)) {
1286       return new this.contentWindow.Promise((resolve, reject) => {
1287         promise.then(resolve, reject);
1288       });
1289     }
1290     return promise;
1291   }
1293   snapshotWindow(win, withCaret, rect, bgcolor) {
1294     return this.snapshotWindowWithOptions(win, rect, bgcolor, {
1295       DRAWWINDOW_DRAW_CARET: withCaret,
1296     });
1297   }
1299   snapshotRect(win, rect, bgcolor) {
1300     return this.snapshotWindowWithOptions(win, rect, bgcolor);
1301   }
1303   gc() {
1304     this.contentWindow.windowUtils.garbageCollect();
1305   }
1307   forceGC() {
1308     Cu.forceGC();
1309   }
1311   forceShrinkingGC() {
1312     Cu.forceShrinkingGC();
1313   }
1315   forceCC() {
1316     Cu.forceCC();
1317   }
1319   finishCC() {
1320     Cu.finishCC();
1321   }
1323   ccSlice(budget) {
1324     Cu.ccSlice(budget);
1325   }
1327   // Due to various dependencies between JS objects and C++ objects, an ordinary
1328   // forceGC doesn't necessarily clear all unused objects, thus the GC and CC
1329   // needs to run several times and when no other JS is running.
1330   // The current number of iterations has been determined according to massive
1331   // cross platform testing.
1332   exactGC(callback) {
1333     let count = 0;
1335     function genGCCallback(cb) {
1336       return function() {
1337         Cu.forceCC();
1338         if (++count < 3) {
1339           Cu.schedulePreciseGC(genGCCallback(cb));
1340         } else if (cb) {
1341           cb();
1342         }
1343       };
1344     }
1346     Cu.schedulePreciseGC(genGCCallback(callback));
1347   }
1349   nondeterministicGetWeakMapKeys(m) {
1350     let keys = ChromeUtils.nondeterministicGetWeakMapKeys(m);
1351     if (!keys) {
1352       return undefined;
1353     }
1354     return this.contentWindow.Array.from(keys);
1355   }
1357   getMemoryReports() {
1358     try {
1359       Cc["@mozilla.org/memory-reporter-manager;1"]
1360         .getService(Ci.nsIMemoryReporterManager)
1361         .getReports(
1362           () => {},
1363           null,
1364           () => {},
1365           null,
1366           false
1367         );
1368     } catch (e) {}
1369   }
1371   setGCZeal(zeal) {
1372     Cu.setGCZeal(zeal);
1373   }
1375   isMainProcess() {
1376     try {
1377       return (
1378         Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
1379       );
1380     } catch (e) {}
1381     return true;
1382   }
1384   get XPCOMABI() {
1385     if (this._xpcomabi != null) {
1386       return this._xpcomabi;
1387     }
1389     var xulRuntime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime);
1391     this._xpcomabi = xulRuntime.XPCOMABI;
1392     return this._xpcomabi;
1393   }
1395   // The optional aWin parameter allows the caller to specify a given window in
1396   // whose scope the runnable should be dispatched. If aFun throws, the
1397   // exception will be reported to aWin.
1398   executeSoon(aFun, aWin) {
1399     // Create the runnable in the scope of aWin to avoid running into COWs.
1400     var runnable = {};
1401     if (aWin) {
1402       runnable = Cu.createObjectIn(aWin);
1403     }
1404     runnable.run = aFun;
1405     Cu.dispatch(runnable, aWin);
1406   }
1408   get OS() {
1409     if (this._os != null) {
1410       return this._os;
1411     }
1413     this._os = Services.appinfo.OS;
1414     return this._os;
1415   }
1417   get useRemoteSubframes() {
1418     return this.docShell.nsILoadContext.useRemoteSubframes;
1419   }
1421   addSystemEventListener(target, type, listener, useCapture) {
1422     Services.els.addSystemEventListener(target, type, listener, useCapture);
1423   }
1424   removeSystemEventListener(target, type, listener, useCapture) {
1425     Services.els.removeSystemEventListener(target, type, listener, useCapture);
1426   }
1428   // helper method to check if the event is consumed by either default group's
1429   // event listener or system group's event listener.
1430   defaultPreventedInAnyGroup(event) {
1431     // FYI: Event.defaultPrevented returns false in content context if the
1432     //      event is consumed only by system group's event listeners.
1433     return event.defaultPrevented;
1434   }
1436   getDOMRequestService() {
1437     var serv = Services.DOMRequest;
1438     var res = {};
1439     var props = [
1440       "createRequest",
1441       "createCursor",
1442       "fireError",
1443       "fireSuccess",
1444       "fireDone",
1445       "fireDetailedError",
1446     ];
1447     for (var i in props) {
1448       let prop = props[i];
1449       res[prop] = function() {
1450         return serv[prop].apply(serv, arguments);
1451       };
1452     }
1453     return Cu.cloneInto(res, this.contentWindow, { cloneFunctions: true });
1454   }
1456   addCategoryEntry(category, entry, value, persists, replace) {
1457     Services.catMan.addCategoryEntry(category, entry, value, persists, replace);
1458   }
1460   deleteCategoryEntry(category, entry, persists) {
1461     Services.catMan.deleteCategoryEntry(category, entry, persists);
1462   }
1463   openDialog(win, args) {
1464     return win.openDialog.apply(win, args);
1465   }
1466   // This is a blocking call which creates and spins a native event loop
1467   spinEventLoop(win) {
1468     // simply do a sync XHR back to our windows location.
1469     var syncXHR = new win.XMLHttpRequest();
1470     syncXHR.open("GET", win.location, false);
1471     syncXHR.send();
1472   }
1474   // :jdm gets credit for this.  ex: getPrivilegedProps(window, 'location.href');
1475   getPrivilegedProps(obj, props) {
1476     var parts = props.split(".");
1477     for (var i = 0; i < parts.length; i++) {
1478       var p = parts[i];
1479       if (obj[p] != undefined) {
1480         obj = obj[p];
1481       } else {
1482         return null;
1483       }
1484     }
1485     return obj;
1486   }
1488   _browsingContextForTarget(target) {
1489     if (BrowsingContext.isInstance(target)) {
1490       return target;
1491     }
1492     if (Element.isInstance(target)) {
1493       return target.browsingContext;
1494     }
1496     return BrowsingContext.getFromWindow(target);
1497   }
1499   getBrowsingContextID(target) {
1500     return this._browsingContextForTarget(target).id;
1501   }
1503   *getGroupTopLevelWindows(target) {
1504     let { group } = this._browsingContextForTarget(target);
1505     for (let bc of group.getToplevels()) {
1506       yield bc.window;
1507     }
1508   }
1510   /**
1511    * Runs a task in the context of the given frame, and returns a
1512    * promise which resolves to the return value of that task.
1513    *
1514    * The given frame may be in-process or out-of-process. Either way,
1515    * the task will run asynchronously, in a sandbox with access to the
1516    * frame's content window via its `content` global. Any arguments
1517    * passed will be copied via structured clone, as will its return
1518    * value.
1519    *
1520    * The sandbox also has access to an Assert object, as provided by
1521    * Assert.jsm. Any assertion methods called before the task resolves
1522    * will be relayed back to the test environment of the caller.
1523    *
1524    * @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target
1525    *        The target in which to run the task. This may be any element
1526    *        which implements the FrameLoaderOwner interface (including
1527    *        HTML <iframe> elements and XUL <browser> elements) or a
1528    *        WindowProxy (either in-process or remote).
1529    * @param {Array<any>} args
1530    *        An array of arguments to pass to the task. All arguments
1531    *        must be structured clone compatible, and will be cloned
1532    *        before being passed to the task.
1533    * @param {function} task
1534    *        The function to run in the context of the target. The
1535    *        function will be stringified and re-evaluated in the context
1536    *        of the target's content window. It may return any structured
1537    *        clone compatible value, or a Promise which resolves to the
1538    *        same, which will be returned to the caller.
1539    *
1540    * @returns {Promise<any>}
1541    *        A promise which resolves to the return value of the task, or
1542    *        which rejects if the task raises an exception. As this is
1543    *        being written, the rejection value will always be undefined
1544    *        in the cases where the task throws an error, though that may
1545    *        change in the future.
1546    */
1547   spawn(target, args, task) {
1548     let browsingContext = this._browsingContextForTarget(target);
1550     return this.sendQuery("Spawn", {
1551       browsingContext,
1552       args,
1553       task: String(task),
1554       caller: Cu.getFunctionSourceLocation(task),
1555       hasHarness: typeof this.SimpleTest === "object",
1556       imports: this._spawnTaskImports,
1557     });
1558   }
1560   /**
1561    * Like `spawn`, but spawns a chrome task in the parent process,
1562    * instead. The task additionally has access to `windowGlobalParent`
1563    * and `browsingContext` globals corresponding to the window from
1564    * which the task was spawned.
1565    */
1566   spawnChrome(args, task) {
1567     return this.sendQuery("SpawnChrome", {
1568       args,
1569       task: String(task),
1570       caller: Cu.getFunctionSourceLocation(task),
1571       imports: this._spawnTaskImports,
1572     });
1573   }
1575   snapshotContext(target, rect, background) {
1576     let browsingContext = this._browsingContextForTarget(target);
1578     return this.sendQuery("Snapshot", {
1579       browsingContext,
1580       rect,
1581       background,
1582     }).then(imageData => {
1583       return this.contentWindow.createImageBitmap(imageData);
1584     });
1585   }
1587   getSecurityState(target) {
1588     let browsingContext = this._browsingContextForTarget(target);
1590     return this.sendQuery("SecurityState", {
1591       browsingContext,
1592     });
1593   }
1595   _spawnTask(task, args, caller, taskId, imports) {
1596     let sb = new SpecialPowersSandbox(
1597       null,
1598       data => {
1599         this.sendAsyncMessage("ProxiedAssert", { taskId, data });
1600       },
1601       { imports }
1602     );
1604     sb.sandbox.SpecialPowers = this;
1605     sb.sandbox.ContentTaskUtils = ContentTaskUtils;
1606     for (let [global, prop] of Object.entries({
1607       content: "contentWindow",
1608       docShell: "docShell",
1609     })) {
1610       Object.defineProperty(sb.sandbox, global, {
1611         get: () => {
1612           return this[prop];
1613         },
1614         enumerable: true,
1615       });
1616     }
1618     return sb.execute(task, args, caller);
1619   }
1621   /**
1622    * Automatically imports the given symbol from the given JSM for any
1623    * task spawned by this SpecialPowers instance.
1624    */
1625   addTaskImport(symbol, url) {
1626     this._spawnTaskImports[symbol] = url;
1627   }
1629   get SimpleTest() {
1630     return this._SimpleTest || this.contentWindow.wrappedJSObject.SimpleTest;
1631   }
1632   set SimpleTest(val) {
1633     this._SimpleTest = val;
1634   }
1636   /**
1637    * Sets this actor as the default assertion result handler for tasks
1638    * which originate in a window without a test harness.
1639    */
1640   setAsDefaultAssertHandler() {
1641     this.sendAsyncMessage("SetAsDefaultAssertHandler");
1642   }
1644   getFocusedElementForWindow(targetWindow, aDeep) {
1645     var outParam = {};
1646     Services.focus.getFocusedElementForWindow(targetWindow, aDeep, outParam);
1647     return outParam.value;
1648   }
1650   get focusManager() {
1651     return Services.focus;
1652   }
1654   activeWindow() {
1655     return Services.focus.activeWindow;
1656   }
1658   focusedWindow() {
1659     return Services.focus.focusedWindow;
1660   }
1662   focus(aWindow) {
1663     // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
1664     // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
1665     if (aWindow) {
1666       aWindow.focus();
1667     }
1669     try {
1670       let actor = aWindow
1671         ? aWindow.windowGlobalChild.getActor("SpecialPowers")
1672         : this;
1673       actor.sendAsyncMessage("SpecialPowers.Focus", {});
1674     } catch (e) {
1675       Cu.reportError(e);
1676     }
1677   }
1679   getClipboardData(flavor, whichClipboard) {
1680     if (whichClipboard === undefined) {
1681       whichClipboard = Services.clipboard.kGlobalClipboard;
1682     }
1684     var xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
1685       Ci.nsITransferable
1686     );
1687     xferable.init(this.docShell);
1688     xferable.addDataFlavor(flavor);
1689     Services.clipboard.getData(xferable, whichClipboard);
1690     var data = {};
1691     try {
1692       xferable.getTransferData(flavor, data);
1693     } catch (e) {}
1694     data = data.value || null;
1695     if (data == null) {
1696       return "";
1697     }
1699     return data.QueryInterface(Ci.nsISupportsString).data;
1700   }
1702   clipboardCopyString(str) {
1703     Cc["@mozilla.org/widget/clipboardhelper;1"]
1704       .getService(Ci.nsIClipboardHelper)
1705       .copyString(str);
1706   }
1708   supportsSelectionClipboard() {
1709     return Services.clipboard.supportsSelectionClipboard();
1710   }
1712   swapFactoryRegistration(cid, contractID, newFactory) {
1713     newFactory = Cu.waiveXrays(newFactory);
1715     var componentRegistrar = Components.manager.QueryInterface(
1716       Ci.nsIComponentRegistrar
1717     );
1719     var currentCID = componentRegistrar.contractIDToCID(contractID);
1720     var currentFactory = Components.manager.getClassObject(
1721       Cc[contractID],
1722       Ci.nsIFactory
1723     );
1724     if (cid) {
1725       componentRegistrar.unregisterFactory(currentCID, currentFactory);
1726     } else {
1727       let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
1728         Ci.nsIUUIDGenerator
1729       );
1730       cid = uuidGenerator.generateUUID();
1731     }
1733     // Restore the original factory.
1734     componentRegistrar.registerFactory(cid, "", contractID, newFactory);
1735     return { originalCID: currentCID };
1736   }
1738   _getElement(aWindow, id) {
1739     return typeof id == "string" ? aWindow.document.getElementById(id) : id;
1740   }
1742   dispatchEvent(aWindow, target, event) {
1743     var el = this._getElement(aWindow, target);
1744     return el.dispatchEvent(event);
1745   }
1747   get isDebugBuild() {
1748     return Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2)
1749       .isDebugBuild;
1750   }
1751   assertionCount() {
1752     var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
1753     return debugsvc.assertionCount;
1754   }
1756   /**
1757    * @param arg one of the following:
1758    *            - A URI string.
1759    *            - A document node.
1760    *            - A dictionary including a URL (`url`) and origin attributes (`attr`).
1761    */
1762   _getPrincipalFromArg(arg) {
1763     arg = WrapPrivileged.unwrap(Cu.unwaiveXrays(arg));
1765     if (arg.nodePrincipal) {
1766       // It's a document.
1767       return arg.nodePrincipal;
1768     }
1770     let secMan = Services.scriptSecurityManager;
1771     if (typeof arg == "string") {
1772       // It's a URL.
1773       let uri = Services.io.newURI(arg);
1774       return secMan.createContentPrincipal(uri, {});
1775     }
1777     let uri = Services.io.newURI(arg.url);
1778     let attrs = arg.originAttributes || {};
1779     return secMan.createContentPrincipal(uri, attrs);
1780   }
1782   async addPermission(type, allow, arg, expireType, expireTime) {
1783     let principal = this._getPrincipalFromArg(arg);
1784     if (principal.isSystemPrincipal) {
1785       return; // nothing to do
1786     }
1788     let permission = allow;
1789     if (typeof permission === "boolean") {
1790       permission =
1791         Ci.nsIPermissionManager[allow ? "ALLOW_ACTION" : "DENY_ACTION"];
1792     }
1794     var msg = {
1795       op: "add",
1796       type,
1797       permission,
1798       principal,
1799       expireType: typeof expireType === "number" ? expireType : 0,
1800       expireTime: typeof expireTime === "number" ? expireTime : 0,
1801     };
1803     await this.sendQuery("SPPermissionManager", msg);
1804   }
1806   /**
1807    * @param type see nsIPermissionsManager::testPermissionFromPrincipal.
1808    * @param arg one of the following:
1809    *            - A URI string.
1810    *            - A document node.
1811    *            - A dictionary including a URL (`url`) and origin attributes (`attr`).
1812    */
1813   async removePermission(type, arg) {
1814     let principal = this._getPrincipalFromArg(arg);
1815     if (principal.isSystemPrincipal) {
1816       return; // nothing to do
1817     }
1819     var msg = {
1820       op: "remove",
1821       type,
1822       principal,
1823     };
1825     await this.sendQuery("SPPermissionManager", msg);
1826   }
1828   async hasPermission(type, arg) {
1829     let principal = this._getPrincipalFromArg(arg);
1830     if (principal.isSystemPrincipal) {
1831       return true; // system principals have all permissions
1832     }
1834     var msg = {
1835       op: "has",
1836       type,
1837       principal,
1838     };
1840     return this.sendQuery("SPPermissionManager", msg);
1841   }
1843   async testPermission(type, value, arg) {
1844     let principal = this._getPrincipalFromArg(arg);
1845     if (principal.isSystemPrincipal) {
1846       return true; // system principals have all permissions
1847     }
1849     var msg = {
1850       op: "test",
1851       type,
1852       value,
1853       principal,
1854     };
1855     return this.sendQuery("SPPermissionManager", msg);
1856   }
1858   isContentWindowPrivate(win) {
1859     return PrivateBrowsingUtils.isContentWindowPrivate(win);
1860   }
1862   async notifyObserversInParentProcess(subject, topic, data) {
1863     if (subject) {
1864       throw new Error("Can't send subject to another process!");
1865     }
1866     if (this.isMainProcess()) {
1867       this.notifyObservers(subject, topic, data);
1868       return;
1869     }
1870     var msg = {
1871       op: "notify",
1872       observerTopic: topic,
1873       observerData: data,
1874     };
1875     await this.sendQuery("SPObserverService", msg);
1876   }
1878   removeAllServiceWorkerData() {
1879     return this.sendQuery("SPRemoveAllServiceWorkers", {});
1880   }
1882   removeServiceWorkerDataForExampleDomain() {
1883     return this.sendQuery("SPRemoveServiceWorkerDataForExampleDomain", {});
1884   }
1886   cleanUpSTSData(origin, flags) {
1887     return this.sendQuery("SPCleanUpSTSData", { origin, flags: flags || 0 });
1888   }
1890   async requestDumpCoverageCounters(cb) {
1891     // We want to avoid a roundtrip between child and parent.
1892     if (!PerTestCoverageUtils.enabled) {
1893       return;
1894     }
1896     await this.sendQuery("SPRequestDumpCoverageCounters", {});
1897   }
1899   async requestResetCoverageCounters(cb) {
1900     // We want to avoid a roundtrip between child and parent.
1901     if (!PerTestCoverageUtils.enabled) {
1902       return;
1903     }
1904     await this.sendQuery("SPRequestResetCoverageCounters", {});
1905   }
1907   loadExtension(ext, handler) {
1908     if (this._extensionListeners == null) {
1909       this._extensionListeners = new Set();
1911       this._addMessageListener("SPExtensionMessage", msg => {
1912         for (let listener of this._extensionListeners) {
1913           try {
1914             listener(msg);
1915           } catch (e) {
1916             Cu.reportError(e);
1917           }
1918         }
1919       });
1920     }
1922     // Note, this is not the addon is as used by the AddonManager etc,
1923     // this is just an identifier used for specialpowers messaging
1924     // between this content process and the chrome process.
1925     let id = this._nextExtensionID++;
1927     handler = Cu.waiveXrays(handler);
1928     ext = Cu.waiveXrays(ext);
1930     let sp = this;
1931     let state = "uninitialized";
1932     let extension = {
1933       get state() {
1934         return state;
1935       },
1937       startup() {
1938         state = "pending";
1939         return sp.sendQuery("SPStartupExtension", { id }).then(
1940           () => {
1941             state = "running";
1942           },
1943           () => {
1944             state = "failed";
1945             sp._extensionListeners.delete(listener);
1946             return Promise.reject("startup failed");
1947           }
1948         );
1949       },
1951       unload() {
1952         state = "unloading";
1953         return sp.sendQuery("SPUnloadExtension", { id }).finally(() => {
1954           sp._extensionListeners.delete(listener);
1955           state = "unloaded";
1956         });
1957       },
1959       sendMessage(...args) {
1960         sp.sendAsyncMessage("SPExtensionMessage", { id, args });
1961       },
1963       grantActiveTab(tabId) {
1964         sp.sendAsyncMessage("SPExtensionGrantActiveTab", { id, tabId });
1965       },
1966     };
1968     this.sendAsyncMessage("SPLoadExtension", { ext, id });
1970     let listener = msg => {
1971       if (msg.data.id == id) {
1972         if (msg.data.type == "extensionSetId") {
1973           extension.id = msg.data.args[0];
1974           extension.uuid = msg.data.args[1];
1975         } else if (msg.data.type in handler) {
1976           handler[msg.data.type](
1977             ...Cu.cloneInto(msg.data.args, this.contentWindow)
1978           );
1979         } else {
1980           dump(`Unexpected: ${msg.data.type}\n`);
1981         }
1982       }
1983     };
1985     this._extensionListeners.add(listener);
1986     return extension;
1987   }
1989   invalidateExtensionStorageCache() {
1990     this.notifyObserversInParentProcess(
1991       null,
1992       "extension-invalidate-storage-cache",
1993       ""
1994     );
1995   }
1997   allowMedia(window, enable) {
1998     window.docShell.allowMedia = enable;
1999   }
2001   createChromeCache(name, url) {
2002     let principal = this._getPrincipalFromArg(url);
2003     return new this.contentWindow.CacheStorage(name, principal);
2004   }
2006   loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
2007     const BinaryInputStream = Components.Constructor(
2008       "@mozilla.org/binaryinputstream;1",
2009       "nsIBinaryInputStream",
2010       "setInputStream"
2011     );
2013     return new Promise(function(resolve) {
2014       let listener = {
2015         httpStatus: 0,
2017         onStartRequest(request) {
2018           request.QueryInterface(Ci.nsIHttpChannel);
2019           this.httpStatus = request.responseStatus;
2020         },
2022         onDataAvailable(request, stream, offset, count) {
2023           new BinaryInputStream(stream).readByteArray(count);
2024         },
2026         onStopRequest(request, status) {
2027           /* testing here that the redirect was not followed. If it was followed
2028             we would see a http status of 200 and status of NS_OK */
2030           let httpStatus = this.httpStatus;
2031           resolve({ status, httpStatus });
2032         },
2033       };
2034       let uri = NetUtil.newURI(url);
2035       let channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal });
2037       channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
2038       channel.QueryInterface(Ci.nsIHttpChannelInternal);
2039       channel.documentURI = uri;
2040       channel.asyncOpen(listener);
2041     });
2042   }
2044   get ParserUtils() {
2045     if (this._pu != null) {
2046       return this._pu;
2047     }
2049     let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
2050     // We need to create and return our own wrapper.
2051     this._pu = {
2052       sanitize(src, flags) {
2053         return pu.sanitize(src, flags);
2054       },
2055       convertToPlainText(src, flags, wrapCol) {
2056         return pu.convertToPlainText(src, flags, wrapCol);
2057       },
2058       parseFragment(fragment, flags, isXML, baseURL, element) {
2059         let baseURI = baseURL ? NetUtil.newURI(baseURL) : null;
2060         return pu.parseFragment(
2061           WrapPrivileged.unwrap(fragment),
2062           flags,
2063           isXML,
2064           baseURI,
2065           WrapPrivileged.unwrap(element)
2066         );
2067       },
2068     };
2069     return this._pu;
2070   }
2072   createDOMWalker(node, showAnonymousContent) {
2073     node = WrapPrivileged.unwrap(node);
2074     let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(
2075       Ci.inIDeepTreeWalker
2076     );
2077     walker.showAnonymousContent = showAnonymousContent;
2078     walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
2079     walker.currentNode = node;
2080     let contentWindow = this.contentWindow;
2081     return {
2082       get firstChild() {
2083         return WrapPrivileged.wrap(walker.firstChild(), contentWindow);
2084       },
2085       get lastChild() {
2086         return WrapPrivileged.wrap(walker.lastChild(), contentWindow);
2087       },
2088     };
2089   }
2091   observeMutationEvents(mo, node, nativeAnonymousChildList, subtree) {
2092     WrapPrivileged.unwrap(mo).observe(WrapPrivileged.unwrap(node), {
2093       nativeAnonymousChildList,
2094       subtree,
2095     });
2096   }
2098   doCommand(window, cmd, param) {
2099     switch (cmd) {
2100       case "cmd_align":
2101       case "cmd_backgroundColor":
2102       case "cmd_fontColor":
2103       case "cmd_fontFace":
2104       case "cmd_fontSize":
2105       case "cmd_highlight":
2106       case "cmd_insertImageNoUI":
2107       case "cmd_insertLinkNoUI":
2108       case "cmd_paragraphState":
2109         let params = Cu.createCommandParams();
2110         params.setStringValue("state_attribute", param);
2111         return window.docShell.doCommandWithParams(cmd, params);
2112       default:
2113         return window.docShell.doCommand(cmd);
2114     }
2115   }
2117   isCommandEnabled(window, cmd) {
2118     return window.docShell.isCommandEnabled(cmd);
2119   }
2121   /**
2122    * See \ref nsIContentViewerEdit.setCommandNode(in Node).
2123    */
2124   setCommandNode(window, node) {
2125     return window.docShell.contentViewer
2126       .QueryInterface(Ci.nsIContentViewerEdit)
2127       .setCommandNode(node);
2128   }
2130   /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
2131    * SystemGroup, but some test cases may run as web content. That would assert
2132    * when trying to enter web content from a runnable labeled by the
2133    * SystemGroup. To avoid that, we run classify from SpecialPowers which is
2134    * chrome-privileged and allowed to run inside SystemGroup
2135    */
2137   doUrlClassify(principal, eventTarget, callback) {
2138     let classifierService = Cc[
2139       "@mozilla.org/url-classifier/dbservice;1"
2140     ].getService(Ci.nsIURIClassifier);
2142     let wrapCallback = (...args) => {
2143       Services.tm.dispatchToMainThread(() => {
2144         if (typeof callback == "function") {
2145           callback(...args);
2146         } else {
2147           callback.onClassifyComplete.call(undefined, ...args);
2148         }
2149       });
2150     };
2152     return classifierService.classify(
2153       WrapPrivileged.unwrap(principal),
2154       eventTarget,
2155       wrapCallback
2156     );
2157   }
2159   // TODO: Bug 1353701 - Supports custom event target for labelling.
2160   doUrlClassifyLocal(uri, tables, callback) {
2161     let classifierService = Cc[
2162       "@mozilla.org/url-classifier/dbservice;1"
2163     ].getService(Ci.nsIURIClassifier);
2165     let wrapCallback = results => {
2166       Services.tm.dispatchToMainThread(() => {
2167         if (typeof callback == "function") {
2168           callback(WrapPrivileged.wrap(results, this.contentWindow));
2169         } else {
2170           callback.onClassifyComplete.call(
2171             undefined,
2172             WrapPrivileged.wrap(results, this.contentWindow)
2173           );
2174         }
2175       });
2176     };
2178     let feature = classifierService.createFeatureWithTables(
2179       "test",
2180       tables.split(","),
2181       []
2182     );
2183     return classifierService.asyncClassifyLocalWithFeatures(
2184       WrapPrivileged.unwrap(uri),
2185       [feature],
2186       Ci.nsIUrlClassifierFeature.blocklist,
2187       wrapCallback
2188     );
2189   }
2192 SpecialPowersChild.prototype._proxiedObservers = {
2193   "specialpowers-http-notify-request": function(aMessage) {
2194     let uri = aMessage.json.uri;
2195     Services.obs.notifyObservers(
2196       null,
2197       "specialpowers-http-notify-request",
2198       uri
2199     );
2200   },
2202   "specialpowers-service-worker-shutdown": function(aMessage) {
2203     Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown");
2204   },
2206   "specialpowers-csp-on-violate-policy": function(aMessage) {
2207     let subject = null;
2209     try {
2210       subject = Services.io.newURI(aMessage.data.subject);
2211     } catch (ex) {
2212       // if it's not a valid URI it must be an nsISupportsCString
2213       subject = Cc["@mozilla.org/supports-cstring;1"].createInstance(
2214         Ci.nsISupportsCString
2215       );
2216       subject.data = aMessage.data.subject;
2217     }
2218     Services.obs.notifyObservers(
2219       subject,
2220       "specialpowers-csp-on-violate-policy",
2221       aMessage.data.data
2222     );
2223   },
2225   "specialpowers-xfo-on-violate-policy": function(aMessage) {
2226     let subject = Services.io.newURI(aMessage.data.subject);
2227     Services.obs.notifyObservers(
2228       subject,
2229       "specialpowers-xfo-on-violate-policy",
2230       aMessage.data.data
2231     );
2232   },
2235 SpecialPowersChild.prototype.EARLY_BETA_OR_EARLIER =
2236   AppConstants.EARLY_BETA_OR_EARLIER;