tor-browser

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

GeckoViewPrompter.sys.mjs (5460B)


      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 { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
      6 
      7 const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter");
      8 
      9 export class GeckoViewPrompter {
     10  constructor(aParent) {
     11    this.id = Services.uuid.generateUUID().toString().slice(1, -1); // Discard surrounding braces
     12 
     13    if (aParent) {
     14      if (Window.isInstance(aParent)) {
     15        this._domWin = aParent;
     16      } else if (aParent.window) {
     17        this._domWin = aParent.window;
     18      } else {
     19        this._domWin =
     20          aParent.embedderElement && aParent.embedderElement.ownerGlobal;
     21      }
     22    }
     23 
     24    if (!this._domWin) {
     25      this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview");
     26    }
     27 
     28    this._innerWindowId =
     29      this._domWin?.browsingContext.currentWindowContext.innerWindowId;
     30  }
     31 
     32  get domWin() {
     33    return this._domWin;
     34  }
     35 
     36  get prompterActor() {
     37    const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter");
     38    return actor;
     39  }
     40 
     41  _changeModalState(aEntering) {
     42    if (!this._domWin) {
     43      // Allow not having a DOM window.
     44      return true;
     45    }
     46    // Accessing the document object can throw if this window no longer exists. See bug 789888.
     47    try {
     48      const winUtils = this._domWin.windowUtils;
     49      if (!aEntering) {
     50        winUtils.leaveModalState();
     51      }
     52 
     53      const event = this._domWin.document.createEvent("Events");
     54      event.initEvent(
     55        aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed",
     56        true,
     57        true
     58      );
     59      winUtils.dispatchEventToChromeOnly(this._domWin, event);
     60 
     61      if (aEntering) {
     62        winUtils.enterModalState();
     63      }
     64      return true;
     65    } catch (ex) {
     66      console.error("Failed to change modal state:", ex);
     67    }
     68    return false;
     69  }
     70 
     71  _dismissUi() {
     72    this.prompterActor?.dismissPrompt(this);
     73  }
     74 
     75  accept(aInputText = this.inputText) {
     76    if (this.callback) {
     77      let acceptMsg = {};
     78      switch (this.message.type) {
     79        case "alert":
     80          acceptMsg = null;
     81          break;
     82        case "button":
     83          acceptMsg.button = 0;
     84          break;
     85        case "text":
     86          acceptMsg.text = aInputText;
     87          break;
     88        default:
     89          acceptMsg = null;
     90          break;
     91      }
     92      this.callback(acceptMsg);
     93      // Notify the UI that this prompt should be hidden.
     94      this._dismissUi();
     95    }
     96  }
     97 
     98  dismiss() {
     99    this.callback(null);
    100    // Notify the UI that this prompt should be hidden.
    101    this._dismissUi();
    102  }
    103 
    104  getPromptType() {
    105    switch (this.message.type) {
    106      case "alert":
    107        return this.message.checkValue ? "alertCheck" : "alert";
    108      case "button":
    109        return this.message.checkValue ? "confirmCheck" : "confirm";
    110      case "text":
    111        return this.message.checkValue ? "promptCheck" : "prompt";
    112      default:
    113        return this.message.type;
    114    }
    115  }
    116 
    117  getPromptText() {
    118    return this.message.msg;
    119  }
    120 
    121  getInputText() {
    122    return this.inputText;
    123  }
    124 
    125  setInputText(aInput) {
    126    this.inputText = aInput;
    127  }
    128 
    129  /**
    130   * Shows a native prompt, and then spins the event loop for this thread while we wait
    131   * for a response
    132   */
    133  showPrompt(aMsg) {
    134    let result = undefined;
    135    if (!this._domWin || !this._changeModalState(/* aEntering */ true)) {
    136      return result;
    137    }
    138    try {
    139      this.asyncShowPrompt(aMsg, res => (result = res));
    140 
    141      // Spin this thread while we wait for a result
    142      Services.tm.spinEventLoopUntil(
    143        "GeckoViewPrompter.sys.mjs:showPrompt",
    144        () => this._domWin.closed || result !== undefined
    145      );
    146    } finally {
    147      this._changeModalState(/* aEntering */ false);
    148    }
    149    return result;
    150  }
    151 
    152  checkInnerWindow() {
    153    // Checks that the innerWindow where this prompt was created still matches
    154    // the current innerWindow.
    155    // This checks will fail if the page navigates away, making this prompt
    156    // obsolete.
    157    return (
    158      this._innerWindowId ===
    159      this._domWin.browsingContext.currentWindowContext.innerWindowId
    160    );
    161  }
    162 
    163  asyncShowPromptPromise(aMsg) {
    164    return new Promise(resolve => {
    165      this.asyncShowPrompt(aMsg, resolve);
    166    });
    167  }
    168 
    169  async asyncShowPrompt(aMsg, aCallback) {
    170    this.message = aMsg;
    171    this.inputText = aMsg.value;
    172    this.callback = aCallback;
    173 
    174    aMsg.id = this.id;
    175 
    176    let response = null;
    177    try {
    178      if (this.checkInnerWindow()) {
    179        response = await this.prompterActor.prompt(this, aMsg);
    180      }
    181    } catch (error) {
    182      // Nothing we can do really, we will treat this as a dismiss.
    183      warn`Error while prompting: ${error}`;
    184    }
    185 
    186    if (!this.checkInnerWindow()) {
    187      // Page has navigated away, let's dismiss the prompt
    188      aCallback(null);
    189    } else {
    190      aCallback(response);
    191    }
    192    // This callback object is tied to the Java garbage collector because
    193    // it is invoked from Java. Manually release the target callback
    194    // here; otherwise we may hold onto resources for too long, because
    195    // we would be relying on both the Java and the JS garbage collectors
    196    // to run.
    197    aMsg = undefined;
    198    aCallback = undefined;
    199  }
    200 
    201  update(aMsg) {
    202    this.message = aMsg;
    203    aMsg.id = this.id;
    204    this.prompterActor?.updatePrompt(aMsg);
    205  }
    206 }