tor-browser

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

SpecialPowersParent.sys.mjs (46928B)


      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 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  ExtensionData: "resource://gre/modules/Extension.sys.mjs",
     11  ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
     12  HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs",
     13  PerTestCoverageUtils:
     14    "resource://testing-common/PerTestCoverageUtils.sys.mjs",
     15  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
     16  SpecialPowersSandbox:
     17    "resource://testing-common/SpecialPowersSandbox.sys.mjs",
     18 });
     19 
     20 class SpecialPowersError extends Error {
     21  get name() {
     22    return "SpecialPowersError";
     23  }
     24 }
     25 
     26 const PREF_TYPES = {
     27  [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID",
     28  [Ci.nsIPrefBranch.PREF_INT]: "INT",
     29  [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL",
     30  [Ci.nsIPrefBranch.PREF_STRING]: "STRING",
     31  number: "INT",
     32  boolean: "BOOL",
     33  string: "STRING",
     34 };
     35 
     36 // We share a single preference environment stack between all
     37 // SpecialPowers instances, across all processes.
     38 let prefUndoStack = [];
     39 let inPrefEnvOp = false;
     40 
     41 let permissionUndoStack = [];
     42 
     43 function doPrefEnvOp(fn) {
     44  if (inPrefEnvOp) {
     45    throw new Error(
     46      "Reentrant preference environment operations not supported"
     47    );
     48  }
     49  inPrefEnvOp = true;
     50  try {
     51    return fn();
     52  } finally {
     53    inPrefEnvOp = false;
     54  }
     55 }
     56 
     57 async function createWindowlessBrowser({ isPrivate = false } = {}) {
     58  const { promiseDocumentLoaded, promiseEvent, promiseObserved } =
     59    ChromeUtils.importESModule(
     60      "resource://gre/modules/ExtensionUtils.sys.mjs"
     61    ).ExtensionUtils;
     62 
     63  let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
     64 
     65  if (isPrivate) {
     66    let loadContext = windowlessBrowser.docShell.QueryInterface(
     67      Ci.nsILoadContext
     68    );
     69    loadContext.usePrivateBrowsing = true;
     70  }
     71 
     72  let chromeShell = windowlessBrowser.docShell.QueryInterface(
     73    Ci.nsIWebNavigation
     74  );
     75 
     76  const system = Services.scriptSecurityManager.getSystemPrincipal();
     77  chromeShell.createAboutBlankDocumentViewer(system, system);
     78  windowlessBrowser.browsingContext.useGlobalHistory = false;
     79  chromeShell.loadURI(
     80    Services.io.newURI("chrome://extensions/content/dummy.xhtml"),
     81    {
     82      triggeringPrincipal: system,
     83    }
     84  );
     85 
     86  await promiseObserved(
     87    "chrome-document-global-created",
     88    win => win.document == chromeShell.document
     89  );
     90 
     91  let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
     92 
     93  let browser = chromeDoc.createXULElement("browser");
     94  browser.setAttribute("type", "content");
     95  browser.setAttribute("disableglobalhistory", "true");
     96  browser.setAttribute("remote", "true");
     97 
     98  let promise = promiseEvent(browser, "XULFrameLoaderCreated");
     99  chromeDoc.documentElement.appendChild(browser);
    100 
    101  await promise;
    102 
    103  return { windowlessBrowser, browser };
    104 }
    105 
    106 // Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
    107 // used to bounce assertion messages back down to the correct child.
    108 let nextTaskID = 1;
    109 
    110 // The default actor to send assertions to if a task originated in a
    111 // window without a test harness.
    112 let defaultAssertHandler;
    113 
    114 export class SpecialPowersParent extends JSWindowActorParent {
    115  constructor() {
    116    super();
    117 
    118    this._messageManager = Services.mm;
    119    this._serviceWorkerListener = null;
    120 
    121    this._observer = this.observe.bind(this);
    122 
    123    this.didDestroy = this.uninit.bind(this);
    124 
    125    this._registerObservers = {
    126      _self: this,
    127      _topics: [],
    128      _add(topic) {
    129        if (!this._topics.includes(topic)) {
    130          this._topics.push(topic);
    131          Services.obs.addObserver(this, topic);
    132        }
    133      },
    134      observe(aSubject, aTopic, aData) {
    135        var msg = { aData };
    136        switch (aTopic) {
    137          case "csp-on-violate-policy": {
    138            // the subject is either an nsIURI or an nsISupportsCString
    139            let subject = null;
    140            if (aSubject instanceof Ci.nsIURI) {
    141              subject = aSubject.asciiSpec;
    142            } else if (aSubject instanceof Ci.nsISupportsCString) {
    143              subject = aSubject.data;
    144            } else {
    145              throw new Error("Subject must be nsIURI or nsISupportsCString");
    146            }
    147            msg = {
    148              subject,
    149              data: aData,
    150            };
    151            this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
    152            return;
    153          }
    154          case "xfo-on-violate-policy": {
    155            let uriSpec = null;
    156            if (aSubject instanceof Ci.nsIURI) {
    157              uriSpec = aSubject.asciiSpec;
    158            } else {
    159              throw new Error("Subject must be nsIURI");
    160            }
    161            msg = {
    162              subject: uriSpec,
    163              data: aData,
    164            };
    165            this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
    166            return;
    167          }
    168          default:
    169            this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
    170        }
    171      },
    172    };
    173 
    174    this._basePrefs = null;
    175    this.init();
    176 
    177    this._crashDumpDir = null;
    178    this._processCrashObserversRegistered = false;
    179    this._chromeScriptListeners = [];
    180    this._extensions = new Map();
    181    this._taskActors = new Map();
    182  }
    183 
    184  static registerActor() {
    185    ChromeUtils.registerWindowActor("SpecialPowers", {
    186      allFrames: true,
    187      includeChrome: true,
    188      child: {
    189        esModuleURI: "resource://testing-common/SpecialPowersChild.sys.mjs",
    190        observers: [
    191          "chrome-document-global-created",
    192          "content-document-global-created",
    193        ],
    194      },
    195      parent: {
    196        esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs",
    197      },
    198    });
    199    ChromeUtils.registerProcessActor("SpecialPowersProcessActor", {
    200      child: {
    201        esModuleURI:
    202          "resource://testing-common/SpecialPowersProcessActor.sys.mjs",
    203      },
    204      parent: {
    205        esModuleURI:
    206          "resource://testing-common/SpecialPowersProcessActor.sys.mjs",
    207      },
    208    });
    209  }
    210 
    211  static unregisterActor() {
    212    ChromeUtils.unregisterWindowActor("SpecialPowers");
    213    ChromeUtils.unregisterProcessActor("SpecialPowersProcessActor");
    214  }
    215 
    216  init() {
    217    Services.obs.addObserver(this._observer, "http-on-modify-request");
    218 
    219    // We would like to check that tests don't leave service workers around
    220    // after they finish, but that information lives in the parent process.
    221    // Ideally, we'd be able to tell the child processes whenever service
    222    // workers are registered or unregistered so they would know at all times,
    223    // but service worker lifetimes are complicated enough to make that
    224    // difficult. For the time being, let the child process know when a test
    225    // registers a service worker so it can ask, synchronously, at the end if
    226    // the service worker had unregister called on it.
    227    let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
    228      Ci.nsIServiceWorkerManager
    229    );
    230    let self = this;
    231    this._serviceWorkerListener = {
    232      onRegister() {
    233        self.onRegister();
    234      },
    235 
    236      onUnregister() {
    237        // no-op
    238      },
    239    };
    240    swm.addListener(this._serviceWorkerListener);
    241 
    242    this.getBaselinePrefs();
    243  }
    244 
    245  uninit() {
    246    if (defaultAssertHandler === this) {
    247      defaultAssertHandler = null;
    248    }
    249 
    250    var obs = Services.obs;
    251    obs.removeObserver(this._observer, "http-on-modify-request");
    252    this._registerObservers._topics.splice(0).forEach(element => {
    253      obs.removeObserver(this._registerObservers, element);
    254    });
    255    this._removeProcessCrashObservers();
    256 
    257    let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
    258      Ci.nsIServiceWorkerManager
    259    );
    260    swm.removeListener(this._serviceWorkerListener);
    261  }
    262 
    263  observe(aSubject, aTopic) {
    264    function addDumpIDToMessage(propertyName) {
    265      try {
    266        var id = aSubject.getPropertyAsAString(propertyName);
    267      } catch (ex) {
    268        id = null;
    269      }
    270      if (id) {
    271        message.dumpIDs.push({ id, extension: "dmp" });
    272        message.dumpIDs.push({ id, extension: "extra" });
    273      }
    274    }
    275 
    276    switch (aTopic) {
    277      case "http-on-modify-request":
    278        if (aSubject instanceof Ci.nsIChannel) {
    279          let uri = aSubject.URI.spec;
    280          this.sendAsyncMessage("specialpowers-http-notify-request", { uri });
    281        }
    282        break;
    283 
    284      case "ipc:content-shutdown":
    285        aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
    286        if (!aSubject.hasKey("abnormal")) {
    287          return; // This is a normal shutdown, ignore it
    288        }
    289 
    290        var message = { type: "crash-observed", dumpIDs: [] };
    291        addDumpIDToMessage("dumpID");
    292        this.sendAsyncMessage("SPProcessCrashService", message);
    293        break;
    294    }
    295  }
    296 
    297  _getCrashDumpDir() {
    298    if (!this._crashDumpDir) {
    299      this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    300      this._crashDumpDir.append("minidumps");
    301    }
    302    return this._crashDumpDir;
    303  }
    304 
    305  _getPendingCrashDumpDir() {
    306    if (!this._pendingCrashDumpDir) {
    307      this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
    308      this._pendingCrashDumpDir.append("Crash Reports");
    309      this._pendingCrashDumpDir.append("pending");
    310    }
    311    return this._pendingCrashDumpDir;
    312  }
    313 
    314  _deleteCrashDumpFiles(aFilenames) {
    315    var crashDumpDir = this._getCrashDumpDir();
    316    if (!crashDumpDir.exists()) {
    317      return false;
    318    }
    319 
    320    var success = !!aFilenames.length;
    321    aFilenames.forEach(function (crashFilename) {
    322      var file = crashDumpDir.clone();
    323      file.append(crashFilename);
    324      if (file.exists()) {
    325        file.remove(false);
    326      } else {
    327        success = false;
    328      }
    329    });
    330    return success;
    331  }
    332 
    333  _findCrashDumpFiles(aToIgnore) {
    334    var crashDumpDir = this._getCrashDumpDir();
    335    var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
    336    if (!entries) {
    337      return [];
    338    }
    339 
    340    var crashDumpFiles = [];
    341    while (entries.hasMoreElements()) {
    342      var file = entries.nextFile;
    343      var path = String(file.path);
    344      if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
    345        crashDumpFiles.push(path);
    346      }
    347    }
    348    return crashDumpFiles.concat();
    349  }
    350 
    351  _deletePendingCrashDumpFiles() {
    352    var crashDumpDir = this._getPendingCrashDumpDir();
    353    var removed = false;
    354    if (crashDumpDir.exists()) {
    355      let entries = crashDumpDir.directoryEntries;
    356      while (entries.hasMoreElements()) {
    357        let file = entries.nextFile;
    358        if (file.isFile()) {
    359          file.remove(false);
    360          removed = true;
    361        }
    362      }
    363    }
    364    return removed;
    365  }
    366 
    367  _addProcessCrashObservers() {
    368    if (this._processCrashObserversRegistered) {
    369      return;
    370    }
    371 
    372    Services.obs.addObserver(this._observer, "ipc:content-shutdown");
    373    this._processCrashObserversRegistered = true;
    374  }
    375 
    376  _removeProcessCrashObservers() {
    377    if (!this._processCrashObserversRegistered) {
    378      return;
    379    }
    380 
    381    Services.obs.removeObserver(this._observer, "ipc:content-shutdown");
    382    this._processCrashObserversRegistered = false;
    383  }
    384 
    385  onRegister() {
    386    this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true });
    387  }
    388 
    389  _getURI(url) {
    390    return Services.io.newURI(url);
    391  }
    392  _notifyCategoryAndObservers(subject, topic, data) {
    393    const serviceMarker = "service,";
    394 
    395    // First create observers from the category manager.
    396 
    397    let observers = [];
    398 
    399    for (let { value: contractID } of Services.catMan.enumerateCategory(
    400      topic
    401    )) {
    402      let factoryFunction;
    403      if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
    404        contractID = contractID.substring(serviceMarker.length);
    405        factoryFunction = "getService";
    406      } else {
    407        factoryFunction = "createInstance";
    408      }
    409 
    410      try {
    411        let handler = Cc[contractID][factoryFunction]();
    412        if (handler) {
    413          let observer = handler.QueryInterface(Ci.nsIObserver);
    414          observers.push(observer);
    415        }
    416      } catch (e) {}
    417    }
    418 
    419    // Next enumerate the registered observers.
    420    for (let observer of Services.obs.enumerateObservers(topic)) {
    421      if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) {
    422        observers.push(observer);
    423      }
    424    }
    425 
    426    observers.forEach(function (observer) {
    427      try {
    428        observer.observe(subject, topic, data);
    429      } catch (e) {}
    430    });
    431  }
    432 
    433  /*
    434    Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
    435    All actions performed must modify the relevant pref.
    436 
    437    Returns whether we need to wait for a refresh driver tick for the pref to
    438    have effect. This is only needed for ui. and font. prefs, which affect the
    439    look and feel code and have some change-coalescing going on.
    440  */
    441  _applyPrefs(actions) {
    442    let requiresRefresh = false;
    443    for (let pref of actions) {
    444      // This logic should match PrefRequiresRefresh in reftest.sys.mjs
    445      requiresRefresh =
    446        requiresRefresh ||
    447        pref.name == "layout.css.prefers-color-scheme.content-override" ||
    448        pref.name.startsWith("ui.") ||
    449        pref.name.startsWith("browser.display.") ||
    450        pref.name.startsWith("font.");
    451      if (pref.action == "set") {
    452        this._setPref(pref.name, pref.type, pref.value, pref.iid);
    453      } else if (pref.action == "clear") {
    454        Services.prefs.clearUserPref(pref.name);
    455      }
    456    }
    457    return requiresRefresh;
    458  }
    459 
    460  /**
    461   * Take in a list of pref changes to make, pushes their current values
    462   * onto the restore stack, and makes the changes.  When the test
    463   * finishes, these changes are reverted.
    464   *
    465   * |inPrefs| must be an object with up to two properties: "set" and "clear".
    466   * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
    467   * the prefs indicated in |inPrefs.clear|.
    468   *
    469   * For example, you might pass |inPrefs| as:
    470   *
    471   *  inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
    472   *             'clear': [['clear.this'], ['also.this']] };
    473   *
    474   * Notice that |set| and |clear| are both an array of arrays.  In |set|, each
    475   * of the inner arrays must have the form [pref_name, value] or [pref_name,
    476   * value, iid].  (The latter form is used for prefs with "complex" values.)
    477   *
    478   * In |clear|, each inner array should have the form [pref_name].
    479   *
    480   * If you set the same pref more than once (or both set and clear a pref),
    481   * the behavior of this method is undefined.
    482   */
    483  pushPrefEnv(inPrefs) {
    484    return doPrefEnvOp(() => {
    485      let pendingActions = [];
    486      let cleanupActions = [];
    487 
    488      for (let [action, prefs] of Object.entries(inPrefs)) {
    489        for (let pref of prefs) {
    490          let name = pref[0];
    491          let value = null;
    492          let iid = null;
    493          let type = PREF_TYPES[Services.prefs.getPrefType(name)];
    494          let originalValue = null;
    495 
    496          if (pref.length == 3) {
    497            value = pref[1];
    498            iid = pref[2];
    499          } else if (pref.length == 2) {
    500            value = pref[1];
    501          }
    502 
    503          /* If pref is not found or invalid it doesn't exist. */
    504          if (type !== "INVALID") {
    505            if (
    506              (Services.prefs.prefHasUserValue(name) && action == "clear") ||
    507              action == "set"
    508            ) {
    509              originalValue = this._getPref(name, type);
    510            }
    511          } else if (action == "set") {
    512            /* name doesn't exist, so 'clear' is pointless */
    513            if (iid) {
    514              type = "COMPLEX";
    515            }
    516          }
    517 
    518          if (type === "INVALID") {
    519            type = PREF_TYPES[typeof value];
    520          }
    521          if (type === "INVALID") {
    522            throw new Error("Unexpected preference type for " + name);
    523          }
    524 
    525          pendingActions.push({ action, type, name, value, iid });
    526 
    527          /* Push original preference value or clear into cleanup array */
    528          var cleanupTodo = { type, name, value: originalValue, iid };
    529          if (originalValue == null) {
    530            cleanupTodo.action = "clear";
    531          } else {
    532            cleanupTodo.action = "set";
    533          }
    534          cleanupActions.push(cleanupTodo);
    535        }
    536      }
    537 
    538      prefUndoStack.push(cleanupActions);
    539      let requiresRefresh = this._applyPrefs(pendingActions);
    540      return { requiresRefresh };
    541    });
    542  }
    543 
    544  popPrefEnv() {
    545    return doPrefEnvOp(() => {
    546      let env = prefUndoStack.pop();
    547      if (env) {
    548        let requiresRefresh = this._applyPrefs(env);
    549        return { popped: true, requiresRefresh };
    550      }
    551      return { popped: false, requiresRefresh: false };
    552    });
    553  }
    554 
    555  flushPrefEnv() {
    556    let requiresRefresh = false;
    557    while (prefUndoStack.length) {
    558      // bitwise |= (and not logical ||=) so that we always call popPrefEnv and
    559      // don't lazily evaluate.
    560      requiresRefresh |= this.popPrefEnv().requiresRefresh;
    561    }
    562    // Make requiresRefresh a boolean from number.
    563    requiresRefresh = !!requiresRefresh;
    564    return { requiresRefresh };
    565  }
    566 
    567  _setPref(name, type, value, iid) {
    568    switch (type) {
    569      case "BOOL":
    570        return Services.prefs.setBoolPref(name, value);
    571      case "INT":
    572        return Services.prefs.setIntPref(name, value);
    573      case "CHAR":
    574        return Services.prefs.setCharPref(name, value);
    575      case "COMPLEX":
    576        return Services.prefs.setComplexValue(name, iid, value);
    577      case "STRING":
    578        return Services.prefs.setStringPref(name, value);
    579    }
    580    switch (typeof value) {
    581      case "boolean":
    582        return Services.prefs.setBoolPref(name, value);
    583      case "number":
    584        return Services.prefs.setIntPref(name, value);
    585      case "string":
    586        return Services.prefs.setStringPref(name, value);
    587    }
    588    throw new Error(
    589      `Unexpected preference type: ${type} for ${name} with value ${value} and type ${typeof value}`
    590    );
    591  }
    592 
    593  _getPref(name, type, defaultValue, iid) {
    594    switch (type) {
    595      case "BOOL":
    596        if (defaultValue !== undefined) {
    597          return Services.prefs.getBoolPref(name, defaultValue);
    598        }
    599        return Services.prefs.getBoolPref(name);
    600      case "INT":
    601        if (defaultValue !== undefined) {
    602          return Services.prefs.getIntPref(name, defaultValue);
    603        }
    604        return Services.prefs.getIntPref(name);
    605      case "CHAR":
    606        if (defaultValue !== undefined) {
    607          return Services.prefs.getCharPref(name, defaultValue);
    608        }
    609        return Services.prefs.getCharPref(name);
    610      case "COMPLEX":
    611        return Services.prefs.getComplexValue(name, iid);
    612      case "STRING":
    613        if (defaultValue !== undefined) {
    614          return Services.prefs.getStringPref(name, defaultValue);
    615        }
    616        return Services.prefs.getStringPref(name);
    617    }
    618    throw new Error(
    619      `Unexpected preference type: ${type} for preference ${name}`
    620    );
    621  }
    622 
    623  getBaselinePrefs() {
    624    this._basePrefs = this._getAllPreferences();
    625  }
    626 
    627  _comparePrefs(base, target, ignorePrefs, partialMatches) {
    628    let failures = [];
    629    for (const [key, value] of base) {
    630      if (ignorePrefs.includes(key)) {
    631        continue;
    632      }
    633      let partialFind = false;
    634      partialMatches.forEach(pm => {
    635        if (key.startsWith(pm)) {
    636          partialFind = true;
    637        }
    638      });
    639      if (partialFind) {
    640        continue;
    641      }
    642 
    643      if (value === target.get(key)) {
    644        continue;
    645      }
    646      if (!failures.includes(key)) {
    647        failures.push(key);
    648      }
    649    }
    650    return failures;
    651  }
    652 
    653  comparePrefsToBaseline(ignorePrefs) {
    654    let newPrefs = this._getAllPreferences();
    655 
    656    // find all items in ignorePrefs that end in *, add to partialMatch
    657    let partialMatch = [];
    658    if (ignorePrefs === undefined) {
    659      ignorePrefs = [];
    660    }
    661    ignorePrefs.forEach(pref => {
    662      if (pref.endsWith("*")) {
    663        partialMatch.push(pref.split("*")[0]);
    664      }
    665    });
    666 
    667    // find all new prefs different than old
    668    let rv1 = this._comparePrefs(
    669      newPrefs,
    670      this._basePrefs,
    671      ignorePrefs,
    672      partialMatch
    673    );
    674 
    675    // find all old prefs different than new (in case we delete)
    676    let rv2 = this._comparePrefs(
    677      this._basePrefs,
    678      newPrefs,
    679      ignorePrefs,
    680      partialMatch
    681    );
    682 
    683    let failures = [...new Set([...rv1, ...rv2])];
    684 
    685    // reset failures
    686    failures.forEach(f => {
    687      if (this._basePrefs.get(f)) {
    688        this._setPref(
    689          f,
    690          PREF_TYPES[Services.prefs.getPrefType(f)],
    691          this._basePrefs.get(f)
    692        );
    693      } else {
    694        Services.prefs.clearUserPref(f);
    695      }
    696    });
    697 
    698    if (failures.length) {
    699      // Because we can't reset prefs on the default branch, reset our baseline.
    700      this.getBaselinePrefs();
    701    }
    702 
    703    if (ignorePrefs.length > 1) {
    704      return failures;
    705    }
    706    return [];
    707  }
    708 
    709  _getAllPreferences() {
    710    let names = new Map();
    711    for (let prefName of Services.prefs.getChildList("")) {
    712      let prefType = PREF_TYPES[Services.prefs.getPrefType(prefName)];
    713      let prefValue = this._getPref(prefName, prefType);
    714      names.set(prefName, prefValue);
    715    }
    716    return names;
    717  }
    718 
    719  _toggleMuteAudio(aMuted) {
    720    let browser = this.browsingContext.top.embedderElement;
    721    if (aMuted) {
    722      browser.mute();
    723    } else {
    724      browser.unmute();
    725    }
    726  }
    727 
    728  _permOp(perm) {
    729    switch (perm.op) {
    730      case "add":
    731        Services.perms.addFromPrincipal(
    732          perm.principal,
    733          perm.type,
    734          perm.permission,
    735          perm.expireType,
    736          perm.expireTime
    737        );
    738        break;
    739      case "remove":
    740        Services.perms.removeFromPrincipal(perm.principal, perm.type);
    741        break;
    742      default:
    743        throw new Error(`Unexpected permission op: ${perm.op}`);
    744    }
    745  }
    746 
    747  pushPermissions(inPermissions) {
    748    let pendingPermissions = [];
    749    let cleanupPermissions = [];
    750 
    751    for (let permission of inPermissions) {
    752      let { principal } = permission;
    753      if (principal.isSystemPrincipal) {
    754        continue;
    755      }
    756 
    757      let originalValue = Services.perms.testPermissionFromPrincipal(
    758        principal,
    759        permission.type
    760      );
    761 
    762      let perm = permission.allow;
    763      if (typeof perm === "boolean") {
    764        perm = Ci.nsIPermissionManager[perm ? "ALLOW_ACTION" : "DENY_ACTION"];
    765      }
    766 
    767      if (permission.remove) {
    768        perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
    769      }
    770 
    771      if (originalValue == perm) {
    772        continue;
    773      }
    774 
    775      let todo = {
    776        op: "add",
    777        type: permission.type,
    778        permission: perm,
    779        value: perm,
    780        principal,
    781        expireType:
    782          typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER
    783        expireTime:
    784          typeof permission.expireTime === "number" ? permission.expireTime : 0,
    785      };
    786 
    787      var cleanupTodo = Object.assign({}, todo);
    788 
    789      if (permission.remove) {
    790        todo.op = "remove";
    791      }
    792 
    793      pendingPermissions.push(todo);
    794 
    795      if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
    796        cleanupTodo.op = "remove";
    797      } else {
    798        cleanupTodo.value = originalValue;
    799        cleanupTodo.permission = originalValue;
    800      }
    801      cleanupPermissions.push(cleanupTodo);
    802    }
    803 
    804    permissionUndoStack.push(cleanupPermissions);
    805 
    806    for (let perm of pendingPermissions) {
    807      this._permOp(perm);
    808    }
    809  }
    810 
    811  popPermissions() {
    812    if (permissionUndoStack.length) {
    813      for (let perm of permissionUndoStack.pop()) {
    814        this._permOp(perm);
    815      }
    816    }
    817  }
    818 
    819  flushPermissions() {
    820    while (permissionUndoStack.length) {
    821      this.popPermissions();
    822    }
    823  }
    824 
    825  _spawnChrome(task, args, caller, imports) {
    826    let sb = new lazy.SpecialPowersSandbox(
    827      null,
    828      data => {
    829        this.sendAsyncMessage("Assert", data);
    830      },
    831      { imports }
    832    );
    833 
    834    // If more variables are made available, don't forget to update
    835    // tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js.
    836    for (let [global, prop] of Object.entries({
    837      windowGlobalParent: "manager",
    838      browsingContext: "browsingContext",
    839    })) {
    840      Object.defineProperty(sb.sandbox, global, {
    841        get: () => {
    842          return this[prop];
    843        },
    844        enumerable: true,
    845      });
    846    }
    847 
    848    return sb.execute(task, args, caller);
    849  }
    850 
    851  /**
    852   * messageManager callback function
    853   * This will get requests from our API in the window and process them in chrome for it
    854   */
    855  // eslint-disable-next-line complexity
    856  async receiveMessage(aMessage) {
    857    let startTime = ChromeUtils.now();
    858    // Try block so we can use a finally statement to add a profiler marker
    859    // despite all the return statements.
    860    try {
    861      // We explicitly return values in the below code so that this function
    862      // doesn't trigger a flurry of warnings about "does not always return
    863      // a value".
    864      switch (aMessage.name) {
    865        case "SPToggleMuteAudio":
    866          return this._toggleMuteAudio(aMessage.data.mute);
    867 
    868        case "Ping":
    869          return undefined;
    870 
    871        case "SpecialPowers.Quit":
    872          if (
    873            !AppConstants.RELEASE_OR_BETA &&
    874            !AppConstants.DEBUG &&
    875            !AppConstants.MOZ_CODE_COVERAGE &&
    876            !AppConstants.ASAN &&
    877            !AppConstants.TSAN
    878          ) {
    879            if (Services.profiler.IsActive()) {
    880              let filename = Services.env.get("MOZ_PROFILER_SHUTDOWN");
    881              if (filename) {
    882                await Services.profiler.dumpProfileToFileAsync(filename);
    883                await Services.profiler.StopProfiler();
    884              }
    885            }
    886            Cu.exitIfInAutomation();
    887          } else {
    888            Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
    889          }
    890          return undefined;
    891 
    892        case "EnsureFocus": {
    893          let bc = aMessage.data.browsingContext;
    894          // Send a message to the child telling it to focus the window.
    895          // If the message responds with a browsing context, then
    896          // a child browsing context in a subframe should be focused.
    897          // Iterate until nothing is returned and we get to the most
    898          // deeply nested subframe that should be focused.
    899          do {
    900            let spParent = bc.currentWindowGlobal.getActor("SpecialPowers");
    901            if (spParent) {
    902              bc = await spParent.sendQuery("EnsureFocus", {
    903                blurSubframe: aMessage.data.blurSubframe,
    904              });
    905            }
    906          } while (bc && !aMessage.data.blurSubframe);
    907          return undefined;
    908        }
    909 
    910        case "SpecialPowers.Focus":
    911          if (this.manager.rootFrameLoader) {
    912            this.manager.rootFrameLoader.ownerElement.focus();
    913          }
    914          return undefined;
    915 
    916        case "SpecialPowers.CreateFiles":
    917          return (async () => {
    918            let filePaths = [];
    919            if (!this._createdFiles) {
    920              this._createdFiles = [];
    921            }
    922            let createdFiles = this._createdFiles;
    923 
    924            let promises = [];
    925            aMessage.data.forEach(function (request) {
    926              const filePerms = 0o666;
    927              let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
    928              if (request.name) {
    929                testFile.appendRelativePath(request.name);
    930              } else {
    931                testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms);
    932              }
    933              let outStream = Cc[
    934                "@mozilla.org/network/file-output-stream;1"
    935              ].createInstance(Ci.nsIFileOutputStream);
    936              outStream.init(
    937                testFile,
    938                0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE
    939                filePerms,
    940                0
    941              );
    942              if (request.data) {
    943                outStream.write(request.data, request.data.length);
    944              }
    945              outStream.close();
    946              promises.push(
    947                File.createFromFileName(testFile.path, request.options).then(
    948                  function (file) {
    949                    filePaths.push(file);
    950                  }
    951                )
    952              );
    953              createdFiles.push(testFile);
    954            });
    955 
    956            await Promise.all(promises);
    957            return filePaths;
    958          })().catch(e => {
    959            console.error(e);
    960            return Promise.reject(String(e));
    961          });
    962 
    963        case "SpecialPowers.RemoveFiles":
    964          if (this._createdFiles) {
    965            this._createdFiles.forEach(function (testFile) {
    966              try {
    967                testFile.remove(false);
    968              } catch (e) {}
    969            });
    970            this._createdFiles = null;
    971          }
    972          return undefined;
    973 
    974        case "Wakeup":
    975          return undefined;
    976 
    977        case "EvictAllDocumentViewers":
    978          this.browsingContext.top.sessionHistory.evictAllDocumentViewers();
    979          return undefined;
    980 
    981        case "getBaselinePrefs":
    982          return this.getBaselinePrefs();
    983 
    984        case "comparePrefsToBaseline":
    985          return this.comparePrefsToBaseline(aMessage.data);
    986 
    987        case "PushPrefEnv":
    988          return this.pushPrefEnv(aMessage.data);
    989 
    990        case "PopPrefEnv":
    991          return this.popPrefEnv();
    992 
    993        case "FlushPrefEnv":
    994          return this.flushPrefEnv();
    995 
    996        case "PushPermissions":
    997          return this.pushPermissions(aMessage.data);
    998 
    999        case "PopPermissions":
   1000          return this.popPermissions();
   1001 
   1002        case "FlushPermissions":
   1003          return this.flushPermissions();
   1004 
   1005        case "SPPrefService": {
   1006          let prefs = Services.prefs;
   1007          let prefType = aMessage.json.prefType.toUpperCase();
   1008          let { prefName, prefValue, iid, defaultValue } = aMessage.json;
   1009 
   1010          if (aMessage.json.op == "get") {
   1011            if (!prefName || !prefType) {
   1012              throw new SpecialPowersError(
   1013                "Invalid parameters for get in SPPrefService"
   1014              );
   1015            }
   1016 
   1017            // return null if the pref doesn't exist
   1018            if (
   1019              defaultValue === undefined &&
   1020              prefs.getPrefType(prefName) == prefs.PREF_INVALID
   1021            ) {
   1022              return null;
   1023            }
   1024            return this._getPref(prefName, prefType, defaultValue, iid);
   1025          } else if (aMessage.json.op == "set") {
   1026            if (!prefName || !prefType || prefValue === undefined) {
   1027              throw new SpecialPowersError(
   1028                "Invalid parameters for set in SPPrefService"
   1029              );
   1030            }
   1031 
   1032            return this._setPref(prefName, prefType, prefValue, iid);
   1033          } else if (aMessage.json.op == "clear") {
   1034            if (!prefName) {
   1035              throw new SpecialPowersError(
   1036                "Invalid parameters for clear in SPPrefService"
   1037              );
   1038            }
   1039 
   1040            prefs.clearUserPref(prefName);
   1041          } else {
   1042            throw new SpecialPowersError("Invalid operation for SPPrefService");
   1043          }
   1044 
   1045          return undefined; // See comment at the beginning of this function.
   1046        }
   1047 
   1048        case "SPProcessCrashService": {
   1049          switch (aMessage.json.op) {
   1050            case "register-observer":
   1051              this._addProcessCrashObservers();
   1052              break;
   1053            case "unregister-observer":
   1054              this._removeProcessCrashObservers();
   1055              break;
   1056            case "delete-crash-dump-files":
   1057              return this._deleteCrashDumpFiles(aMessage.json.filenames);
   1058            case "find-crash-dump-files":
   1059              return this._findCrashDumpFiles(
   1060                aMessage.json.crashDumpFilesToIgnore
   1061              );
   1062            case "delete-pending-crash-dump-files":
   1063              return this._deletePendingCrashDumpFiles();
   1064            default:
   1065              throw new SpecialPowersError(
   1066                "Invalid operation for SPProcessCrashService"
   1067              );
   1068          }
   1069          return undefined; // See comment at the beginning of this function.
   1070        }
   1071 
   1072        case "SPProcessCrashManagerWait": {
   1073          let promises = aMessage.json.crashIds.map(crashId => {
   1074            return Services.crashmanager.ensureCrashIsPresent(crashId);
   1075          });
   1076          return Promise.all(promises);
   1077        }
   1078 
   1079        case "SPPermissionManager": {
   1080          let msg = aMessage.data;
   1081          switch (msg.op) {
   1082            case "add":
   1083            case "remove":
   1084              this._permOp(msg);
   1085              break;
   1086            case "has": {
   1087              let hasPerm = Services.perms.testPermissionFromPrincipal(
   1088                msg.principal,
   1089                msg.type
   1090              );
   1091              return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
   1092            }
   1093            case "test": {
   1094              let testPerm = Services.perms.testPermissionFromPrincipal(
   1095                msg.principal,
   1096                msg.type
   1097              );
   1098              return testPerm == msg.value;
   1099            }
   1100            default:
   1101              throw new SpecialPowersError(
   1102                "Invalid operation for SPPermissionManager"
   1103              );
   1104          }
   1105          return undefined; // See comment at the beginning of this function.
   1106        }
   1107 
   1108        case "SPObserverService": {
   1109          let topic = aMessage.json.observerTopic;
   1110          switch (aMessage.json.op) {
   1111            case "notify": {
   1112              let data = aMessage.json.observerData;
   1113              Services.obs.notifyObservers(null, topic, data);
   1114              break;
   1115            }
   1116            case "add":
   1117              this._registerObservers._add(topic);
   1118              break;
   1119            default:
   1120              throw new SpecialPowersError(
   1121                "Invalid operation for SPObserverervice"
   1122              );
   1123          }
   1124          return undefined; // See comment at the beginning of this function.
   1125        }
   1126 
   1127        case "SPLoadChromeScript": {
   1128          let id = aMessage.json.id;
   1129          let scriptName;
   1130 
   1131          let jsScript = aMessage.json.function.body;
   1132          if (aMessage.json.url) {
   1133            scriptName = aMessage.json.url;
   1134          } else if (aMessage.json.function) {
   1135            scriptName =
   1136              aMessage.json.function.name ||
   1137              "<loadChromeScript anonymous function>";
   1138          } else {
   1139            throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
   1140          }
   1141 
   1142          // Setup a chrome sandbox that has access to sendAsyncMessage
   1143          // and {add,remove}MessageListener in order to communicate with
   1144          // the mochitest.
   1145          let sb = new lazy.SpecialPowersSandbox(
   1146            scriptName,
   1147            data => {
   1148              this.sendAsyncMessage("Assert", data);
   1149            },
   1150            aMessage.data
   1151          );
   1152 
   1153          Object.assign(sb.sandbox, {
   1154            createWindowlessBrowser,
   1155            sendAsyncMessage: (name, message) => {
   1156              this.sendAsyncMessage("SPChromeScriptMessage", {
   1157                id,
   1158                name,
   1159                message,
   1160              });
   1161            },
   1162            addMessageListener: (name, listener) => {
   1163              this._chromeScriptListeners.push({ id, name, listener });
   1164            },
   1165            removeMessageListener: (name, listener) => {
   1166              let index = this._chromeScriptListeners.findIndex(function (obj) {
   1167                return (
   1168                  obj.id == id && obj.name == name && obj.listener == listener
   1169                );
   1170              });
   1171              if (index >= 0) {
   1172                this._chromeScriptListeners.splice(index, 1);
   1173              }
   1174            },
   1175            actorParent: this.manager,
   1176            console,
   1177          });
   1178 
   1179          // Evaluate the chrome script
   1180          try {
   1181            Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
   1182          } catch (e) {
   1183            throw new SpecialPowersError(
   1184              "Error while executing chrome script '" +
   1185                scriptName +
   1186                "':\n" +
   1187                e +
   1188                "\n" +
   1189                e.fileName +
   1190                ":" +
   1191                e.lineNumber
   1192            );
   1193          }
   1194          return undefined; // See comment at the beginning of this function.
   1195        }
   1196 
   1197        case "SPChromeScriptMessage": {
   1198          let id = aMessage.json.id;
   1199          let name = aMessage.json.name;
   1200          let message = aMessage.json.message;
   1201          let result;
   1202          for (let listener of this._chromeScriptListeners) {
   1203            if (listener.name == name && listener.id == id) {
   1204              result = listener.listener(message);
   1205            }
   1206          }
   1207          return result;
   1208        }
   1209 
   1210        case "SPCleanUpSTSData": {
   1211          let origin = aMessage.data.origin;
   1212          let uri = Services.io.newURI(origin);
   1213          let sss = Cc["@mozilla.org/ssservice;1"].getService(
   1214            Ci.nsISiteSecurityService
   1215          );
   1216          sss.resetState(uri);
   1217          return undefined;
   1218        }
   1219 
   1220        case "SPRequestDumpCoverageCounters": {
   1221          return lazy.PerTestCoverageUtils.afterTest();
   1222        }
   1223 
   1224        case "SPRequestResetCoverageCounters": {
   1225          return lazy.PerTestCoverageUtils.beforeTest();
   1226        }
   1227 
   1228        case "SPCheckServiceWorkers": {
   1229          let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
   1230            Ci.nsIServiceWorkerManager
   1231          );
   1232          let regs = swm.getAllRegistrations();
   1233 
   1234          // XXX This code is shared with specialpowers.js.
   1235          let workers = new Array(regs.length);
   1236          for (let i = 0; i < regs.length; ++i) {
   1237            let { scope, scriptSpec } = regs.queryElementAt(
   1238              i,
   1239              Ci.nsIServiceWorkerRegistrationInfo
   1240            );
   1241            workers[i] = { scope, scriptSpec };
   1242          }
   1243          return { workers };
   1244        }
   1245 
   1246        case "SPLoadExtension": {
   1247          let id = aMessage.data.id;
   1248          let ext = aMessage.data.ext;
   1249          if (AppConstants.MOZ_GECKOVIEW) {
   1250            // Some extension APIs are partially implemented in Java, and the
   1251            // interface between the JS and Java side (GeckoViewWebExtension)
   1252            // expects extensions to be registered with the AddonManager.
   1253            //
   1254            // For simplicity, default to using an Addon Manager (if not null).
   1255            if (ext.useAddonManager === undefined) {
   1256              ext.useAddonManager = "geckoview-only";
   1257            }
   1258          }
   1259          // delayedStartup is only supported in xpcshell
   1260          if (ext.delayedStartup !== undefined) {
   1261            throw new Error(
   1262              `delayedStartup is only supported in xpcshell, use "useAddonManager".`
   1263            );
   1264          }
   1265 
   1266          let extension = lazy.ExtensionTestCommon.generate(ext);
   1267 
   1268          let resultListener = (...args) => {
   1269            this.sendAsyncMessage("SPExtensionMessage", {
   1270              id,
   1271              type: "testResult",
   1272              args,
   1273            });
   1274          };
   1275 
   1276          let messageListener = (...args) => {
   1277            args.shift();
   1278            this.sendAsyncMessage("SPExtensionMessage", {
   1279              id,
   1280              type: "testMessage",
   1281              args,
   1282            });
   1283          };
   1284 
   1285          // Register pass/fail handlers.
   1286          extension.on("test-result", resultListener);
   1287          extension.on("test-eq", resultListener);
   1288          extension.on("test-log", resultListener);
   1289          extension.on("test-done", resultListener);
   1290          // Web Platform Test subtest started and finished events.
   1291          extension.on("test-task-start", resultListener);
   1292          extension.on("test-task-done", resultListener);
   1293 
   1294          extension.on("test-message", messageListener);
   1295 
   1296          this._extensions.set(id, extension);
   1297          return undefined;
   1298        }
   1299 
   1300        case "SPStartupExtension": {
   1301          let id = aMessage.data.id;
   1302          // This is either an Extension, or (if useAddonManager is set) a MockExtension.
   1303          let extension = this._extensions.get(id);
   1304          extension.on("startup", (eventName, ext) => {
   1305            if (AppConstants.platform === "android") {
   1306              // We need a way to notify the embedding layer that a new extension
   1307              // has been installed, so that the java layer can be updated too.
   1308              Services.obs.notifyObservers(null, "testing-installed-addon", id);
   1309            }
   1310            // ext is always the "real" Extension object, even when "extension"
   1311            // is a MockExtension.
   1312            this.sendAsyncMessage("SPExtensionMessage", {
   1313              id,
   1314              type: "extensionSetId",
   1315              args: [ext.id, ext.uuid],
   1316            });
   1317          });
   1318 
   1319          // Make sure the extension passes the packaging checks when
   1320          // they're run on a bare archive rather than a running instance,
   1321          // as the add-on manager runs them.
   1322          let extensionData = new lazy.ExtensionData(extension.rootURI);
   1323          return extensionData
   1324            .loadManifest()
   1325            .then(
   1326              () => {
   1327                return extensionData.initAllLocales().then(() => {
   1328                  if (extensionData.errors.length) {
   1329                    return Promise.reject(
   1330                      "Extension contains packaging errors"
   1331                    );
   1332                  }
   1333                  return undefined;
   1334                });
   1335              },
   1336              () => {
   1337                // loadManifest() will throw if we're loading an embedded
   1338                // extension, so don't worry about locale errors in that
   1339                // case.
   1340              }
   1341            )
   1342            .then(async () => {
   1343              // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
   1344              // in that case we have an ID here and we need to set the override.
   1345              if (extension.id) {
   1346                await lazy.ExtensionTestCommon.setIncognitoOverride(extension);
   1347              }
   1348              return extension.startup().then(
   1349                () => {},
   1350                e => {
   1351                  dump(`Extension startup failed: ${e}\n${e.stack}`);
   1352                  throw e;
   1353                }
   1354              );
   1355            });
   1356        }
   1357 
   1358        case "SPExtensionMessage": {
   1359          let id = aMessage.data.id;
   1360          let extension = this._extensions.get(id);
   1361          extension.testMessage(...aMessage.data.args);
   1362          return undefined;
   1363        }
   1364 
   1365        case "SPExtensionGrantActiveTab": {
   1366          let { id, tabId } = aMessage.data;
   1367          let { tabManager } = this._extensions.get(id);
   1368          tabManager.addActiveTabPermission(tabManager.get(tabId).nativeTab);
   1369          return undefined;
   1370        }
   1371 
   1372        case "SPUnloadExtension": {
   1373          let id = aMessage.data.id;
   1374          let extension = this._extensions.get(id);
   1375          this._extensions.delete(id);
   1376          return lazy.ExtensionTestCommon.unloadTestExtension(extension);
   1377        }
   1378 
   1379        case "SPExtensionTerminateBackground": {
   1380          let id = aMessage.data.id;
   1381          let args = aMessage.data.args;
   1382          let extension = this._extensions.get(id);
   1383          return extension.terminateBackground(...args);
   1384        }
   1385 
   1386        case "SPExtensionWakeupBackground": {
   1387          let id = aMessage.data.id;
   1388          let extension = this._extensions.get(id);
   1389          return extension.wakeupBackground();
   1390        }
   1391 
   1392        case "SetAsDefaultAssertHandler": {
   1393          defaultAssertHandler = this;
   1394          return undefined;
   1395        }
   1396 
   1397        case "Spawn": {
   1398          // Use a different variable for the profiler marker start time
   1399          // so that a marker isn't added when we return, but instead when
   1400          // our promise resolves.
   1401          let spawnStartTime = startTime;
   1402          startTime = undefined;
   1403          let { browsingContext, task, args, caller, hasHarness, imports } =
   1404            aMessage.data;
   1405 
   1406          let spParent =
   1407            browsingContext.currentWindowGlobal.getActor("SpecialPowers");
   1408 
   1409          let taskId = nextTaskID++;
   1410          if (hasHarness) {
   1411            spParent._taskActors.set(taskId, this);
   1412          }
   1413 
   1414          return spParent
   1415            .sendQuery("Spawn", { task, args, caller, taskId, imports })
   1416            .finally(() => {
   1417              ChromeUtils.addProfilerMarker(
   1418                "SpecialPowers",
   1419                { startTime: spawnStartTime, category: "Test" },
   1420                aMessage.name
   1421              );
   1422              return spParent._taskActors.delete(taskId);
   1423            });
   1424        }
   1425 
   1426        case "SpawnChrome": {
   1427          let { task, args, caller, imports } = aMessage.data;
   1428 
   1429          return this._spawnChrome(task, args, caller, imports);
   1430        }
   1431 
   1432        case "Snapshot": {
   1433          let { browsingContext, rect, background, resetScrollPosition } =
   1434            aMessage.data;
   1435 
   1436          return browsingContext.currentWindowGlobal
   1437            .drawSnapshot(rect, 1.0, background, resetScrollPosition)
   1438            .then(async image => {
   1439              let hiddenFrame = new lazy.HiddenFrame();
   1440              let win = await hiddenFrame.get();
   1441 
   1442              let canvas = win.document.createElement("canvas");
   1443              canvas.width = image.width;
   1444              canvas.height = image.height;
   1445 
   1446              const ctx = canvas.getContext("2d");
   1447              ctx.drawImage(image, 0, 0);
   1448 
   1449              let data = ctx.getImageData(0, 0, image.width, image.height);
   1450              hiddenFrame.destroy();
   1451              return data;
   1452            });
   1453        }
   1454 
   1455        case "SecurityState": {
   1456          let { browsingContext } = aMessage.data;
   1457          return browsingContext.secureBrowserUI.state;
   1458        }
   1459 
   1460        case "ProxiedAssert": {
   1461          let { taskId, data } = aMessage.data;
   1462 
   1463          let actor = this._taskActors.get(taskId) || defaultAssertHandler;
   1464          actor.sendAsyncMessage("Assert", data);
   1465 
   1466          return undefined;
   1467        }
   1468 
   1469        case "SPRemoveAllServiceWorkers": {
   1470          return lazy.ServiceWorkerCleanUp.removeAll();
   1471        }
   1472 
   1473        case "SPRemoveServiceWorkerDataForExampleDomain": {
   1474          return lazy.ServiceWorkerCleanUp.removeFromHost("example.com");
   1475        }
   1476 
   1477        case "SPGenerateMediaControlKeyTestEvent": {
   1478          // eslint-disable-next-line no-undef
   1479          MediaControlService.generateMediaControlKey(aMessage.data.event);
   1480          return undefined;
   1481        }
   1482 
   1483        default:
   1484          throw new SpecialPowersError(
   1485            `Unrecognized Special Powers API: ${aMessage.name}`
   1486          );
   1487      }
   1488      // This should be unreachable. If it ever becomes reachable, ESLint
   1489      // will produce an error about inconsistent return values.
   1490    } finally {
   1491      if (startTime) {
   1492        ChromeUtils.addProfilerMarker(
   1493          "SpecialPowers",
   1494          { startTime, category: "Test" },
   1495          aMessage.name
   1496        );
   1497      }
   1498    }
   1499  }
   1500 }