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");