tor-browser

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

GeckoViewNavigation.sys.mjs (22643B)


      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 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.sys.mjs",
     11  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     12  LoadURIDelegate: "resource://gre/modules/LoadURIDelegate.sys.mjs",
     13  TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
     14 });
     15 
     16 ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
     17  Components.Constructor(
     18    "@mozilla.org/referrer-info;1",
     19    "nsIReferrerInfo",
     20    "init"
     21  )
     22 );
     23 
     24 // Filter out request headers as per discussion in Bug #1567549
     25 // CONNECTION: Used by Gecko to manage connections
     26 // HOST: Relates to how gecko will ultimately interpret the resulting resource as that
     27 //       determines the effective request URI
     28 const BAD_HEADERS = ["connection", "host"];
     29 
     30 // Headers use |\r\n| as separator so these characters cannot appear
     31 // in the header name or value
     32 const FORBIDDEN_HEADER_CHARACTERS = ["\n", "\r"];
     33 
     34 // Keep in sync with GeckoSession.java
     35 const HEADER_FILTER_CORS_SAFELISTED = 1;
     36 // eslint-disable-next-line no-unused-vars
     37 const HEADER_FILTER_UNRESTRICTED_UNSAFE = 2;
     38 
     39 // Create default ReferrerInfo instance for the given referrer URI string.
     40 const createReferrerInfo = aReferrer => {
     41  let referrerUri;
     42  try {
     43    referrerUri = Services.io.newURI(aReferrer);
     44  } catch (ignored) {}
     45 
     46  return new lazy.ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, referrerUri);
     47 };
     48 
     49 function convertFlags(aFlags) {
     50  let navFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
     51  if (!aFlags) {
     52    return navFlags;
     53  }
     54  // These need to match the values in GeckoSession.LOAD_FLAGS_*
     55  if (aFlags & (1 << 0)) {
     56    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
     57  }
     58  if (aFlags & (1 << 1)) {
     59    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY;
     60  }
     61  if (aFlags & (1 << 2)) {
     62    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
     63  }
     64  if (aFlags & (1 << 3)) {
     65    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
     66  }
     67  if (aFlags & (1 << 4)) {
     68    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER;
     69  }
     70  if (aFlags & (1 << 5)) {
     71    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
     72  }
     73  if (aFlags & (1 << 6)) {
     74    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
     75  }
     76  if (aFlags & (1 << 7)) {
     77    navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE;
     78  }
     79  return navFlags;
     80 }
     81 
     82 // Handles navigation requests between Gecko and a GeckoView.
     83 // Handles GeckoView:GoBack and :GoForward requests dispatched by
     84 // GeckoView.goBack and .goForward.
     85 // Dispatches GeckoView:LocationChange to the GeckoView on location change when
     86 // active.
     87 // Implements nsIBrowserDOMWindow.
     88 export class GeckoViewNavigation extends GeckoViewModule {
     89  onInitBrowser() {
     90    this.window.browserDOMWindow = this;
     91 
     92    debug`sessionContextId=${this.settings.sessionContextId}`;
     93 
     94    if (this.settings.sessionContextId !== null) {
     95      // Gecko may have issues with strings containing special characters,
     96      // so we restrict the string format to a specific pattern.
     97      if (!/^gvctx(-)?([a-f0-9]+)$/.test(this.settings.sessionContextId)) {
     98        throw new Error("sessionContextId has illegal format");
     99      }
    100 
    101      this.browser.setAttribute(
    102        "geckoViewSessionContextId",
    103        this.settings.sessionContextId
    104      );
    105    }
    106 
    107    // There may be a GeckoViewNavigation module in another window waiting for
    108    // us to create a browser so it can set openWindowInfo, so allow them to do
    109    // that now.
    110    Services.obs.notifyObservers(this.window, "geckoview-window-created");
    111  }
    112 
    113  onInit() {
    114    debug`onInit`;
    115 
    116    this.registerListener([
    117      "GeckoView:GoBack",
    118      "GeckoView:GoForward",
    119      "GeckoView:GotoHistoryIndex",
    120      "GeckoView:LoadUri",
    121      "GeckoView:Reload",
    122      "GeckoView:Stop",
    123      "GeckoView:PurgeHistory",
    124      "GeckoView:DotPrintFinish",
    125    ]);
    126 
    127    this._initialAboutBlank = true;
    128  }
    129 
    130  validateHeader(key, value, filter) {
    131    if (!key) {
    132      // Key cannot be empty
    133      return false;
    134    }
    135 
    136    for (const c of FORBIDDEN_HEADER_CHARACTERS) {
    137      if (key.includes(c) || value?.includes(c)) {
    138        return false;
    139      }
    140    }
    141 
    142    if (BAD_HEADERS.includes(key.toLowerCase().trim())) {
    143      return false;
    144    }
    145 
    146    if (
    147      filter == HEADER_FILTER_CORS_SAFELISTED &&
    148      !this.window.windowUtils.isCORSSafelistedRequestHeader(key, value)
    149    ) {
    150      return false;
    151    }
    152 
    153    return true;
    154  }
    155 
    156  // Bundle event handler.
    157  async onEvent(aEvent, aData) {
    158    debug`onEvent: event=${aEvent}, data=${aData}`;
    159 
    160    switch (aEvent) {
    161      case "GeckoView:GoBack":
    162        this.browser.goBack(aData.userInteraction);
    163        break;
    164      case "GeckoView:GoForward":
    165        this.browser.goForward(aData.userInteraction);
    166        break;
    167      case "GeckoView:GotoHistoryIndex":
    168        this.browser.gotoIndex(aData.index);
    169        break;
    170      case "GeckoView:LoadUri": {
    171        const {
    172          uri,
    173          referrerUri,
    174          referrerSessionId,
    175          flags,
    176          headers,
    177          headerFilter,
    178          originalInput,
    179          textDirectiveUserActivation,
    180          appLinkLaunchType,
    181        } = aData;
    182 
    183        let navFlags = convertFlags(flags);
    184        // For performance reasons we don't call the LoadUriDelegate.loadUri
    185        // from Gecko, and instead we call it directly in the loadUri Java API.
    186        navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE;
    187 
    188        let triggeringPrincipal, referrerInfo, policyContainer;
    189        if (referrerSessionId) {
    190          const referrerWindow = Services.ww.getWindowByName(referrerSessionId);
    191          triggeringPrincipal = referrerWindow.browser.contentPrincipal;
    192          policyContainer = referrerWindow.browser.policyContainer;
    193 
    194          const { contentPrincipal } = this.browser;
    195          const isNormal = contentPrincipal.privateBrowsingId == 0;
    196          const referrerIsPrivate = triggeringPrincipal.privateBrowsingId != 0;
    197 
    198          const referrerPolicy = referrerWindow.browser.referrerInfo
    199            ? referrerWindow.browser.referrerInfo.referrerPolicy
    200            : Ci.nsIReferrerInfo.EMPTY;
    201 
    202          referrerInfo = new lazy.ReferrerInfo(
    203            referrerPolicy,
    204            // Don't `sendReferrer` if the private session (current) is opened by a normal session (referrer)
    205            isNormal || referrerIsPrivate,
    206            referrerWindow.browser.documentURI
    207          );
    208        } else if (referrerUri) {
    209          referrerInfo = createReferrerInfo(referrerUri);
    210        } else {
    211          // External apps are treated like web pages, so they should not get
    212          // a privileged principal.
    213          const isExternal =
    214            navFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
    215          if (!isExternal || Services.io.newURI(uri).schemeIs("content")) {
    216            // Always use the system principal as the triggering principal
    217            // for user-initiated (ie. no referrer session and not external)
    218            // loads. See discussion in bug 1573860.
    219            triggeringPrincipal =
    220              Services.scriptSecurityManager.getSystemPrincipal();
    221          }
    222        }
    223 
    224        if (!triggeringPrincipal) {
    225          triggeringPrincipal =
    226            Services.scriptSecurityManager.createNullPrincipal({});
    227        }
    228 
    229        let additionalHeaders = null;
    230        if (headers) {
    231          additionalHeaders = "";
    232          for (const [key, value] of Object.entries(headers)) {
    233            if (!this.validateHeader(key, value, headerFilter)) {
    234              console.error(`Ignoring invalid header '${key}'='${value}'.`);
    235              continue;
    236            }
    237 
    238            additionalHeaders += `${key}:${value ?? ""}\r\n`;
    239          }
    240 
    241          if (additionalHeaders != "") {
    242            additionalHeaders =
    243              lazy.E10SUtils.makeInputStream(additionalHeaders);
    244          } else {
    245            additionalHeaders = null;
    246          }
    247        }
    248 
    249        let schemelessInput = 0;
    250        if (originalInput) {
    251          schemelessInput =
    252            !originalInput.toLowerCase().startsWith("http://") &&
    253            uri.toLowerCase().startsWith("http://")
    254              ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless
    255              : Ci.nsILoadInfo.SchemelessInputTypeSchemeful;
    256        }
    257 
    258        // For any navigation here, we should have an appropriate triggeringPrincipal:
    259        //
    260        // 1) If we have a referring session, triggeringPrincipal is the contentPrincipal from the
    261        //    referring document.
    262        // 2) For certain URI schemes listed above, we will have a codebase principal.
    263        // 3) In all other cases, we create a NullPrincipal.
    264        //
    265        // The navigation flags are driven by the app. We purposely do not propagate these from
    266        // the referring document, but expect that the app will in most cases.
    267        //
    268        // The referrerInfo is derived from the referring document, if present, by propagating any
    269        // referrer policy. If we only have the referrerUri from the app, we create a referrerInfo
    270        // with the specified URI and no policy set. If no referrerUri is present and we have no
    271        // referring session, the referrerInfo is null.
    272        //
    273        // policyContainer is only present if we have a referring document, null otherwise.
    274        this.browser.fixupAndLoadURIString(uri, {
    275          loadFlags: navFlags,
    276          referrerInfo,
    277          triggeringPrincipal,
    278          headers: additionalHeaders,
    279          policyContainer,
    280          textDirectiveUserActivation,
    281          schemelessInput,
    282          appLinkLaunchType,
    283        });
    284        break;
    285      }
    286      case "GeckoView:Reload":
    287        // At the moment, GeckoView only supports one reload, which uses
    288        // nsIWebNavigation.LOAD_FLAGS_NONE flag, and the telemetry doesn't
    289        // do anything to differentiate reloads (i.e normal vs skip caches)
    290        // So whenever we add more reload methods, please make sure the
    291        // telemetry probe is adjusted
    292        this.browser.reloadWithFlags(convertFlags(aData.flags));
    293        break;
    294      case "GeckoView:Stop":
    295        this.browser.stop();
    296        break;
    297      case "GeckoView:PurgeHistory":
    298        this.browser.purgeSessionHistory();
    299        break;
    300      case "GeckoView:DotPrintFinish":
    301        var printActor = this.moduleManager.getActor("GeckoViewPrintDelegate");
    302        printActor.clearStaticClone();
    303        break;
    304    }
    305  }
    306 
    307  handleNewSession(aUri, aOpenWindowInfo, aWhere, aFlags, aName) {
    308    debug`handleNewSession: uri=${aUri && aUri.spec}
    309                             where=${aWhere} flags=${aFlags}`;
    310 
    311    let browser = undefined;
    312    this._handleNewSessionAsync({
    313      aUri,
    314      aOpenWindowInfo,
    315      aName,
    316    }).then(
    317      result => {
    318        browser = result;
    319      },
    320      () => {
    321        browser = null;
    322      }
    323    );
    324 
    325    // Wait indefinitely for app to respond with a browser or null
    326    Services.tm.spinEventLoopUntil(
    327      "GeckoViewNavigation.sys.mjs:handleNewSession",
    328      () => this.window.closed || browser !== undefined
    329    );
    330    return browser || null;
    331  }
    332 
    333  #isNewTab(aWhere) {
    334    return [
    335      Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
    336      Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND,
    337    ].includes(aWhere);
    338  }
    339 
    340  /**
    341   * Similar to handleNewSession. But this returns a promise to wait for new
    342   * browser.
    343   */
    344  _handleNewSessionAsync({ aUri, aOpenWindowInfo, aName }) {
    345    if (!this.enabled) {
    346      return Promise.reject();
    347    }
    348 
    349    const newSessionId = Services.uuid
    350      .generateUUID()
    351      .toString()
    352      .slice(1, -1)
    353      .replace(/-/g, "");
    354 
    355    const message = {
    356      type: "GeckoView:OnNewSession",
    357      uri: aUri ? aUri.displaySpec : "",
    358      newSessionId,
    359    };
    360 
    361    // The window might be already open by the time we get the response from
    362    // the Java layer, so we need to start waiting before sending the message.
    363    const setupPromise = lazy.GeckoViewUtils.waitAndSetupWindow(
    364      newSessionId,
    365      aOpenWindowInfo,
    366      aName
    367    );
    368 
    369    return this.eventDispatcher
    370      .sendRequestForResult(message)
    371      .then(didOpenSession => {
    372        if (!didOpenSession) {
    373          // New session cannot be opened, so we should throw NS_ERROR_ABORT.
    374          return Promise.reject();
    375        }
    376        return setupPromise;
    377      })
    378      .then(newWindow => {
    379        return newWindow.browser;
    380      });
    381  }
    382 
    383  // nsIBrowserDOMWindow.
    384  createContentWindow(
    385    aUri,
    386    aOpenWindowInfo,
    387    aWhere,
    388    aFlags,
    389    aTriggeringPrincipal,
    390    aPolicyContainer
    391  ) {
    392    debug`createContentWindow: uri=${aUri && aUri.spec}
    393                                where=${aWhere} flags=${aFlags}`;
    394 
    395    if (!this.enabled) {
    396      Components.returnCode = Cr.NS_ERROR_ABORT;
    397      return null;
    398    }
    399 
    400    const promise = lazy.LoadURIDelegate.load(
    401      this.window,
    402      this.eventDispatcher,
    403      aUri,
    404      aWhere,
    405      aFlags,
    406      aTriggeringPrincipal
    407    ).then(handled => {
    408      if (handled) {
    409        // This will throw NS_ERROR_ABORT
    410        return Promise.reject();
    411      }
    412      return this._handleNewSessionAsync({
    413        aUri,
    414        aOpenWindowInfo,
    415        aWhere,
    416      });
    417    });
    418 
    419    const newTab = this.#isNewTab(aWhere);
    420 
    421    // Actually, GeckoView's createContentWindow always creates new window even
    422    // if OPEN_NEWTAB. So the browsing context will be observed via
    423    // nsFrameLoader.
    424    if (aOpenWindowInfo && !newTab) {
    425      promise.catch(() => {
    426        aOpenWindowInfo.cancel();
    427      });
    428      // If nsIOpenWindowInfo isn't null, caller should use the callback.
    429      // Also, nsIWindowProvider.provideWindow doesn't use callback, if new
    430      // tab option, we have to return browsing context instead of async.
    431      return null;
    432    }
    433 
    434    let browser = undefined;
    435    promise.then(
    436      result => {
    437        browser = result;
    438      },
    439      () => {
    440        browser = null;
    441      }
    442    );
    443 
    444    // Wait indefinitely for app to respond with a browser or null.
    445    // if browser is null, return error.
    446    Services.tm.spinEventLoopUntil(
    447      "GeckoViewNavigation.sys.mjs:createContentWindow",
    448      () => this.window.closed || browser !== undefined
    449    );
    450 
    451    if (!browser) {
    452      Components.returnCode = Cr.NS_ERROR_ABORT;
    453      return null;
    454    }
    455 
    456    return browser.browsingContext;
    457  }
    458 
    459  async _createContentWindowInFrameAsync(aUri, aParams, aWhere, aFlags, aName) {
    460    if (
    461      await lazy.LoadURIDelegate.load(
    462        this.window,
    463        this.eventDispatcher,
    464        aUri,
    465        aWhere,
    466        aFlags,
    467        aParams.triggeringPrincipal
    468      )
    469    ) {
    470      return null;
    471    }
    472 
    473    return await this._handleNewSessionAsync({
    474      aUri,
    475      aOpenWindowInfo: aParams.openWindowInfo,
    476      aName,
    477    });
    478  }
    479 
    480  // nsIBrowserDOMWindow.
    481  createContentWindowInFrame(aUri, aParams, aWhere, aFlags, aName) {
    482    debug`createContentWindowInFrame: uri=${aUri && aUri.spec}
    483                                       where=${aWhere} flags=${aFlags}
    484                                       name=${aName}`;
    485 
    486    if (aWhere === Ci.nsIBrowserDOMWindow.OPEN_PRINT_BROWSER) {
    487      return this.window.moduleManager.onPrintWindow(aParams);
    488    }
    489 
    490    let browser = undefined;
    491    this._createContentWindowInFrameAsync(
    492      aUri,
    493      aParams,
    494      aWhere,
    495      aFlags,
    496      aName
    497    ).then(
    498      result => {
    499        browser = result;
    500      },
    501      () => {
    502        browser = null;
    503      }
    504    );
    505 
    506    // Wait indefinitely for app to respond with a browser or null.
    507    // if browser is null, return error.
    508    Services.tm.spinEventLoopUntil(
    509      "GeckoViewNavigation.sys.mjs:createContentWindowInFrame",
    510      () => this.window.closed || browser !== undefined
    511    );
    512 
    513    if (!browser) {
    514      Components.returnCode = Cr.NS_ERROR_ABORT;
    515      return null;
    516    }
    517 
    518    return browser;
    519  }
    520 
    521  async _handleOpenUriAsync({
    522    uri,
    523    openWindowInfo,
    524    where,
    525    flags,
    526    triggeringPrincipal,
    527    policyContainer,
    528    referrerInfo = null,
    529    name = null,
    530  }) {
    531    debug`_handleOpenUriAsync: uri=${uri?.spec} where=${where} flags=${flags}`;
    532 
    533    if (
    534      await lazy.LoadURIDelegate.load(
    535        this.window,
    536        this.eventDispatcher,
    537        uri,
    538        where,
    539        flags,
    540        triggeringPrincipal
    541      )
    542    ) {
    543      return null;
    544    }
    545 
    546    const browser = await (async () => {
    547      if (
    548        where === Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
    549        where === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
    550        where === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND ||
    551        where === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_FOREGROUND
    552      ) {
    553        return await this._handleNewSessionAsync({
    554          aUri: uri,
    555          aOpenWindowInfo: openWindowInfo,
    556          aName: name,
    557        });
    558      }
    559      return this.browser;
    560    })();
    561 
    562    if (!browser) {
    563      return null;
    564    }
    565 
    566    // 3) We have a new session and a browser element, load the requested URI.
    567    browser.loadURI(uri, {
    568      triggeringPrincipal,
    569      policyContainer,
    570      referrerInfo,
    571      hasValidUserGestureActivation:
    572        !!openWindowInfo?.hasValidUserGestureActivation,
    573      textDirectiveUserActivation:
    574        !!openWindowInfo?.textDirectiveUserActivation,
    575    });
    576 
    577    return browser;
    578  }
    579 
    580  _handleOpenUri(openUriInfo) {
    581    let browser = undefined;
    582    this._handleOpenUriAsync(openUriInfo).then(
    583      result => {
    584        browser = result;
    585      },
    586      () => {
    587        browser = null;
    588      }
    589    );
    590 
    591    Services.tm.spinEventLoopUntil(
    592      "GeckoViewNavigation.sys.mjs:_handleOpenUri",
    593      () => this.window.closed || browser !== undefined
    594    );
    595 
    596    return browser;
    597  }
    598 
    599  // nsIBrowserDOMWindow.
    600  openURI(
    601    aUri,
    602    aOpenWindowInfo,
    603    aWhere,
    604    aFlags,
    605    aTriggeringPrincipal,
    606    aPolicyContainer
    607  ) {
    608    const browser = this._handleOpenUri({
    609      uri: aUri,
    610      openWindowInfo: aOpenWindowInfo,
    611      where: aWhere,
    612      flags: aFlags,
    613      triggeringPrincipal: aTriggeringPrincipal,
    614      policyContainer: aPolicyContainer,
    615    });
    616 
    617    if (!browser) {
    618      Components.returnCode = Cr.NS_ERROR_ABORT;
    619      return null;
    620    }
    621 
    622    return browser && browser.browsingContext;
    623  }
    624 
    625  // nsIBrowserDOMWindow.
    626  openURIInFrame(aUri, aParams, aWhere, aFlags, aName) {
    627    const browser = this._handleOpenUri({
    628      uri: aUri,
    629      openWindowInfo: aParams.openWindowInfo,
    630      where: aWhere,
    631      flags: aFlags,
    632      triggeringPrincipal: aParams.triggeringPrincipal,
    633      policyContainer: aParams.policyContainer,
    634      referrerInfo: aParams.referrerInfo,
    635      name: aName,
    636    });
    637 
    638    if (!browser) {
    639      Components.returnCode = Cr.NS_ERROR_ABORT;
    640      return null;
    641    }
    642 
    643    return browser;
    644  }
    645 
    646  // nsIBrowserDOMWindow.
    647  canClose() {
    648    debug`canClose`;
    649    return true;
    650  }
    651 
    652  onEnable() {
    653    debug`onEnable`;
    654 
    655    const flags = Ci.nsIWebProgress.NOTIFY_LOCATION;
    656    this.progressFilter = Cc[
    657      "@mozilla.org/appshell/component/browser-status-filter;1"
    658    ].createInstance(Ci.nsIWebProgress);
    659    this.progressFilter.addProgressListener(this, flags);
    660    this.browser.addProgressListener(this.progressFilter, flags);
    661  }
    662 
    663  onDisable() {
    664    debug`onDisable`;
    665 
    666    if (!this.progressFilter) {
    667      return;
    668    }
    669    this.progressFilter.removeProgressListener(this);
    670    this.browser.removeProgressListener(this.progressFilter);
    671  }
    672 
    673  serializePermission({ type, capability, principal }) {
    674    const { URI, originAttributes, privateBrowsingId } = principal;
    675    return {
    676      uri: Services.io.createExposableURI(URI).displaySpec,
    677      principal: lazy.E10SUtils.serializePrincipal(principal),
    678      perm: type,
    679      value: capability,
    680      contextId: originAttributes.geckoViewSessionContextId,
    681      privateMode: privateBrowsingId != 0,
    682    };
    683  }
    684 
    685  // WebProgress event handler.
    686  onLocationChange(aWebProgress, aRequest, aLocationURI) {
    687    debug`onLocationChange`;
    688 
    689    let fixedURI = aLocationURI;
    690 
    691    try {
    692      fixedURI = Services.io.createExposableURI(aLocationURI);
    693    } catch (ex) {}
    694 
    695    // We manually fire the initial about:blank messages to make sure that we
    696    // consistently send them so there's nothing to do here.
    697    const ignore = this._initialAboutBlank && fixedURI.spec === "about:blank";
    698    this._initialAboutBlank = false;
    699 
    700    if (ignore) {
    701      return;
    702    }
    703 
    704    const { contentPrincipal } = this.browser;
    705    let permissions;
    706    if (
    707      contentPrincipal &&
    708      lazy.GeckoViewUtils.isSupportedPermissionsPrincipal(contentPrincipal)
    709    ) {
    710      let rawPerms = [];
    711      try {
    712        rawPerms = Services.perms.getAllForPrincipal(contentPrincipal);
    713      } catch (ex) {
    714        warn`Could not get permissions for principal. ${ex}`;
    715      }
    716      permissions = rawPerms.map(this.serializePermission);
    717 
    718      // The only way for apps to set permissions is to get hold of an existing
    719      // permission and change its value.
    720      // Tracking protection exception permissions are only present when
    721      // explicitly added by the app, so if one is not present, we need to send
    722      // a DENY_ACTION tracking protection permission so that apps can use it
    723      // to add tracking protection exceptions.
    724      const trackingProtectionPermission =
    725        contentPrincipal.privateBrowsingId == 0
    726          ? "trackingprotection"
    727          : "trackingprotection-pb";
    728      if (
    729        contentPrincipal.isContentPrincipal &&
    730        rawPerms.findIndex(p => p.type == trackingProtectionPermission) == -1
    731      ) {
    732        permissions.push(
    733          this.serializePermission({
    734            type: trackingProtectionPermission,
    735            capability: Ci.nsIPermissionManager.DENY_ACTION,
    736            principal: contentPrincipal,
    737          })
    738        );
    739      }
    740    }
    741 
    742    const message = {
    743      type: "GeckoView:LocationChange",
    744      uri: fixedURI.displaySpec,
    745      canGoBack: this.browser.canGoBack,
    746      canGoForward: this.browser.canGoForward,
    747      isTopLevel: aWebProgress.isTopLevel,
    748      permissions,
    749      hasUserGesture:
    750        this.window.document.hasValidTransientUserGestureActivation !== null
    751          ? this.window.document.hasValidTransientUserGestureActivation
    752          : false,
    753    };
    754    lazy.TranslationsParent.onLocationChange(this.browser);
    755    this.eventDispatcher.sendRequest(message);
    756  }
    757 }
    758 
    759 const { debug, warn } = GeckoViewNavigation.initLogging("GeckoViewNavigation");