NavigationManager.sys.mjs (34326B)
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 { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", 11 BrowsingContextListener: 12 "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs", 13 DownloadListener: 14 "chrome://remote/content/shared/listeners/DownloadListener.sys.mjs", 15 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", 16 Log: "chrome://remote/content/shared/Log.sys.mjs", 17 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 18 ParentWebProgressListener: 19 "chrome://remote/content/shared/listeners/ParentWebProgressListener.sys.mjs", 20 PromptListener: 21 "chrome://remote/content/shared/listeners/PromptListener.sys.mjs", 22 registerWebDriverDocumentInsertedActor: 23 "chrome://remote/content/shared/js-process-actors/WebDriverDocumentInsertedActor.sys.mjs", 24 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 25 truncate: "chrome://remote/content/shared/Format.sys.mjs", 26 unregisterWebDriverDocumentInsertedActor: 27 "chrome://remote/content/shared/js-process-actors/WebDriverDocumentInsertedActor.sys.mjs", 28 }); 29 30 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 31 32 /** 33 * @typedef {object} BrowsingContextDetails 34 * @property {string} browsingContextId - The browsing context id. 35 * @property {string} browserId - The id of the Browser owning the browsing 36 * context. 37 * @property {BrowsingContext=} context - The BrowsingContext itself, if 38 * available. 39 * @property {boolean} isTopBrowsingContext - Whether the browsing context is 40 * top level. 41 */ 42 43 /** 44 * Enum of all supported navigation manager events. 45 * 46 * @enum {string} 47 */ 48 export const NAVIGATION_EVENTS = { 49 DownloadEnd: "download-end", 50 DownloadStarted: "download-started", 51 FragmentNavigated: "fragment-navigated", 52 HistoryUpdated: "history-updated", 53 NavigationCommitted: "navigation-committed", 54 NavigationFailed: "navigation-failed", 55 NavigationStarted: "navigation-started", 56 NavigationStopped: "navigation-stopped", 57 SameDocumentChanged: "same-document-changed", 58 }; 59 60 /** 61 * Enum of navigation states. 62 * 63 * @enum {string} 64 */ 65 export const NavigationState = { 66 Registered: "registered", 67 InitialAboutBlank: "initial-about-blank", 68 Started: "started", 69 Finished: "finished", 70 }; 71 72 /** 73 * @typedef {object} NavigationInfo 74 * @property {boolean} committed - Whether the navigation was ever committed. 75 * @property {string} contextId - ID of the browsing context. 76 * @property {string} navigable - The UUID for the navigable. 77 * @property {string} navigationId - The UUID for the navigation. 78 * @property {NavigationState} state - The navigation state. 79 * @property {string} url - The target url for the navigation. 80 */ 81 82 /** 83 * The NavigationRegistry is responsible for monitoring all navigations happening 84 * in the browser. 85 * 86 * The NavigationRegistry singleton holds the map of navigations, from navigable 87 * to NavigationInfo. It will also be called by WebProgressListenerParent 88 * whenever a navigation event happens. 89 * 90 * This singleton is not exported outside of this class, and consumers instead 91 * need to use the NavigationManager class. The NavigationRegistry keeps track 92 * of how many NavigationListener instances are currently listening in order to 93 * know if the WebProgressListenerActor should be registered or not. 94 * 95 * The NavigationRegistry exposes an API to retrieve the current or last 96 * navigation for a given navigable, and also forwards events to notify about 97 * navigation updates to individual NavigationManager instances. 98 * 99 * @class NavigationRegistry 100 */ 101 class NavigationRegistry extends EventEmitter { 102 #contextListener; 103 #downloadListener; 104 #downloadNavigations; 105 #managers; 106 #navigations; 107 #promptListener; 108 #webProgressListener; 109 110 constructor() { 111 super(); 112 113 // Set of NavigationManager instances currently used. 114 this.#managers = new Set(); 115 116 // Maps navigable id to NavigationInfo. 117 this.#navigations = new Map(); 118 119 // Keep track of ongoing download navigations, from Download object to 120 // navigation id. 121 this.#downloadNavigations = new WeakMap(); 122 123 this.#webProgressListener = new lazy.ParentWebProgressListener(); 124 125 this.#contextListener = new lazy.BrowsingContextListener(); 126 this.#contextListener.on("attached", this.#onContextAttached); 127 this.#contextListener.on("discarded", this.#onContextDiscarded); 128 129 this.#downloadListener = new lazy.DownloadListener(); 130 this.#downloadListener.on("download-started", this.#onDownloadStarted); 131 this.#downloadListener.on("download-stopped", this.#onDownloadStopped); 132 133 this.#promptListener = new lazy.PromptListener(); 134 this.#promptListener.on("closed", this.#onPromptClosed); 135 this.#promptListener.on("opened", this.#onPromptOpened); 136 } 137 138 /** 139 * Retrieve the last known navigation data for a given browsing context. 140 * 141 * @param {BrowsingContext} context 142 * The browsing context for which the navigation event was recorded. 143 * @returns {NavigationInfo|null} 144 * The last known navigation data, or null. 145 */ 146 getNavigationForBrowsingContext(context) { 147 if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) { 148 // Bail out if the provided context is not a valid CanonicalBrowsingContext 149 // instance. 150 return null; 151 } 152 153 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 154 if (!this.#navigations.has(navigableId)) { 155 return null; 156 } 157 158 return this.#navigations.get(navigableId); 159 } 160 161 /** 162 * Start monitoring navigations in all browsing contexts. 163 */ 164 startMonitoring(listener) { 165 if (this.#managers.size == 0) { 166 lazy.registerWebDriverDocumentInsertedActor(); 167 168 this.#contextListener.startListening(); 169 this.#webProgressListener.startListening(); 170 this.#downloadListener.startListening(); 171 this.#promptListener.startListening(); 172 } 173 174 this.#managers.add(listener); 175 } 176 177 /** 178 * Stop monitoring navigations. This will clear the information collected 179 * about navigations so far. 180 */ 181 stopMonitoring(listener) { 182 if (!this.#managers.has(listener)) { 183 return; 184 } 185 186 this.#managers.delete(listener); 187 if (this.#managers.size == 0) { 188 this.#contextListener.stopListening(); 189 this.#webProgressListener.stopListening(); 190 this.#downloadListener.stopListening(); 191 this.#promptListener.stopListening(); 192 193 lazy.unregisterWebDriverDocumentInsertedActor(); 194 195 // Clear the map. 196 this.#navigations = new Map(); 197 } 198 } 199 200 /** 201 * This entry point is only intended to be called from 202 * WebProgressListenerParent, to avoid setting up observers or listeners, 203 * which are unnecessary since NavigationManager has to be a singleton. 204 * 205 * @param {object} data 206 * @param {BrowsingContext} data.context 207 * The browsing context for which the navigation event was recorded. 208 * @param {string} data.url 209 * The URL as string for the navigation. 210 * @returns {NavigationInfo} 211 * The navigation created for this hash changed navigation. 212 */ 213 notifyFragmentNavigated(data) { 214 const { contextDetails, url } = data; 215 216 const context = this.#getContextFromContextDetails(contextDetails); 217 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 218 219 const navigationId = this.#getOrCreateNavigationId(navigableId); 220 const navigation = this.#createNavigationObject({ 221 contextId: context.id, 222 state: NavigationState.Finished, 223 navigationId, 224 url, 225 }); 226 227 // Update the current navigation for the navigable only if there is no 228 // ongoing navigation for the navigable. 229 const currentNavigation = this.#navigations.get(navigableId); 230 if ( 231 !currentNavigation || 232 currentNavigation.state == NavigationState.Finished 233 ) { 234 this.#navigations.set(navigableId, navigation); 235 } 236 237 // Hash change navigations are immediately done, fire a single event. 238 this.emit(NAVIGATION_EVENTS.FragmentNavigated, { 239 navigationId, 240 navigableId, 241 url, 242 }); 243 244 return navigation; 245 } 246 247 /** 248 * Called when a history updated event is recorded from the 249 * WebProgressListener actors. 250 * 251 * This entry point is only intended to be called from 252 * WebProgressListenerParent, to avoid setting up observers or listeners, 253 * which are unnecessary since NavigationManager has to be a singleton. 254 * 255 * Note that a history-updated event should not create a new navigation, or 256 * generate a new navigation id. 257 * 258 * @param {object} data 259 * @param {BrowsingContext} data.context 260 * The browsing context for which the navigation event was recorded. 261 * @param {string} data.url 262 * The URL as string for the navigation. 263 */ 264 notifyHistoryUpdated(data) { 265 const { contextDetails, url } = data; 266 267 const context = this.#getContextFromContextDetails(contextDetails); 268 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 269 270 // History updates are immediately done, fire a single event. 271 this.emit(NAVIGATION_EVENTS.HistoryUpdated, { 272 contextId: context.id, 273 navigableId, 274 url, 275 }); 276 } 277 278 /** 279 * Called when a same-document navigation is recorded from the 280 * WebProgressListener actors. 281 * 282 * This entry point is only intended to be called from 283 * WebProgressListenerParent, to avoid setting up observers or listeners, 284 * which are unnecessary since NavigationManager has to be a singleton. 285 * 286 * @param {object} data 287 * @param {BrowsingContext} data.context 288 * The browsing context for which the navigation event was recorded. 289 * @param {string} data.url 290 * The URL as string for the navigation. 291 * @returns {NavigationInfo} 292 * The navigation created for this same-document navigation. 293 */ 294 notifySameDocumentChanged(data) { 295 const { contextDetails, url } = data; 296 297 const context = this.#getContextFromContextDetails(contextDetails); 298 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 299 300 const navigationId = this.#getOrCreateNavigationId(navigableId); 301 const navigation = this.#createNavigationObject({ 302 state: NavigationState.Finished, 303 navigationId, 304 url, 305 }); 306 307 // Update the current navigation for the navigable only if there is no 308 // ongoing navigation for the navigable. 309 const currentNavigation = this.#navigations.get(navigableId); 310 if ( 311 !currentNavigation || 312 currentNavigation.state == NavigationState.Finished 313 ) { 314 this.#navigations.set(navigableId, navigation); 315 } 316 317 // Same document navigations are immediately done, fire a single event. 318 319 this.emit(NAVIGATION_EVENTS.SameDocumentChanged, { 320 navigationId, 321 navigableId, 322 url, 323 }); 324 325 return navigation; 326 } 327 328 /** 329 * Called when a `document-inserted` event is recorded from the 330 * WebDriverDocumentInserted actors. 331 * 332 * This entry point is only intended to be called from 333 * WebDriverDocumentInsertedParent, to avoid setting up 334 * observers or listeners, which are unnecessary since 335 * NavigationManager has to be a singleton. 336 * 337 * @param {object} data 338 * @param {BrowsingContextDetails} data.contextDetails 339 * The details about the browsing context for this navigation. 340 * @param {string} data.errorName 341 * The error message. 342 * @param {string} data.url 343 * The URL as string for the navigation. 344 * @returns {NavigationInfo} 345 * The created navigation or the ongoing navigation, if applicable. 346 */ 347 notifyNavigationCommitted(data) { 348 const { contextDetails, errorName, url } = data; 349 350 const context = this.#getContextFromContextDetails(contextDetails); 351 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 352 const navigation = this.#navigations.get(navigableId); 353 354 if (!navigation) { 355 lazy.logger.trace( 356 lazy.truncate`[${navigableId}] No navigation found to commit for url: ${url}` 357 ); 358 return null; 359 } 360 361 // We don't want to notify that navigation for "about:blank" (or "about:blank" with parameter) 362 // is committed if it happens when the top-level browsing context is created. 363 if ( 364 navigation.state === NavigationState.InitialAboutBlank && 365 new URL(url).pathname == "blank" 366 ) { 367 lazy.logger.trace( 368 `[${navigableId}] Skipping this navigation for url: ${navigation.url}, since it's an initial navigation.` 369 ); 370 return navigation; 371 } 372 373 // Flag the navigation as committed. We don't set it as the state, because 374 // we need to know if at some point a navigation was committed, regardless 375 // of its current state (eg finished). 376 navigation.committed = true; 377 378 lazy.logger.trace( 379 lazy.truncate`[${navigableId}] Navigation committed for url: ${url} (${navigation.navigationId})` 380 ); 381 382 this.emit(NAVIGATION_EVENTS.NavigationCommitted, { 383 contextId: context.id, 384 errorName, 385 navigationId: navigation.navigationId, 386 navigableId, 387 url, 388 }); 389 390 return navigation; 391 } 392 393 /** 394 * Called when a navigation-failed event is recorded from the 395 * WebProgressListener actors. 396 * 397 * This entry point is only intended to be called from 398 * WebProgressListenerParent, to avoid setting up observers or listeners, 399 * which are unnecessary since NavigationManager has to be a singleton. 400 * 401 * @param {object} data 402 * @param {BrowsingContextDetails} data.contextDetails 403 * The details about the browsing context for this navigation. 404 * @param {string} data.errorName 405 * The error message. 406 * @param {string} data.url 407 * The URL as string for the navigation. 408 * @returns {NavigationInfo} 409 * The created navigation or the ongoing navigation, if applicable. 410 */ 411 notifyNavigationFailed(data) { 412 const { contextDetails, errorName, url } = data; 413 414 const context = this.#getContextFromContextDetails(contextDetails); 415 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 416 417 const navigation = this.#navigations.get(navigableId); 418 419 if (!navigation) { 420 lazy.logger.trace( 421 lazy.truncate`[${navigableId}] No navigation found to fail for url: ${url}` 422 ); 423 return null; 424 } 425 426 if (navigation.state === NavigationState.Finished) { 427 lazy.logger.trace( 428 `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}` 429 ); 430 return navigation; 431 } 432 433 lazy.logger.trace( 434 lazy.truncate`[${navigableId}] Navigation failed for url: ${url} (${navigation.navigationId})` 435 ); 436 437 navigation.state = NavigationState.Finished; 438 439 this.emit(NAVIGATION_EVENTS.NavigationFailed, { 440 contextId: context.id, 441 errorName, 442 navigationId: navigation.navigationId, 443 navigableId, 444 url, 445 }); 446 447 return navigation; 448 } 449 450 /** 451 * Called when a navigation-started event is recorded from the 452 * WebProgressListener actors. 453 * 454 * This entry point is only intended to be called from 455 * WebProgressListenerParent, to avoid setting up observers or listeners, 456 * which are unnecessary since NavigationManager has to be a singleton. 457 * 458 * @param {object} data 459 * @param {BrowsingContextDetails} data.contextDetails 460 * The details about the browsing context for this navigation. 461 * @param {string} data.url 462 * The URL as string for the navigation. 463 * @returns {NavigationInfo} 464 * The created navigation or the ongoing navigation, if applicable. 465 */ 466 notifyNavigationStarted(data) { 467 const { contextDetails, url } = data; 468 const context = this.#getContextFromContextDetails(contextDetails); 469 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 470 471 let navigation = this.#navigations.get(navigableId); 472 473 // For top-level navigations, `context` is the current browsing context for 474 // the browser with id = `contextDetails.browserId`. 475 // If the navigation replaced the browsing contexts, retrieve the original 476 // browsing context to check if the event is relevant. 477 const originalContext = BrowsingContext.get( 478 contextDetails.browsingContextId 479 ); 480 481 // If we have a previousNavigation for the same URL, and the browsing 482 // context for this event (originalContext) is outdated, skip the event. 483 // Any further event from this browsing context will come with the aborted 484 // flag set and will also be ignored. 485 // Bug 1930616: Moving the NavigationManager to the parent process should 486 // hopefully make this irrelevant. 487 if ( 488 url == navigation?.url && 489 context != originalContext && 490 !context.isReplaced && 491 originalContext?.isReplaced 492 ) { 493 return null; 494 } 495 496 if (navigation) { 497 if (navigation.state === NavigationState.Started) { 498 // Bug 1908952. As soon as we have support for the "url" field in case of beforeunload 499 // prompt being open, we can remove "!navigation.url" check. 500 if (!navigation.url || navigation.url === url) { 501 // If we are already monitoring a navigation for this navigable and the same url, 502 // for which we did not receive a navigation-stopped event, this navigation 503 // is already tracked and we don't want to create another id & event. 504 lazy.logger.trace( 505 `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}` 506 ); 507 return navigation; 508 } 509 510 lazy.logger.trace( 511 `[${navigableId}] We're going to fail the navigation for url: ${navigation.url} (${navigation.navigationId}), ` + 512 "since it was interrupted by a new navigation." 513 ); 514 515 // If there is already a navigation in progress but with a different url, 516 // it means that this navigation was interrupted by a new navigation. 517 // Note: ideally we should monitor this using NS_BINDING_ABORTED, 518 // but due to intermittent issues, when monitoring this in content processes, 519 // we can't reliable use it. 520 notifyNavigationFailed({ 521 contextDetails, 522 errorName: "A new navigation interrupted an unfinished navigation", 523 url: navigation.url, 524 }); 525 } 526 527 // We don't want to notify that navigation for "about:blank" (or "about:blank" with parameter) 528 // has started if it happens when the top-level browsing context is created. 529 if ( 530 navigation.state === NavigationState.InitialAboutBlank && 531 new URL(url).pathname == "blank" 532 ) { 533 lazy.logger.trace( 534 `[${navigableId}] Skipping this navigation for url: ${navigation.url}, since it's an initial navigation.` 535 ); 536 return navigation; 537 } 538 } 539 540 const navigationId = this.#getOrCreateNavigationId(navigableId); 541 navigation = this.#createNavigationObject({ 542 state: NavigationState.Started, 543 navigationId, 544 url, 545 }); 546 this.#navigations.set(navigableId, navigation); 547 548 lazy.logger.trace( 549 lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})` 550 ); 551 552 this.emit(NAVIGATION_EVENTS.NavigationStarted, { 553 contextId: context.id, 554 navigationId, 555 navigableId, 556 url, 557 }); 558 559 return navigation; 560 } 561 562 /** 563 * Called when a navigation-stopped event is recorded from the 564 * WebProgressListener actors. 565 * 566 * @param {object} data 567 * @param {BrowsingContextDetails} data.contextDetails 568 * The details about the browsing context for this navigation. 569 * @param {string} data.url 570 * The URL as string for the navigation. 571 * @returns {NavigationInfo} 572 * The stopped navigation if any, or null. 573 */ 574 notifyNavigationStopped(data) { 575 const { contextDetails, url } = data; 576 577 const context = this.#getContextFromContextDetails(contextDetails); 578 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 579 580 const navigation = this.#navigations.get(navigableId); 581 if (!navigation) { 582 lazy.logger.trace( 583 lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}` 584 ); 585 return null; 586 } 587 588 if (navigation.state === NavigationState.Finished) { 589 lazy.logger.trace( 590 `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}` 591 ); 592 return navigation; 593 } 594 595 lazy.logger.trace( 596 lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})` 597 ); 598 599 navigation.state = NavigationState.Finished; 600 601 this.emit(NAVIGATION_EVENTS.NavigationStopped, { 602 navigationId: navigation.navigationId, 603 navigableId, 604 url, 605 }); 606 607 return navigation; 608 } 609 610 /** 611 * Register a navigation id to be used for the next navigation for the 612 * provided browsing context details. 613 * 614 * @param {object} data 615 * @param {BrowsingContextDetails} data.contextDetails 616 * The details about the browsing context for this navigation. 617 * @returns {string} 618 * The UUID created the upcoming navigation. 619 */ 620 registerNavigationId(data) { 621 const { contextDetails } = data; 622 const context = this.#getContextFromContextDetails(contextDetails); 623 const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context); 624 625 const existingNavigation = this.#navigations.get(navigableId); 626 if ( 627 existingNavigation && 628 existingNavigation.state === NavigationState.Started 629 ) { 630 lazy.logger.trace( 631 `[${navigableId}] We're going to fail the navigation for url: ${existingNavigation.url} (${existingNavigation.navigationId}), ` + 632 "since it was interrupted by a new navigation." 633 ); 634 635 // If there is already a navigation in progress but with a different url, 636 // it means that this navigation was interrupted by a new navigation. 637 // Note: ideally we should monitor this using NS_BINDING_ABORTED, 638 // but due to intermittent issues, when monitoring this in content processes, 639 // we can't reliable use it. 640 notifyNavigationFailed({ 641 contextDetails, 642 errorName: "A new navigation interrupted an unfinished navigation", 643 url: existingNavigation.url, 644 }); 645 } 646 647 const navigationId = lazy.generateUUID(); 648 const navigation = this.#createNavigationObject({ 649 state: NavigationState.registered, 650 navigationId, 651 }); 652 this.#navigations.set(navigableId, navigation); 653 654 return navigationId; 655 } 656 657 #createNavigationObject(params) { 658 const { state, navigationId, url } = params; 659 return { 660 committed: false, 661 state, 662 navigationId, 663 url, 664 }; 665 } 666 667 #getContextFromContextDetails(contextDetails) { 668 if (contextDetails.context) { 669 return contextDetails.context; 670 } 671 672 return contextDetails.isContent && contextDetails.isTopBrowsingContext 673 ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId) 674 : BrowsingContext.get(contextDetails.browsingContextId); 675 } 676 677 #getOrCreateNavigationId(navigableId) { 678 const navigation = this.#navigations.get(navigableId); 679 if ( 680 navigation !== undefined && 681 navigation.state === NavigationState.registered 682 ) { 683 return navigation.navigationId; 684 } 685 return lazy.generateUUID(); 686 } 687 688 #onContextAttached = async (eventName, data) => { 689 const { browsingContext, why } = data; 690 691 // We only care about top-level browsing contexts. 692 if (browsingContext.parent !== null) { 693 return; 694 } 695 // Filter out top-level browsing contexts that are created because of a 696 // cross-group navigation. 697 if (why === "replace") { 698 return; 699 } 700 701 const navigableId = 702 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 703 let navigation = this.#navigations.get(navigableId); 704 705 if (navigation) { 706 return; 707 } 708 709 const navigationId = this.#getOrCreateNavigationId(navigableId); 710 navigation = { 711 state: NavigationState.InitialAboutBlank, 712 navigationId, 713 url: browsingContext.currentURI.displaySpec, 714 }; 715 this.#navigations.set(navigableId, navigation); 716 }; 717 718 #onContextDiscarded = async (eventName, data = {}) => { 719 const { browsingContext, why } = data; 720 721 // Filter out top-level browsing contexts that are destroyed because of a 722 // cross-group navigation. 723 if (why === "replace") { 724 return; 725 } 726 727 // TODO: Bug 1852941. We should also filter out events which are emitted 728 // for DevTools frames. 729 730 // Filter out notifications for chrome context until support gets 731 // added (bug 1722679). 732 if (!browsingContext.webProgress) { 733 return; 734 } 735 736 // Filter out notifications for webextension contexts until support gets 737 // added (bug 1755014). 738 if (browsingContext.currentRemoteType === "extension") { 739 return; 740 } 741 742 const navigableId = 743 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 744 const navigation = this.#navigations.get(navigableId); 745 746 // No need to fail navigation, if there is no navigation in progress. 747 if (!navigation) { 748 return; 749 } 750 751 notifyNavigationFailed({ 752 contextDetails: { 753 context: browsingContext, 754 }, 755 errorName: "Browsing context got discarded", 756 url: navigation.url, 757 }); 758 759 // If the navigable is discarded, we can safely clean up the navigation info. 760 this.#navigations.delete(navigableId); 761 }; 762 763 #onDownloadStarted = (eventName, data) => { 764 const { download } = data; 765 766 const contextId = download.source.browsingContextId; 767 const browsingContext = BrowsingContext.get(contextId); 768 if (!browsingContext) { 769 return; 770 } 771 772 const navigableId = 773 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 774 const url = download.source.url; 775 776 const navigation = this.#navigations.get(navigableId); 777 let navigationId = null; 778 if (navigation && navigation.state === NavigationState.Started) { 779 // navigationId is optional and should only be set if there is an ongoing 780 // navigation. 781 navigationId = navigation.navigationId; 782 // Track the navigation id for this download object, for the upcoming 783 // NAVIGATION_EVENTS.DownloadEnd event. 784 this.#downloadNavigations.set(download, navigationId); 785 } 786 787 // Tracking navigations is delegated to the DownloadListener. It is exposed 788 // via the DownloadManager for consistency and also to enforce having a 789 // singleton and consistent navigation ids across sessions. 790 this.emit(NAVIGATION_EVENTS.DownloadStarted, { 791 contextId: browsingContext.id, 792 navigationId, 793 navigableId, 794 suggestedFilename: PathUtils.filename(download.target.path), 795 timestamp: download.startTime.getTime(), 796 url, 797 }); 798 }; 799 800 #onDownloadStopped = (eventName, data) => { 801 const { download } = data; 802 803 const contextId = download.source.browsingContextId; 804 const browsingContext = BrowsingContext.get(contextId); 805 if (!browsingContext) { 806 return; 807 } 808 809 const navigableId = 810 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 811 const url = download.source.url; 812 813 let navigationId = null; 814 if (this.#downloadNavigations.has(download)) { 815 navigationId = this.#downloadNavigations.get(download); 816 } 817 818 const canceled = download.canceled || download.error; 819 this.emit(NAVIGATION_EVENTS.DownloadEnd, { 820 canceled, 821 contextId: browsingContext.id, 822 filepath: download.target.path, 823 navigableId, 824 navigationId, 825 timestamp: download.endTime, 826 url, 827 }); 828 }; 829 830 #onPromptClosed = (eventName, data) => { 831 const { contentBrowser, detail } = data; 832 const { accepted, browsingContext, promptType } = detail; 833 834 // Send navigation failed event if beforeunload prompt was rejected. 835 if (promptType === "beforeunload" && accepted === false) { 836 // TODO: Bug 2007385. We can remove this fallback 837 // when we have support for browsing context property in event details on Android. 838 const context = lazy.AppInfo.isAndroid 839 ? contentBrowser.browsingContext 840 : browsingContext; 841 notifyNavigationFailed({ 842 contextDetails: { 843 context, 844 }, 845 errorName: "Beforeunload prompt was rejected", 846 // Bug 1908952. Add support for the "url" field. 847 }); 848 } 849 }; 850 851 #onPromptOpened = (eventName, data) => { 852 const { browsingContext, contentBrowser, prompt } = data; 853 const { promptType } = prompt; 854 855 // We should start the navigation when beforeunload prompt is open. 856 if (promptType === "beforeunload") { 857 // TODO: Bug 2007385. We can remove this fallback 858 // when we have support for browsing context property in event details on Android. 859 const context = lazy.AppInfo.isAndroid 860 ? contentBrowser.browsingContext 861 : browsingContext; 862 notifyNavigationStarted({ 863 contextDetails: { 864 context, 865 }, 866 // Bug 1908952. Add support for the "url" field. 867 }); 868 } 869 }; 870 } 871 872 // Create a private NavigationRegistry singleton. 873 const navigationRegistry = new NavigationRegistry(); 874 875 /** 876 * See NavigationRegistry.notifyFragmentNavigated. 877 * 878 * This entry point is only intended to be called from WebProgressListenerParent, 879 * to avoid setting up observers or listeners, which are unnecessary since 880 * NavigationRegistry has to be a singleton. 881 */ 882 export function notifyFragmentNavigated(data) { 883 return navigationRegistry.notifyFragmentNavigated(data); 884 } 885 886 /** 887 * See NavigationRegistry.notifyHistoryUpdated. 888 * 889 * This entry point is only intended to be called from WebProgressListenerParent, 890 * to avoid setting up observers or listeners, which are unnecessary since 891 * NavigationRegistry has to be a singleton. 892 */ 893 export function notifyHistoryUpdated(data) { 894 return navigationRegistry.notifyHistoryUpdated(data); 895 } 896 897 /** 898 * See NavigationRegistry.notifySameDocumentChanged. 899 * 900 * This entry point is only intended to be called from WebProgressListenerParent, 901 * to avoid setting up observers or listeners, which are unnecessary since 902 * NavigationRegistry has to be a singleton. 903 */ 904 export function notifySameDocumentChanged(data) { 905 return navigationRegistry.notifySameDocumentChanged(data); 906 } 907 908 /** 909 * See NavigationRegistry.notifyNavigationCommitted. 910 * 911 * This entry point is only intended to be called from WebProgressListenerParent, 912 * to avoid setting up observers or listeners, which are unnecessary since 913 * NavigationRegistry has to be a singleton. 914 */ 915 export function notifyNavigationCommitted(data) { 916 return navigationRegistry.notifyNavigationCommitted(data); 917 } 918 919 /** 920 * See NavigationRegistry.notifyNavigationFailed. 921 * 922 * This entry point is only intended to be called from WebProgressListenerParent, 923 * to avoid setting up observers or listeners, which are unnecessary since 924 * NavigationRegistry has to be a singleton. 925 */ 926 export function notifyNavigationFailed(data) { 927 return navigationRegistry.notifyNavigationFailed(data); 928 } 929 930 /** 931 * See NavigationRegistry.notifyNavigationStarted. 932 * 933 * This entry point is only intended to be called from WebProgressListenerParent, 934 * to avoid setting up observers or listeners, which are unnecessary since 935 * NavigationRegistry has to be a singleton. 936 */ 937 export function notifyNavigationStarted(data) { 938 return navigationRegistry.notifyNavigationStarted(data); 939 } 940 941 /** 942 * See NavigationRegistry.notifyNavigationStopped. 943 * 944 * This entry point is only intended to be called from WebProgressListenerParent, 945 * to avoid setting up observers or listeners, which are unnecessary since 946 * NavigationRegistry has to be a singleton. 947 */ 948 export function notifyNavigationStopped(data) { 949 return navigationRegistry.notifyNavigationStopped(data); 950 } 951 952 export function registerNavigationId(data) { 953 return navigationRegistry.registerNavigationId(data); 954 } 955 956 /** 957 * The NavigationManager exposes the NavigationRegistry data via a class which 958 * needs to be individually instantiated by each consumer. This allow to track 959 * how many consumers need navigation data at any point so that the 960 * NavigationRegistry can register or unregister the underlying listeners/actors 961 * correctly. 962 * 963 * @fires NavigationManager#"navigation-started" 964 * The NavigationManager emits "navigation-started" when a new navigation is 965 * detected, with the following object as payload: 966 * - {string} navigationId - The UUID for the navigation. 967 * - {string} navigableId - The UUID for the navigable. 968 * - {string} url - The target url for the navigation. 969 * @fires NavigationManager#"navigation-stopped" 970 * The NavigationManager emits "navigation-stopped" when a known navigation 971 * is stopped, with the following object as payload: 972 * - {string} navigationId - The UUID for the navigation. 973 * - {string} navigableId - The UUID for the navigable. 974 * - {string} url - The target url for the navigation. 975 */ 976 export class NavigationManager extends EventEmitter { 977 #monitoring; 978 979 constructor() { 980 super(); 981 982 this.#monitoring = false; 983 } 984 985 destroy() { 986 this.stopMonitoring(); 987 } 988 989 getNavigationForBrowsingContext(context) { 990 return navigationRegistry.getNavigationForBrowsingContext(context); 991 } 992 993 startMonitoring() { 994 if (this.#monitoring) { 995 return; 996 } 997 998 this.#monitoring = true; 999 navigationRegistry.startMonitoring(this); 1000 for (const eventName of Object.values(NAVIGATION_EVENTS)) { 1001 navigationRegistry.on(eventName, this.#onNavigationEvent); 1002 } 1003 } 1004 1005 stopMonitoring() { 1006 if (!this.#monitoring) { 1007 return; 1008 } 1009 1010 this.#monitoring = false; 1011 navigationRegistry.stopMonitoring(this); 1012 for (const eventName of Object.values(NAVIGATION_EVENTS)) { 1013 navigationRegistry.off(eventName, this.#onNavigationEvent); 1014 } 1015 } 1016 1017 #onNavigationEvent = (eventName, data) => { 1018 this.emit(eventName, data); 1019 }; 1020 }