tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SpecialPowersChild.sys.mjs (68116B)


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