tor-browser

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

BrowserTestUtilsChild.sys.mjs (11105B)


      1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 const lazy = {};
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
      9 });
     10 
     11 class BrowserTestUtilsChildObserver {
     12  constructor() {
     13    this.currentObserverStatus = "";
     14    this.observerItems = [];
     15  }
     16 
     17  startObservingTopics(aTopics) {
     18    for (let topic of aTopics) {
     19      Services.obs.addObserver(this, topic);
     20      this.observerItems.push({ topic });
     21    }
     22  }
     23 
     24  stopObservingTopics(aTopics) {
     25    if (aTopics) {
     26      for (let topic of aTopics) {
     27        let index = this.observerItems.findIndex(item => item.topic == topic);
     28        if (index >= 0) {
     29          Services.obs.removeObserver(this, topic);
     30          this.observerItems.splice(index, 1);
     31        }
     32      }
     33    } else {
     34      for (let topic of this.observerItems) {
     35        Services.obs.removeObserver(this, topic);
     36      }
     37      this.observerItems = [];
     38    }
     39 
     40    if (this.currentObserverStatus) {
     41      let error = new Error(this.currentObserverStatus);
     42      this.currentObserverStatus = "";
     43      throw error;
     44    }
     45  }
     46 
     47  observeTopic(topic, count, filterFn, callbackResolver) {
     48    // If the topic is in the list already, assume that it came from a
     49    // startObservingTopics call. If it isn't in the list already, assume
     50    // that it isn't within a start/stop set and the observer has to be
     51    // removed afterwards.
     52    let removeObserver = false;
     53    let index = this.observerItems.findIndex(item => item.topic == topic);
     54    if (index == -1) {
     55      removeObserver = true;
     56      this.startObservingTopics([topic]);
     57    }
     58 
     59    for (let item of this.observerItems) {
     60      if (item.topic == topic) {
     61        item.count = count || 1;
     62        item.filterFn = filterFn;
     63        item.promiseResolver = () => {
     64          if (removeObserver) {
     65            this.stopObservingTopics([topic]);
     66          }
     67          callbackResolver();
     68        };
     69        break;
     70      }
     71    }
     72  }
     73 
     74  observe(aSubject, aTopic, aData) {
     75    for (let item of this.observerItems) {
     76      if (item.topic != aTopic) {
     77        continue;
     78      }
     79      if (item.filterFn && !item.filterFn(aSubject, aTopic, aData)) {
     80        break;
     81      }
     82 
     83      if (--item.count >= 0) {
     84        if (item.count == 0 && item.promiseResolver) {
     85          item.promiseResolver();
     86        }
     87        return;
     88      }
     89    }
     90 
     91    // Otherwise, if the observer doesn't match, fail.
     92    console.log(
     93      "Failed: Observer topic " + aTopic + " not expected in content process"
     94    );
     95    this.currentObserverStatus +=
     96      "Topic " + aTopic + " not expected in content process\n";
     97  }
     98 }
     99 
    100 BrowserTestUtilsChildObserver.prototype.QueryInterface = ChromeUtils.generateQI(
    101  ["nsIObserver", "nsISupportsWeakReference"]
    102 );
    103 
    104 export class BrowserTestUtilsChild extends JSWindowActorChild {
    105  actorCreated() {
    106    this._EventUtils = null;
    107  }
    108 
    109  get EventUtils() {
    110    if (!this._EventUtils) {
    111      // Set up a dummy environment so that EventUtils works. We need to be careful to
    112      // pass a window object into each EventUtils method we call rather than having
    113      // it rely on the |window| global.
    114      let win = this.contentWindow;
    115      let EventUtils = {
    116        get KeyboardEvent() {
    117          return win.KeyboardEvent;
    118        },
    119        // EventUtils' `sendChar` function relies on the navigator to synthetize events.
    120        get navigator() {
    121          return win.navigator;
    122        },
    123      };
    124 
    125      EventUtils.window = {};
    126      EventUtils.parent = EventUtils.window;
    127      EventUtils._EU_Ci = Ci;
    128      EventUtils._EU_Cc = Cc;
    129 
    130      Services.scriptloader.loadSubScript(
    131        "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
    132        EventUtils
    133      );
    134 
    135      this._EventUtils = EventUtils;
    136    }
    137 
    138    return this._EventUtils;
    139  }
    140 
    141  receiveMessage(aMessage) {
    142    switch (aMessage.name) {
    143      case "Test:SynthesizeMouse": {
    144        return this.synthesizeMouse(aMessage.data, this.contentWindow);
    145      }
    146 
    147      case "Test:SynthesizeTouch": {
    148        return this.synthesizeTouch(aMessage.data, this.contentWindow);
    149      }
    150 
    151      case "Test:SendChar": {
    152        return this.EventUtils.sendChar(aMessage.data.char, this.contentWindow);
    153      }
    154 
    155      case "Test:SynthesizeKey":
    156        this.EventUtils.synthesizeKey(
    157          aMessage.data.key,
    158          aMessage.data.event || {},
    159          this.contentWindow
    160        );
    161        break;
    162 
    163      case "Test:SynthesizeComposition": {
    164        return this.EventUtils.synthesizeComposition(
    165          aMessage.data.event,
    166          this.contentWindow
    167        );
    168      }
    169 
    170      case "Test:SynthesizeCompositionChange":
    171        this.EventUtils.synthesizeCompositionChange(
    172          aMessage.data.event,
    173          this.contentWindow
    174        );
    175        break;
    176 
    177      case "BrowserTestUtils:StartObservingTopics": {
    178        this.observer = new BrowserTestUtilsChildObserver();
    179        this.observer.startObservingTopics(aMessage.data.topics);
    180        break;
    181      }
    182 
    183      case "BrowserTestUtils:StopObservingTopics": {
    184        if (this.observer) {
    185          this.observer.stopObservingTopics(aMessage.data.topics);
    186          this.observer = null;
    187        }
    188        break;
    189      }
    190 
    191      case "BrowserTestUtils:ObserveTopic": {
    192        return new Promise(resolve => {
    193          let filterFn;
    194          if (aMessage.data.filterFunctionSource) {
    195            // eslint-disable-next-line mozilla/reject-globalThis-modification
    196            let sb = Cu.Sandbox(globalThis, {
    197              sandboxPrototype: {
    198                __proto__: globalThis,
    199                content: this.contentWindow,
    200              },
    201            });
    202            filterFn = Cu.evalInSandbox(
    203              `(() => (${aMessage.data.filterFunctionSource}))()`,
    204              sb
    205            );
    206          }
    207 
    208          let observer = this.observer || new BrowserTestUtilsChildObserver();
    209          observer.observeTopic(
    210            aMessage.data.topic,
    211            aMessage.data.count,
    212            filterFn,
    213            resolve
    214          );
    215        });
    216      }
    217 
    218      case "BrowserTestUtils:CrashFrame": {
    219        // This is to intentionally crash the frame.
    220        // We crash by using js-ctypes. The crash
    221        // should happen immediately
    222        // upon loading this frame script.
    223 
    224        const { ctypes } = ChromeUtils.importESModule(
    225          "resource://gre/modules/ctypes.sys.mjs"
    226        );
    227 
    228        let dies = function () {
    229          dump("\nEt tu, Brute?\n");
    230          ChromeUtils.privateNoteIntentionalCrash();
    231 
    232          try {
    233            // Annotate test failure to allow callers to separate intentional
    234            // crashes from unintentional crashes.
    235            Services.appinfo.annotateCrashReport("TestKey", "CrashFrame");
    236          } catch (e) {
    237            dump(`Failed to annotate crash in CrashFrame: ${e}\n`);
    238          }
    239 
    240          switch (aMessage.data.crashType) {
    241            case "CRASH_OOM": {
    242              let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(
    243                Ci.nsIDebug2
    244              );
    245              debug.crashWithOOM();
    246              break;
    247            }
    248            case "CRASH_SYSCALL": {
    249              if (Services.appinfo.OS == "Linux") {
    250                let libc = ctypes.open("libc.so.6");
    251                let chroot = libc.declare(
    252                  "chroot",
    253                  ctypes.default_abi,
    254                  ctypes.int,
    255                  ctypes.char.ptr
    256                );
    257                chroot("/");
    258              }
    259              break;
    260            }
    261            case "CRASH_INVALID_POINTER_DEREF": // Fallthrough
    262            default: {
    263              // Dereference a bad pointer.
    264              let zero = new ctypes.intptr_t(8);
    265              let badptr = ctypes.cast(
    266                zero,
    267                ctypes.PointerType(ctypes.int32_t)
    268              );
    269              badptr.contents;
    270            }
    271          }
    272        };
    273 
    274        if (aMessage.data.asyncCrash) {
    275          let { setTimeout } = ChromeUtils.importESModule(
    276            "resource://gre/modules/Timer.sys.mjs"
    277          );
    278          // Get out of the stack.
    279          setTimeout(dies, 0);
    280        } else {
    281          dies();
    282        }
    283      }
    284    }
    285 
    286    return undefined;
    287  }
    288 
    289  handleEvent(aEvent) {
    290    switch (aEvent.type) {
    291      case "DOMContentLoaded":
    292      case "load": {
    293        this.sendAsyncMessage(aEvent.type, {
    294          internalURL: aEvent.target.documentURI,
    295          visibleURL: aEvent.target.location
    296            ? aEvent.target.location.href
    297            : null,
    298        });
    299        break;
    300      }
    301    }
    302  }
    303 
    304  #getTarget(data, window) {
    305    let { target, targetFn } = data;
    306    if (typeof target == "string") {
    307      return this.document.querySelector(target);
    308    }
    309    if (typeof targetFn == "string") {
    310      let sb = Cu.Sandbox(window, { sandboxPrototype: window });
    311      return Cu.evalInSandbox(`(${targetFn})()`, sb);
    312    }
    313    return null;
    314  }
    315 
    316  synthesizeMouse(data, window) {
    317    let target = this.#getTarget(data, window);
    318 
    319    let left = data.x;
    320    let top = data.y;
    321    if (target) {
    322      if (target.ownerDocument !== this.document) {
    323        // Account for nodes found in iframes.
    324        let cur = target;
    325        do {
    326          // eslint-disable-next-line mozilla/use-ownerGlobal
    327          let frame = cur.ownerDocument.defaultView.frameElement;
    328          let rect = frame.getBoundingClientRect();
    329 
    330          left += rect.left;
    331          top += rect.top;
    332 
    333          cur = frame;
    334        } while (cur && cur.ownerDocument !== this.document);
    335 
    336        // node must be in this document tree.
    337        if (!cur) {
    338          throw new Error("target must be in the main document tree");
    339        }
    340      }
    341 
    342      let rect = target.getBoundingClientRect();
    343      left += rect.left;
    344      top += rect.top;
    345 
    346      if (data.event.centered) {
    347        left += rect.width / 2;
    348        top += rect.height / 2;
    349      }
    350    }
    351 
    352    let result;
    353 
    354    lazy.E10SUtils.wrapHandlingUserInput(window, data.handlingUserInput, () => {
    355      if (data.event && data.event.wheel) {
    356        this.EventUtils.synthesizeWheelAtPoint(left, top, data.event, window);
    357      } else {
    358        result = this.EventUtils.synthesizeMouseAtPoint(
    359          left,
    360          top,
    361          data.event,
    362          window
    363        );
    364      }
    365    });
    366 
    367    return result;
    368  }
    369 
    370  synthesizeTouch(data, window) {
    371    let target = this.#getTarget(data, window);
    372 
    373    if (target) {
    374      if (target.ownerDocument !== this.document) {
    375        // Account for nodes found in iframes.
    376        let cur = target;
    377        do {
    378          cur = cur.ownerGlobal.frameElement;
    379        } while (cur && cur.ownerDocument !== this.document);
    380 
    381        // node must be in this document tree.
    382        if (!cur) {
    383          throw new Error("target must be in the main document tree");
    384        }
    385      }
    386    }
    387 
    388    return this.EventUtils.synthesizeTouch(
    389      target,
    390      data.x,
    391      data.y,
    392      data.event,
    393      window
    394    );
    395  }
    396 }