tor-browser

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

GeckoViewContent.sys.mjs (19590B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
      6 
      7 const lazy = {};
      8 ChromeUtils.defineESModuleGetters(lazy, {
      9  TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
     10 });
     11 
     12 export class GeckoViewContent extends GeckoViewModule {
     13  onInit() {
     14    this.registerListener([
     15      "GeckoViewContent:ExitFullScreen",
     16      "GeckoView:ClearMatches",
     17      "GeckoView:DisplayMatches",
     18      "GeckoView:FindInPage",
     19      "GeckoView:HasCookieBannerRuleForBrowsingContextTree",
     20      "GeckoView:RestoreState",
     21      "GeckoView:ContainsFormData",
     22      "GeckoView:ScrollBy",
     23      "GeckoView:ScrollTo",
     24      "GeckoView:SetActive",
     25      "GeckoView:SetFocused",
     26      "GeckoView:SetPriorityHint",
     27      "GeckoView:UpdateInitData",
     28      "GeckoView:ZoomToInput",
     29      "GeckoView:IsPdfJs",
     30      "GeckoView:GetWebCompatInfo",
     31      "GeckoView:SendMoreWebCompatInfo",
     32      "GeckoView:GetTorCircuit",
     33      "GeckoView:NewTorCircuit",
     34    ]);
     35  }
     36 
     37  onEnable() {
     38    this.window.addEventListener(
     39      "MozDOMFullscreen:Entered",
     40      this,
     41      /* capture */ true,
     42      /* untrusted */ false
     43    );
     44    this.window.addEventListener(
     45      "MozDOMFullscreen:Exited",
     46      this,
     47      /* capture */ true,
     48      /* untrusted */ false
     49    );
     50    this.window.addEventListener(
     51      "framefocusrequested",
     52      this,
     53      /* capture */ true,
     54      /* untrusted */ false
     55    );
     56 
     57    this.window.addEventListener("DOMWindowClose", this);
     58    this.window.addEventListener("pagetitlechanged", this);
     59    this.window.addEventListener("pageinfo", this);
     60 
     61    this.window.addEventListener("cookiebannerdetected", this);
     62    this.window.addEventListener("cookiebannerhandled", this);
     63 
     64    Services.obs.addObserver(this, "oop-frameloader-crashed");
     65    Services.obs.addObserver(this, "ipc:content-shutdown");
     66  }
     67 
     68  onDisable() {
     69    this.window.removeEventListener(
     70      "MozDOMFullscreen:Entered",
     71      this,
     72      /* capture */ true
     73    );
     74    this.window.removeEventListener(
     75      "MozDOMFullscreen:Exited",
     76      this,
     77      /* capture */ true
     78    );
     79    this.window.removeEventListener(
     80      "framefocusrequested",
     81      this,
     82      /* capture */ true
     83    );
     84 
     85    this.window.removeEventListener("DOMWindowClose", this);
     86    this.window.removeEventListener("pagetitlechanged", this);
     87    this.window.removeEventListener("pageinfo", this);
     88 
     89    this.window.removeEventListener("cookiebannerdetected", this);
     90    this.window.removeEventListener("cookiebannerhandled", this);
     91 
     92    Services.obs.removeObserver(this, "oop-frameloader-crashed");
     93    Services.obs.removeObserver(this, "ipc:content-shutdown");
     94  }
     95 
     96  get actor() {
     97    return this.getActor("GeckoViewContent");
     98  }
     99 
    100  get isPdfJs() {
    101    return (
    102      this.browser.contentPrincipal.spec === "resource://pdf.js/web/viewer.html"
    103    );
    104  }
    105 
    106  // Goes up the browsingContext chain and sends the message every time
    107  // we cross the process boundary so that every process in the chain is
    108  // notified.
    109  sendToAllChildren(aEvent, aData) {
    110    let { browsingContext } = this.actor;
    111 
    112    while (browsingContext) {
    113      if (!browsingContext.currentWindowGlobal) {
    114        break;
    115      }
    116 
    117      const currentPid = browsingContext.currentWindowGlobal.osPid;
    118      const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
    119 
    120      if (currentPid != parentPid) {
    121        const actor =
    122          browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
    123        actor.sendAsyncMessage(aEvent, aData);
    124      }
    125 
    126      browsingContext = browsingContext.parent;
    127    }
    128  }
    129 
    130  #sendEnterDOMFullscreenEvent(aRequestOrigin) {
    131    // Track the actors that are involved in the fullscreen request. And we will
    132    // use them to send the exit message when the fullscreen is exited.
    133    this._fullscreenRequest = { actors: [] };
    134 
    135    let currentBC = aRequestOrigin.browsingContext;
    136    let currentPid = currentBC.currentWindowGlobal.osPid;
    137    let parentBC = currentBC.parent;
    138 
    139    while (parentBC) {
    140      if (!parentBC.currentWindowGlobal) {
    141        break;
    142      }
    143 
    144      const parentPid = parentBC.currentWindowGlobal.osPid;
    145      if (currentPid != parentPid) {
    146        const actor = parentBC.currentWindowGlobal.getActor("GeckoViewContent");
    147        actor.sendAsyncMessage("GeckoView:DOMFullscreenEntered", {
    148          remoteFrameBC: currentBC,
    149        });
    150        this._fullscreenRequest.actors.push(actor);
    151        currentPid = parentPid;
    152      }
    153 
    154      currentBC = parentBC;
    155      parentBC = parentBC.parent;
    156    }
    157 
    158    const actor =
    159      aRequestOrigin.browsingContext.currentWindowGlobal.getActor(
    160        "GeckoViewContent"
    161      );
    162    actor.sendAsyncMessage("GeckoView:DOMFullscreenEntered", {});
    163    this._fullscreenRequest.actors.push(actor);
    164  }
    165 
    166  #sendExitDOMFullScreenEvent() {
    167    if (!this._fullscreenRequest) {
    168      return;
    169    }
    170 
    171    for (const actor of this._fullscreenRequest.actors) {
    172      if (
    173        !actor.hasBeenDestroyed() &&
    174        actor.windowContext &&
    175        !actor.windowContext.isInBFCache
    176      ) {
    177        actor.sendAsyncMessage("GeckoView:DOMFullscreenExited", {});
    178      }
    179    }
    180    delete this._fullscreenRequest;
    181  }
    182 
    183  // Bundle event handler.
    184  onEvent(aEvent, aData, aCallback) {
    185    debug`onEvent: event=${aEvent}, data=${aData}`;
    186 
    187    switch (aEvent) {
    188      case "GeckoViewContent:ExitFullScreen":
    189        this.browser.ownerDocument.exitFullscreen();
    190        break;
    191      case "GeckoView:ClearMatches": {
    192        if (!this.isPdfJs) {
    193          this._clearMatches();
    194        }
    195        break;
    196      }
    197      case "GeckoView:DisplayMatches": {
    198        if (!this.isPdfJs) {
    199          this._displayMatches(aData);
    200        }
    201        break;
    202      }
    203      case "GeckoView:FindInPage": {
    204        if (!this.isPdfJs) {
    205          this._findInPage(aData, aCallback);
    206        }
    207        break;
    208      }
    209      case "GeckoView:ZoomToInput": {
    210        const sendZoomToFocusedInputMessage = function () {
    211          // For ZoomToInput we just need to send the message to the current focused one.
    212          const actor =
    213            Services.focus.focusedContentBrowsingContext.currentWindowGlobal.getActor(
    214              "GeckoViewContent"
    215            );
    216 
    217          actor.sendAsyncMessage(aEvent, aData);
    218        };
    219 
    220        const { force } = aData;
    221        let gotResize = false;
    222        const onResize = () => {
    223          gotResize = true;
    224          if (this.window.windowUtils.isMozAfterPaintPending) {
    225            this.window.addEventListener(
    226              "MozAfterPaint",
    227              () => sendZoomToFocusedInputMessage(),
    228              { capture: true, once: true }
    229            );
    230          } else {
    231            sendZoomToFocusedInputMessage();
    232          }
    233        };
    234 
    235        this.window.addEventListener("resize", onResize, { capture: true });
    236 
    237        // When the keyboard is displayed, we can get one resize event,
    238        // multiple resize events, or none at all. Try to handle all these
    239        // cases by allowing resizing within a set interval, and still zoom to
    240        // input if there is no resize event at the end of the interval.
    241        this.window.setTimeout(() => {
    242          this.window.removeEventListener("resize", onResize, {
    243            capture: true,
    244          });
    245          if (!gotResize && force) {
    246            onResize();
    247          }
    248        }, 500);
    249        break;
    250      }
    251      case "GeckoView:ScrollBy":
    252        // Unclear if that actually works with oop iframes?
    253        this.sendToAllChildren(aEvent, aData);
    254        break;
    255      case "GeckoView:ScrollTo":
    256        // Unclear if that actually works with oop iframes?
    257        this.sendToAllChildren(aEvent, aData);
    258        break;
    259      case "GeckoView:UpdateInitData":
    260        this.sendToAllChildren(aEvent, aData);
    261        break;
    262      case "GeckoView:SetActive":
    263        this.browser.docShellIsActive = !!aData.active;
    264        break;
    265      case "GeckoView:SetFocused":
    266        if (aData.focused) {
    267          this.browser.focus();
    268          this.browser.setAttribute("primary", "true");
    269        } else {
    270          this.browser.removeAttribute("primary");
    271          this.browser.blur();
    272        }
    273        break;
    274      case "GeckoView:SetPriorityHint":
    275        if (this.browser.isRemoteBrowser) {
    276          const remoteTab = this.browser.frameLoader?.remoteTab;
    277          if (remoteTab) {
    278            remoteTab.priorityHint = aData.priorityHint;
    279          }
    280        }
    281        break;
    282      case "GeckoView:RestoreState":
    283        this.actor.restoreState(aData);
    284        break;
    285      case "GeckoView:ContainsFormData":
    286        this._containsFormData(aCallback);
    287        break;
    288      case "GeckoView:GetWebCompatInfo":
    289        this._getWebCompatInfo(aCallback);
    290        break;
    291      case "GeckoView:SendMoreWebCompatInfo":
    292        this._sendMoreWebCompatInfo(aData, aCallback);
    293        break;
    294      case "GeckoView:IsPdfJs":
    295        aCallback.onSuccess(this.isPdfJs);
    296        break;
    297      case "GeckoView:HasCookieBannerRuleForBrowsingContextTree":
    298        this._hasCookieBannerRuleForBrowsingContextTree(aCallback);
    299        break;
    300      case "GeckoView:GetTorCircuits":
    301        this._getTorCircuits(aCallback);
    302        break;
    303      case "GeckoView:NewTorCircuit":
    304        this._newTorCircuit(aCallback);
    305        break;
    306    }
    307  }
    308 
    309  // DOM event handler
    310  handleEvent(aEvent) {
    311    debug`handleEvent: ${aEvent.type}`;
    312 
    313    switch (aEvent.type) {
    314      case "framefocusrequested":
    315        if (this.browser != aEvent.target) {
    316          return;
    317        }
    318        if (this.browser.hasAttribute("primary")) {
    319          return;
    320        }
    321        this.eventDispatcher.sendRequest({
    322          type: "GeckoView:FocusRequest",
    323        });
    324        aEvent.preventDefault();
    325        break;
    326      case "MozDOMFullscreen:Entered":
    327        if (this.browser == aEvent.target) {
    328          const chromeWindow = this.browser.ownerGlobal;
    329          const requestOrigin =
    330            chromeWindow.browsingContext?.fullscreenRequestOrigin?.get();
    331          if (!requestOrigin) {
    332            chromeWindow.document.exitFullscreen();
    333            return;
    334          }
    335 
    336          // Remote browser; dispatch to content process.
    337          this.#sendEnterDOMFullscreenEvent(requestOrigin);
    338        }
    339        break;
    340      case "MozDOMFullscreen:Exited":
    341        this.#sendExitDOMFullScreenEvent();
    342        break;
    343      case "pagetitlechanged":
    344        this.eventDispatcher.sendRequest({
    345          type: "GeckoView:PageTitleChanged",
    346          title: this.browser.contentTitle,
    347        });
    348        break;
    349      case "DOMWindowClose":
    350        // We need this because we want to allow the app
    351        // to close the window itself. If we don't preventDefault()
    352        // here Gecko will close it immediately.
    353        aEvent.preventDefault();
    354 
    355        this.eventDispatcher.sendRequest({
    356          type: "GeckoView:DOMWindowClose",
    357        });
    358        break;
    359      case "pageinfo":
    360        if (aEvent.detail.previewImageURL) {
    361          this.eventDispatcher.sendRequest({
    362            type: "GeckoView:PreviewImage",
    363            previewImageUrl: aEvent.detail.previewImageURL,
    364          });
    365        }
    366        break;
    367      case "cookiebannerdetected":
    368        this.eventDispatcher.sendRequest({
    369          type: "GeckoView:CookieBannerEvent:Detected",
    370        });
    371        break;
    372      case "cookiebannerhandled":
    373        this.eventDispatcher.sendRequest({
    374          type: "GeckoView:CookieBannerEvent:Handled",
    375        });
    376        break;
    377    }
    378  }
    379 
    380  // nsIObserver event handler
    381  observe(aSubject, aTopic) {
    382    debug`observe: ${aTopic}`;
    383    this._contentCrashed = false;
    384    const browser = aSubject.ownerElement;
    385 
    386    switch (aTopic) {
    387      case "oop-frameloader-crashed": {
    388        if (!browser || browser != this.browser) {
    389          return;
    390        }
    391        this.window.setTimeout(() => {
    392          if (this._contentCrashed) {
    393            this.eventDispatcher.sendRequest({
    394              type: "GeckoView:ContentCrash",
    395            });
    396          } else {
    397            this.eventDispatcher.sendRequest({
    398              type: "GeckoView:ContentKill",
    399            });
    400          }
    401        }, 250);
    402        break;
    403      }
    404      case "ipc:content-shutdown": {
    405        aSubject.QueryInterface(Ci.nsIPropertyBag2);
    406        if (aSubject.get("dumpID")) {
    407          if (
    408            browser &&
    409            aSubject.get("childID") != browser.frameLoader.childID
    410          ) {
    411            return;
    412          }
    413          this._contentCrashed = true;
    414        }
    415        break;
    416      }
    417    }
    418  }
    419 
    420  async _getWebCompatInfo(aCallback) {
    421    if (
    422      Cu.isInAutomation &&
    423      Services.prefs.getBoolPref(
    424        "browser.webcompat.geckoview.enableAllTestMocks",
    425        false
    426      )
    427    ) {
    428      const mockResult = {
    429        devicePixelRatio: 2.5,
    430        antitracking: { hasTrackingContentBlocked: false },
    431      };
    432      aCallback.onSuccess(JSON.stringify(mockResult));
    433      return;
    434    }
    435    try {
    436      const actor =
    437        this.browser.browsingContext.currentWindowGlobal.getActor(
    438          "ReportBrokenSite"
    439        );
    440      const info = await actor.sendQuery("GetWebCompatInfo");
    441 
    442      // Stringify to convert potential non-ASCII
    443      // characters in the returned web compat info map.
    444      aCallback.onSuccess(JSON.stringify(info));
    445    } catch (error) {
    446      aCallback.onError(`Cannot get web compat info, error: ${error}`);
    447    }
    448  }
    449 
    450  async _sendMoreWebCompatInfo(aData, aCallback) {
    451    if (
    452      Cu.isInAutomation &&
    453      Services.prefs.getBoolPref(
    454        "browser.webcompat.geckoview.enableAllTestMocks",
    455        false
    456      )
    457    ) {
    458      aCallback.onSuccess();
    459      return;
    460    }
    461    let infoObj = JSON.parse(aData.info);
    462    try {
    463      const actor =
    464        this.browser.browsingContext.currentWindowGlobal.getActor(
    465          "ReportBrokenSite"
    466        );
    467 
    468      await actor.sendQuery("SendDataToWebcompatCom", infoObj);
    469      aCallback.onSuccess();
    470    } catch (error) {
    471      aCallback.onError(`Cannot send more web compat info, error: ${error}`);
    472    }
    473  }
    474 
    475  _getTorCircuits(aCallback) {
    476    if (this.browser && aCallback) {
    477      const domain = lazy.TorDomainIsolator.getDomainForBrowser(this.browser);
    478      const circuits = lazy.TorDomainIsolator.getCircuits(
    479        this.browser,
    480        domain,
    481        this.browser.contentPrincipal.originAttributes.userContextId
    482      );
    483      aCallback?.onSuccess({ domain, circuits });
    484    } else {
    485      aCallback?.onSuccess(null);
    486    }
    487  }
    488 
    489  _newTorCircuit(aCallback) {
    490    lazy.TorDomainIsolator.newCircuitForBrowser(this.browser);
    491    aCallback?.onSuccess();
    492  }
    493 
    494  async _containsFormData(aCallback) {
    495    aCallback.onSuccess(await this.actor.containsFormData());
    496  }
    497 
    498  async _hasCookieBannerRuleForBrowsingContextTree(aCallback) {
    499    const { browsingContext } = this.actor;
    500    aCallback.onSuccess(
    501      Services.cookieBanners.hasRuleForBrowsingContextTree(browsingContext)
    502    );
    503  }
    504 
    505  _findInPage(aData, aCallback) {
    506    debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`;
    507 
    508    let finder;
    509    try {
    510      finder = this.browser.finder;
    511    } catch (e) {
    512      if (aCallback) {
    513        aCallback.onError(`No finder: ${e}`);
    514      }
    515      return;
    516    }
    517 
    518    if (this._finderListener) {
    519      finder.removeResultListener(this._finderListener);
    520    }
    521 
    522    this._finderListener = {
    523      response: {
    524        found: false,
    525        wrapped: false,
    526        current: 0,
    527        total: -1,
    528        searchString: aData.searchString || finder.searchString,
    529        linkURL: null,
    530        clientRect: null,
    531        flags: {
    532          backwards: !!aData.backwards,
    533          linksOnly: !!aData.linksOnly,
    534          matchCase: !!aData.matchCase,
    535          wholeWord: !!aData.wholeWord,
    536        },
    537      },
    538 
    539      onFindResult(aOptions) {
    540        if (!aCallback || aOptions.searchString !== aData.searchString) {
    541          // Result from a previous search.
    542          return;
    543        }
    544 
    545        Object.assign(this.response, {
    546          found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND,
    547          wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND,
    548          linkURL: aOptions.linkURL,
    549          clientRect: aOptions.rect && {
    550            left: aOptions.rect.left,
    551            top: aOptions.rect.top,
    552            right: aOptions.rect.right,
    553            bottom: aOptions.rect.bottom,
    554          },
    555          flags: {
    556            backwards: aOptions.findBackwards,
    557            linksOnly: aOptions.linksOnly,
    558            matchCase: this.response.flags.matchCase,
    559            wholeWord: this.response.flags.wholeWord,
    560          },
    561        });
    562 
    563        if (!this.response.found) {
    564          this.response.current = 0;
    565          this.response.total = 0;
    566        }
    567 
    568        // Only send response if we have a count.
    569        if (!this.response.found || this.response.current !== 0) {
    570          debug`onFindResult: ${this.response}`;
    571          aCallback.onSuccess(this.response);
    572          aCallback = undefined;
    573        }
    574      },
    575 
    576      onMatchesCountResult(aResult) {
    577        if (!aCallback || finder.searchString !== aData.searchString) {
    578          // Result from a previous search.
    579          return;
    580        }
    581 
    582        Object.assign(this.response, {
    583          current: aResult.current,
    584          total: aResult.total,
    585        });
    586 
    587        // Only send response if we have a result. `found` and `wrapped` are
    588        // both false only when we haven't received a result yet.
    589        if (this.response.found || this.response.wrapped) {
    590          debug`onMatchesCountResult: ${this.response}`;
    591          aCallback.onSuccess(this.response);
    592          aCallback = undefined;
    593        }
    594      },
    595 
    596      onCurrentSelection() {},
    597 
    598      onHighlightFinished() {},
    599    };
    600 
    601    finder.caseSensitive = !!aData.matchCase;
    602    finder.entireWord = !!aData.wholeWord;
    603    finder.matchDiacritics = !!aData.matchDiacritics;
    604    finder.addResultListener(this._finderListener);
    605 
    606    const drawOutline =
    607      this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline;
    608 
    609    if (!aData.searchString || aData.searchString === finder.searchString) {
    610      // Search again.
    611      aData.searchString = finder.searchString;
    612      finder.findAgain(
    613        aData.searchString,
    614        !!aData.backwards,
    615        !!aData.linksOnly,
    616        drawOutline
    617      );
    618    } else {
    619      finder.fastFind(aData.searchString, !!aData.linksOnly, drawOutline);
    620    }
    621  }
    622 
    623  _clearMatches() {
    624    debug`clearMatches`;
    625 
    626    let finder;
    627    try {
    628      finder = this.browser.finder;
    629    } catch (e) {
    630      return;
    631    }
    632 
    633    finder.removeSelection();
    634    finder.highlight(false);
    635 
    636    if (this._finderListener) {
    637      finder.removeResultListener(this._finderListener);
    638      this._finderListener = null;
    639    }
    640  }
    641 
    642  _displayMatches(aData) {
    643    debug`displayMatches: data=${aData}`;
    644 
    645    let finder;
    646    try {
    647      finder = this.browser.finder;
    648    } catch (e) {
    649      return;
    650    }
    651 
    652    this._matchDisplayOptions = aData;
    653    finder.onModalHighlightChange(!!aData.dimPage);
    654    finder.onHighlightAllChange(!!aData.highlightAll);
    655 
    656    if (!aData.highlightAll && !aData.dimPage) {
    657      finder.highlight(false);
    658      return;
    659    }
    660 
    661    if (!this._finderListener || !finder.searchString) {
    662      return;
    663    }
    664    const linksOnly = this._finderListener.response.linksOnly;
    665    finder.highlight(true, finder.searchString, linksOnly, !!aData.drawOutline);
    666  }
    667 }
    668 
    669 const { debug, warn } = GeckoViewContent.initLogging("GeckoViewContent");