summaryrefslogtreecommitdiffstats
path: root/docshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /docshell
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'docshell')
-rw-r--r--docshell/base/BaseHistory.cpp247
-rw-r--r--docshell/base/BaseHistory.h85
-rw-r--r--docshell/base/BrowsingContext.cpp3905
-rw-r--r--docshell/base/BrowsingContext.h1469
-rw-r--r--docshell/base/BrowsingContextGroup.cpp570
-rw-r--r--docshell/base/BrowsingContextGroup.h318
-rw-r--r--docshell/base/BrowsingContextWebProgress.cpp443
-rw-r--r--docshell/base/BrowsingContextWebProgress.h103
-rw-r--r--docshell/base/CanonicalBrowsingContext.cpp3104
-rw-r--r--docshell/base/CanonicalBrowsingContext.h620
-rw-r--r--docshell/base/ChildProcessChannelListener.cpp61
-rw-r--r--docshell/base/ChildProcessChannelListener.h54
-rw-r--r--docshell/base/IHistory.h175
-rw-r--r--docshell/base/LoadContext.cpp236
-rw-r--r--docshell/base/LoadContext.h68
-rw-r--r--docshell/base/SerializedLoadContext.cpp87
-rw-r--r--docshell/base/SerializedLoadContext.h96
-rw-r--r--docshell/base/SyncedContext.h402
-rw-r--r--docshell/base/SyncedContextInlines.h359
-rw-r--r--docshell/base/URIFixup.sys.mjs1306
-rw-r--r--docshell/base/WindowContext.cpp697
-rw-r--r--docshell/base/WindowContext.h429
-rw-r--r--docshell/base/crashtests/1257730-1.html25
-rw-r--r--docshell/base/crashtests/1331295.html25
-rw-r--r--docshell/base/crashtests/1341657.html18
-rw-r--r--docshell/base/crashtests/1584467.html12
-rw-r--r--docshell/base/crashtests/1614211-1.html15
-rw-r--r--docshell/base/crashtests/1617315-1.html8
-rw-r--r--docshell/base/crashtests/1667491.html16
-rw-r--r--docshell/base/crashtests/1667491_1.html21
-rw-r--r--docshell/base/crashtests/1672873.html6
-rw-r--r--docshell/base/crashtests/1690169-1.html11
-rw-r--r--docshell/base/crashtests/1753136.html2
-rw-r--r--docshell/base/crashtests/1804803.html13
-rw-r--r--docshell/base/crashtests/1804803.sjs18
-rw-r--r--docshell/base/crashtests/369126-1.html16
-rw-r--r--docshell/base/crashtests/40929-1-inner.html14
-rw-r--r--docshell/base/crashtests/40929-1.html6
-rw-r--r--docshell/base/crashtests/430124-1.html5
-rw-r--r--docshell/base/crashtests/430628-1.html8
-rw-r--r--docshell/base/crashtests/432114-1.html8
-rw-r--r--docshell/base/crashtests/432114-2.html21
-rw-r--r--docshell/base/crashtests/436900-1-inner.html21
-rw-r--r--docshell/base/crashtests/436900-1.html8
-rw-r--r--docshell/base/crashtests/436900-2-inner.html21
-rw-r--r--docshell/base/crashtests/436900-2.html8
-rw-r--r--docshell/base/crashtests/443655.html15
-rw-r--r--docshell/base/crashtests/500328-1.html17
-rw-r--r--docshell/base/crashtests/514779-1.xhtml9
-rw-r--r--docshell/base/crashtests/614499-1.html20
-rw-r--r--docshell/base/crashtests/678872-1.html36
-rw-r--r--docshell/base/crashtests/914521.html38
-rw-r--r--docshell/base/crashtests/crashtests.list25
-rw-r--r--docshell/base/crashtests/file_432114-2.xhtml1
-rw-r--r--docshell/base/metrics.yaml29
-rw-r--r--docshell/base/moz.build126
-rw-r--r--docshell/base/nsAboutRedirector.cpp318
-rw-r--r--docshell/base/nsAboutRedirector.h26
-rw-r--r--docshell/base/nsCTooltipTextProvider.h15
-rw-r--r--docshell/base/nsDSURIContentListener.cpp297
-rw-r--r--docshell/base/nsDSURIContentListener.h100
-rw-r--r--docshell/base/nsDocShell.cpp13763
-rw-r--r--docshell/base/nsDocShell.h1366
-rw-r--r--docshell/base/nsDocShellEditorData.cpp139
-rw-r--r--docshell/base/nsDocShellEditorData.h66
-rw-r--r--docshell/base/nsDocShellEnumerator.cpp85
-rw-r--r--docshell/base/nsDocShellEnumerator.h39
-rw-r--r--docshell/base/nsDocShellLoadState.cpp1325
-rw-r--r--docshell/base/nsDocShellLoadState.h609
-rw-r--r--docshell/base/nsDocShellLoadTypes.h205
-rw-r--r--docshell/base/nsDocShellTelemetryUtils.cpp202
-rw-r--r--docshell/base/nsDocShellTelemetryUtils.h22
-rw-r--r--docshell/base/nsDocShellTreeOwner.cpp1337
-rw-r--r--docshell/base/nsDocShellTreeOwner.h111
-rw-r--r--docshell/base/nsIDocShell.idl766
-rw-r--r--docshell/base/nsIDocShellTreeItem.idl171
-rw-r--r--docshell/base/nsIDocShellTreeOwner.idl113
-rw-r--r--docshell/base/nsIDocumentLoaderFactory.idl39
-rw-r--r--docshell/base/nsIDocumentViewer.idl318
-rw-r--r--docshell/base/nsIDocumentViewerEdit.idl36
-rw-r--r--docshell/base/nsILoadContext.idl148
-rw-r--r--docshell/base/nsILoadURIDelegate.idl35
-rw-r--r--docshell/base/nsIPrivacyTransitionObserver.idl11
-rw-r--r--docshell/base/nsIReflowObserver.idl31
-rw-r--r--docshell/base/nsIRefreshURI.idl52
-rw-r--r--docshell/base/nsIScrollObserver.h45
-rw-r--r--docshell/base/nsITooltipListener.idl44
-rw-r--r--docshell/base/nsITooltipTextProvider.idl44
-rw-r--r--docshell/base/nsIURIFixup.idl204
-rw-r--r--docshell/base/nsIWebNavigation.idl415
-rw-r--r--docshell/base/nsIWebNavigationInfo.idl55
-rw-r--r--docshell/base/nsIWebPageDescriptor.idl30
-rw-r--r--docshell/base/nsPingListener.cpp345
-rw-r--r--docshell/base/nsPingListener.h48
-rw-r--r--docshell/base/nsRefreshTimer.cpp49
-rw-r--r--docshell/base/nsRefreshTimer.h39
-rw-r--r--docshell/base/nsWebNavigationInfo.cpp64
-rw-r--r--docshell/base/nsWebNavigationInfo.h34
-rw-r--r--docshell/build/components.conf194
-rw-r--r--docshell/build/moz.build25
-rw-r--r--docshell/build/nsDocShellCID.h59
-rw-r--r--docshell/build/nsDocShellModule.cpp25
-rw-r--r--docshell/build/nsDocShellModule.h20
-rw-r--r--docshell/moz.build48
-rw-r--r--docshell/shistory/ChildSHistory.cpp294
-rw-r--r--docshell/shistory/ChildSHistory.h149
-rw-r--r--docshell/shistory/SessionHistoryEntry.cpp1831
-rw-r--r--docshell/shistory/SessionHistoryEntry.h510
-rw-r--r--docshell/shistory/moz.build42
-rw-r--r--docshell/shistory/nsIBFCacheEntry.idl16
-rw-r--r--docshell/shistory/nsISHEntry.idl476
-rw-r--r--docshell/shistory/nsISHistory.idl291
-rw-r--r--docshell/shistory/nsISHistoryListener.idl88
-rw-r--r--docshell/shistory/nsSHEntry.cpp1131
-rw-r--r--docshell/shistory/nsSHEntry.h72
-rw-r--r--docshell/shistory/nsSHEntryShared.cpp365
-rw-r--r--docshell/shistory/nsSHEntryShared.h219
-rw-r--r--docshell/shistory/nsSHistory.cpp2410
-rw-r--r--docshell/shistory/nsSHistory.h343
-rw-r--r--docshell/test/browser/Bug1622420Child.sys.mjs9
-rw-r--r--docshell/test/browser/Bug422543Child.sys.mjs98
-rw-r--r--docshell/test/browser/browser.toml326
-rw-r--r--docshell/test/browser/browser_alternate_fixup_middle_click_link.js52
-rw-r--r--docshell/test/browser/browser_backforward_restore_scroll.js54
-rw-r--r--docshell/test/browser/browser_backforward_userinteraction.js374
-rw-r--r--docshell/test/browser/browser_backforward_userinteraction_about.js67
-rw-r--r--docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js112
-rw-r--r--docshell/test/browser/browser_badCertDomainFixup.js92
-rw-r--r--docshell/test/browser/browser_bfcache_copycommand.js98
-rw-r--r--docshell/test/browser/browser_browsingContext-01.js180
-rw-r--r--docshell/test/browser/browser_browsingContext-02.js235
-rw-r--r--docshell/test/browser/browser_browsingContext-embedder.js156
-rw-r--r--docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js53
-rw-r--r--docshell/test/browser/browser_browsingContext-getWindowByName.js34
-rw-r--r--docshell/test/browser/browser_browsingContext-webProgress.js226
-rw-r--r--docshell/test/browser/browser_browsing_context_attached.js179
-rw-r--r--docshell/test/browser/browser_browsing_context_discarded.js65
-rw-r--r--docshell/test/browser/browser_bug1206879.js50
-rw-r--r--docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js54
-rw-r--r--docshell/test/browser/browser_bug1328501.js69
-rw-r--r--docshell/test/browser/browser_bug1347823.js91
-rw-r--r--docshell/test/browser/browser_bug134911.js57
-rw-r--r--docshell/test/browser/browser_bug1415918_beforeunload_options.js162
-rw-r--r--docshell/test/browser/browser_bug1543077-3.js46
-rw-r--r--docshell/test/browser/browser_bug1594938.js100
-rw-r--r--docshell/test/browser/browser_bug1622420.js31
-rw-r--r--docshell/test/browser/browser_bug1648464-1.js46
-rw-r--r--docshell/test/browser/browser_bug1673702.js27
-rw-r--r--docshell/test/browser/browser_bug1674464.js38
-rw-r--r--docshell/test/browser/browser_bug1688368-1.js24
-rw-r--r--docshell/test/browser/browser_bug1691153.js73
-rw-r--r--docshell/test/browser/browser_bug1705872.js74
-rw-r--r--docshell/test/browser/browser_bug1716290-1.js24
-rw-r--r--docshell/test/browser/browser_bug1716290-2.js24
-rw-r--r--docshell/test/browser/browser_bug1716290-3.js24
-rw-r--r--docshell/test/browser/browser_bug1716290-4.js24
-rw-r--r--docshell/test/browser/browser_bug1719178.js34
-rw-r--r--docshell/test/browser/browser_bug1736248-1.js34
-rw-r--r--docshell/test/browser/browser_bug1757005.js73
-rw-r--r--docshell/test/browser/browser_bug1769189.js32
-rw-r--r--docshell/test/browser/browser_bug1798780.js52
-rw-r--r--docshell/test/browser/browser_bug234628-1.js47
-rw-r--r--docshell/test/browser/browser_bug234628-10.js46
-rw-r--r--docshell/test/browser/browser_bug234628-11.js46
-rw-r--r--docshell/test/browser/browser_bug234628-2.js49
-rw-r--r--docshell/test/browser/browser_bug234628-3.js47
-rw-r--r--docshell/test/browser/browser_bug234628-4.js46
-rw-r--r--docshell/test/browser/browser_bug234628-5.js46
-rw-r--r--docshell/test/browser/browser_bug234628-6.js47
-rw-r--r--docshell/test/browser/browser_bug234628-8.js18
-rw-r--r--docshell/test/browser/browser_bug234628-9.js18
-rw-r--r--docshell/test/browser/browser_bug349769.js79
-rw-r--r--docshell/test/browser/browser_bug388121-1.js22
-rw-r--r--docshell/test/browser/browser_bug388121-2.js74
-rw-r--r--docshell/test/browser/browser_bug420605.js131
-rw-r--r--docshell/test/browser/browser_bug422543.js253
-rw-r--r--docshell/test/browser/browser_bug441169.js44
-rw-r--r--docshell/test/browser/browser_bug503832.js76
-rw-r--r--docshell/test/browser/browser_bug554155.js33
-rw-r--r--docshell/test/browser/browser_bug655270.js62
-rw-r--r--docshell/test/browser/browser_bug655273.js56
-rw-r--r--docshell/test/browser/browser_bug670318.js146
-rw-r--r--docshell/test/browser/browser_bug673087-1.js46
-rw-r--r--docshell/test/browser/browser_bug673087-2.js36
-rw-r--r--docshell/test/browser/browser_bug673467.js62
-rw-r--r--docshell/test/browser/browser_bug852909.js35
-rw-r--r--docshell/test/browser/browser_bug92473.js70
-rw-r--r--docshell/test/browser/browser_click_link_within_view_source.js78
-rw-r--r--docshell/test/browser/browser_cross_process_csp_inheritance.js125
-rw-r--r--docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js55
-rw-r--r--docshell/test/browser/browser_csp_uir.js89
-rw-r--r--docshell/test/browser/browser_dataURI_unique_opaque_origin.js30
-rw-r--r--docshell/test/browser/browser_data_load_inherit_csp.js110
-rw-r--r--docshell/test/browser/browser_fall_back_to_https.js82
-rw-r--r--docshell/test/browser/browser_frameloader_swap_with_bfcache.js37
-rw-r--r--docshell/test/browser/browser_history_triggeringprincipal_viewsource.js88
-rw-r--r--docshell/test/browser/browser_isInitialDocument.js319
-rw-r--r--docshell/test/browser/browser_loadURI_postdata.js42
-rw-r--r--docshell/test/browser/browser_multiple_pushState.js25
-rw-r--r--docshell/test/browser/browser_onbeforeunload_frame.js45
-rw-r--r--docshell/test/browser/browser_onbeforeunload_navigation.js165
-rw-r--r--docshell/test/browser/browser_onbeforeunload_parent.js48
-rw-r--r--docshell/test/browser/browser_onunload_stop.js23
-rw-r--r--docshell/test/browser/browser_overlink.js33
-rw-r--r--docshell/test/browser/browser_platform_emulation.js70
-rw-r--r--docshell/test/browser/browser_search_notification.js49
-rw-r--r--docshell/test/browser/browser_tab_replace_while_loading.js83
-rw-r--r--docshell/test/browser/browser_tab_touch_events.js75
-rw-r--r--docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js285
-rw-r--r--docshell/test/browser/browser_title_in_session_history.js63
-rw-r--r--docshell/test/browser/browser_ua_emulation.js71
-rw-r--r--docshell/test/browser/browser_uriFixupAlternateRedirects.js66
-rw-r--r--docshell/test/browser/browser_uriFixupIntegration.js104
-rw-r--r--docshell/test/browser/browser_viewsource_chrome_to_content.js20
-rw-r--r--docshell/test/browser/browser_viewsource_multipart.js47
-rw-r--r--docshell/test/browser/dummy_iframe_page.html8
-rw-r--r--docshell/test/browser/dummy_page.html6
-rw-r--r--docshell/test/browser/favicon_bug655270.icobin0 -> 1406 bytes
-rw-r--r--docshell/test/browser/file_backforward_restore_scroll.html10
-rw-r--r--docshell/test/browser/file_backforward_restore_scroll.html^headers^1
-rw-r--r--docshell/test/browser/file_basic_multipart.sjs24
-rw-r--r--docshell/test/browser/file_bug1046022.html54
-rw-r--r--docshell/test/browser/file_bug1206879.html9
-rw-r--r--docshell/test/browser/file_bug1328501.html27
-rw-r--r--docshell/test/browser/file_bug1328501_frame.html4
-rw-r--r--docshell/test/browser/file_bug1328501_framescript.js38
-rw-r--r--docshell/test/browser/file_bug1543077-3-child.html11
-rw-r--r--docshell/test/browser/file_bug1543077-3.html16
-rw-r--r--docshell/test/browser/file_bug1622420.html1
-rw-r--r--docshell/test/browser/file_bug1648464-1-child.html13
-rw-r--r--docshell/test/browser/file_bug1648464-1.html18
-rw-r--r--docshell/test/browser/file_bug1673702.json1
-rw-r--r--docshell/test/browser/file_bug1673702.json^headers^1
-rw-r--r--docshell/test/browser/file_bug1688368-1.sjs44
-rw-r--r--docshell/test/browser/file_bug1691153.html27
-rw-r--r--docshell/test/browser/file_bug1716290-1.sjs21
-rw-r--r--docshell/test/browser/file_bug1716290-2.sjs18
-rw-r--r--docshell/test/browser/file_bug1716290-3.sjs17
-rw-r--r--docshell/test/browser/file_bug1716290-4.sjs17
-rw-r--r--docshell/test/browser/file_bug1736248-1.html4
-rw-r--r--docshell/test/browser/file_bug234628-1-child.html12
-rw-r--r--docshell/test/browser/file_bug234628-1.html17
-rw-r--r--docshell/test/browser/file_bug234628-10-child.xhtml4
-rw-r--r--docshell/test/browser/file_bug234628-10.html17
-rw-r--r--docshell/test/browser/file_bug234628-11-child.xhtml4
-rw-r--r--docshell/test/browser/file_bug234628-11-child.xhtml^headers^1
-rw-r--r--docshell/test/browser/file_bug234628-11.html17
-rw-r--r--docshell/test/browser/file_bug234628-2-child.html12
-rw-r--r--docshell/test/browser/file_bug234628-2.html17
-rw-r--r--docshell/test/browser/file_bug234628-3-child.html13
-rw-r--r--docshell/test/browser/file_bug234628-3.html18
-rw-r--r--docshell/test/browser/file_bug234628-4-child.html12
-rw-r--r--docshell/test/browser/file_bug234628-4.html18
-rw-r--r--docshell/test/browser/file_bug234628-5-child.htmlbin0 -> 498 bytes
-rw-r--r--docshell/test/browser/file_bug234628-5.html18
-rw-r--r--docshell/test/browser/file_bug234628-6-child.htmlbin0 -> 540 bytes
-rw-r--r--docshell/test/browser/file_bug234628-6-child.html^headers^1
-rw-r--r--docshell/test/browser/file_bug234628-6.html18
-rw-r--r--docshell/test/browser/file_bug234628-8-child.html12
-rw-r--r--docshell/test/browser/file_bug234628-8.html17
-rw-r--r--docshell/test/browser/file_bug234628-9-child.html12
-rw-r--r--docshell/test/browser/file_bug234628-9.htmlbin0 -> 740 bytes
-rw-r--r--docshell/test/browser/file_bug420605.html31
-rw-r--r--docshell/test/browser/file_bug503832.html35
-rw-r--r--docshell/test/browser/file_bug655270.html11
-rw-r--r--docshell/test/browser/file_bug670318.html23
-rw-r--r--docshell/test/browser/file_bug673087-1-child.html13
-rw-r--r--docshell/test/browser/file_bug673087-1.htmlbin0 -> 432 bytes
-rw-r--r--docshell/test/browser/file_bug673087-1.html^headers^1
-rw-r--r--docshell/test/browser/file_bug673087-2.html2
-rw-r--r--docshell/test/browser/file_bug852909.pdfbin0 -> 1568 bytes
-rw-r--r--docshell/test/browser/file_bug852909.pngbin0 -> 94 bytes
-rw-r--r--docshell/test/browser/file_click_link_within_view_source.html6
-rw-r--r--docshell/test/browser/file_cross_process_csp_inheritance.html11
-rw-r--r--docshell/test/browser/file_csp_sandbox_no_script_js_uri.html11
-rw-r--r--docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^1
-rw-r--r--docshell/test/browser/file_csp_uir.html11
-rw-r--r--docshell/test/browser/file_csp_uir_dummy.html1
-rw-r--r--docshell/test/browser/file_data_load_inherit_csp.html11
-rw-r--r--docshell/test/browser/file_multiple_pushState.html20
-rw-r--r--docshell/test/browser/file_onbeforeunload_0.html9
-rw-r--r--docshell/test/browser/file_onbeforeunload_1.html9
-rw-r--r--docshell/test/browser/file_onbeforeunload_2.html10
-rw-r--r--docshell/test/browser/file_onbeforeunload_3.html9
-rw-r--r--docshell/test/browser/file_open_about_blank.html2
-rw-r--r--docshell/test/browser/file_slow_load.sjs8
-rw-r--r--docshell/test/browser/head.js197
-rw-r--r--docshell/test/browser/head_browser_onbeforeunload.js271
-rw-r--r--docshell/test/browser/onload_message.html25
-rw-r--r--docshell/test/browser/onpageshow_message.html41
-rw-r--r--docshell/test/browser/overlink_test.html7
-rw-r--r--docshell/test/browser/print_postdata.sjs25
-rw-r--r--docshell/test/browser/redirect_to_example.sjs5
-rw-r--r--docshell/test/browser/test-form_sjis.html24
-rw-r--r--docshell/test/chrome/112564_nocache.html10
-rw-r--r--docshell/test/chrome/112564_nocache.html^headers^1
-rw-r--r--docshell/test/chrome/215405_nocache.html14
-rw-r--r--docshell/test/chrome/215405_nocache.html^headers^1
-rw-r--r--docshell/test/chrome/215405_nostore.html14
-rw-r--r--docshell/test/chrome/215405_nostore.html^headers^1
-rw-r--r--docshell/test/chrome/582176_dummy.html1
-rw-r--r--docshell/test/chrome/582176_xml.xml2
-rw-r--r--docshell/test/chrome/582176_xslt.xsl8
-rw-r--r--docshell/test/chrome/662200a.html8
-rw-r--r--docshell/test/chrome/662200b.html8
-rw-r--r--docshell/test/chrome/662200c.html7
-rw-r--r--docshell/test/chrome/89419.html7
-rw-r--r--docshell/test/chrome/92598_nostore.html10
-rw-r--r--docshell/test/chrome/92598_nostore.html^headers^1
-rw-r--r--docshell/test/chrome/DocShellHelpers.sys.mjs74
-rw-r--r--docshell/test/chrome/allowContentRetargeting.sjs7
-rw-r--r--docshell/test/chrome/blue.pngbin0 -> 2745 bytes
-rw-r--r--docshell/test/chrome/bug112564_window.xhtml86
-rw-r--r--docshell/test/chrome/bug113934_window.xhtml156
-rw-r--r--docshell/test/chrome/bug215405_window.xhtml179
-rw-r--r--docshell/test/chrome/bug293235.html13
-rw-r--r--docshell/test/chrome/bug293235_p2.html8
-rw-r--r--docshell/test/chrome/bug293235_window.xhtml120
-rw-r--r--docshell/test/chrome/bug294258_testcase.html43
-rw-r--r--docshell/test/chrome/bug294258_window.xhtml72
-rw-r--r--docshell/test/chrome/bug298622_window.xhtml135
-rw-r--r--docshell/test/chrome/bug301397_1.html9
-rw-r--r--docshell/test/chrome/bug301397_2.html10
-rw-r--r--docshell/test/chrome/bug301397_3.html10
-rw-r--r--docshell/test/chrome/bug301397_4.html9
-rw-r--r--docshell/test/chrome/bug301397_window.xhtml218
-rw-r--r--docshell/test/chrome/bug303267.html23
-rw-r--r--docshell/test/chrome/bug303267_window.xhtml83
-rw-r--r--docshell/test/chrome/bug311007_window.xhtml204
-rw-r--r--docshell/test/chrome/bug321671_window.xhtml128
-rw-r--r--docshell/test/chrome/bug360511_case1.html15
-rw-r--r--docshell/test/chrome/bug360511_case2.html15
-rw-r--r--docshell/test/chrome/bug360511_window.xhtml127
-rw-r--r--docshell/test/chrome/bug364461_window.xhtml253
-rw-r--r--docshell/test/chrome/bug396519_window.xhtml132
-rw-r--r--docshell/test/chrome/bug396649_window.xhtml119
-rw-r--r--docshell/test/chrome/bug449778_window.xhtml107
-rw-r--r--docshell/test/chrome/bug449780_window.xhtml83
-rw-r--r--docshell/test/chrome/bug454235-subframe.xhtml7
-rw-r--r--docshell/test/chrome/bug582176_window.xhtml74
-rw-r--r--docshell/test/chrome/bug608669.xhtml14
-rw-r--r--docshell/test/chrome/bug662200_window.xhtml119
-rw-r--r--docshell/test/chrome/bug690056_window.xhtml173
-rw-r--r--docshell/test/chrome/bug846906.html10
-rw-r--r--docshell/test/chrome/bug89419.sjs12
-rw-r--r--docshell/test/chrome/bug89419_window.xhtml69
-rw-r--r--docshell/test/chrome/bug909218.html11
-rw-r--r--docshell/test/chrome/bug909218.js2
-rw-r--r--docshell/test/chrome/bug92598_window.xhtml89
-rw-r--r--docshell/test/chrome/chrome.toml142
-rw-r--r--docshell/test/chrome/docshell_helpers.js759
-rw-r--r--docshell/test/chrome/file_viewsource_forbidden_in_iframe.html11
-rw-r--r--docshell/test/chrome/gen_template.pl39
-rw-r--r--docshell/test/chrome/generic.html12
-rw-r--r--docshell/test/chrome/mozFrameType_window.xhtml49
-rw-r--r--docshell/test/chrome/red.pngbin0 -> 82 bytes
-rw-r--r--docshell/test/chrome/test.template.txt41
-rw-r--r--docshell/test/chrome/test_allowContentRetargeting.html76
-rw-r--r--docshell/test/chrome/test_bug112564.xhtml37
-rw-r--r--docshell/test/chrome/test_bug113934.xhtml29
-rw-r--r--docshell/test/chrome/test_bug215405.xhtml37
-rw-r--r--docshell/test/chrome/test_bug293235.xhtml38
-rw-r--r--docshell/test/chrome/test_bug294258.xhtml38
-rw-r--r--docshell/test/chrome/test_bug298622.xhtml38
-rw-r--r--docshell/test/chrome/test_bug301397.xhtml38
-rw-r--r--docshell/test/chrome/test_bug303267.xhtml39
-rw-r--r--docshell/test/chrome/test_bug311007.xhtml42
-rw-r--r--docshell/test/chrome/test_bug321671.xhtml38
-rw-r--r--docshell/test/chrome/test_bug360511.xhtml39
-rw-r--r--docshell/test/chrome/test_bug364461.xhtml43
-rw-r--r--docshell/test/chrome/test_bug396519.xhtml28
-rw-r--r--docshell/test/chrome/test_bug396649.xhtml41
-rw-r--r--docshell/test/chrome/test_bug428288.html37
-rw-r--r--docshell/test/chrome/test_bug449778.xhtml29
-rw-r--r--docshell/test/chrome/test_bug449780.xhtml29
-rw-r--r--docshell/test/chrome/test_bug453650.xhtml120
-rw-r--r--docshell/test/chrome/test_bug454235.xhtml40
-rw-r--r--docshell/test/chrome/test_bug456980.xhtml29
-rw-r--r--docshell/test/chrome/test_bug565388.xhtml79
-rw-r--r--docshell/test/chrome/test_bug582176.xhtml38
-rw-r--r--docshell/test/chrome/test_bug608669.xhtml80
-rw-r--r--docshell/test/chrome/test_bug662200.xhtml38
-rw-r--r--docshell/test/chrome/test_bug690056.xhtml26
-rw-r--r--docshell/test/chrome/test_bug789773.xhtml69
-rw-r--r--docshell/test/chrome/test_bug846906.xhtml94
-rw-r--r--docshell/test/chrome/test_bug89419.xhtml38
-rw-r--r--docshell/test/chrome/test_bug909218.html117
-rw-r--r--docshell/test/chrome/test_bug92598.xhtml37
-rw-r--r--docshell/test/chrome/test_docRedirect.sjs6
-rw-r--r--docshell/test/chrome/test_docRedirect.xhtml91
-rw-r--r--docshell/test/chrome/test_mozFrameType.xhtml42
-rw-r--r--docshell/test/chrome/test_open_and_immediately_close_opener.html54
-rw-r--r--docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml159
-rw-r--r--docshell/test/chrome/window.template.txt44
-rw-r--r--docshell/test/iframesandbox/file_child_navigation_by_location.html1
-rw-r--r--docshell/test/iframesandbox/file_marquee_event_handlers.html17
-rw-r--r--docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html15
-rw-r--r--docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html15
-rw-r--r--docshell/test/iframesandbox/file_parent_navigation_by_location.html18
-rw-r--r--docshell/test/iframesandbox/file_sibling_navigation_by_location.html15
-rw-r--r--docshell/test/iframesandbox/file_top_navigation_by_location.html20
-rw-r--r--docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html27
-rw-r--r--docshell/test/iframesandbox/file_top_navigation_by_user_activation.html27
-rw-r--r--docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html32
-rw-r--r--docshell/test/iframesandbox/mochitest.toml43
-rw-r--r--docshell/test/iframesandbox/test_child_navigation_by_location.html91
-rw-r--r--docshell/test/iframesandbox/test_marquee_event_handlers.html95
-rw-r--r--docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html80
-rw-r--r--docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html84
-rw-r--r--docshell/test/iframesandbox/test_parent_navigation_by_location.html75
-rw-r--r--docshell/test/iframesandbox/test_sibling_navigation_by_location.html78
-rw-r--r--docshell/test/iframesandbox/test_top_navigation_by_location.html167
-rw-r--r--docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html204
-rw-r--r--docshell/test/iframesandbox/test_top_navigation_by_user_activation.html74
-rw-r--r--docshell/test/mochitest/bug1422334_redirect.html3
-rw-r--r--docshell/test/mochitest/bug1422334_redirect.html^headers^2
-rw-r--r--docshell/test/mochitest/bug404548-subframe.html7
-rw-r--r--docshell/test/mochitest/bug404548-subframe_window.html1
-rw-r--r--docshell/test/mochitest/bug413310-post.sjs10
-rw-r--r--docshell/test/mochitest/bug413310-subframe.html7
-rw-r--r--docshell/test/mochitest/bug529119-window.html7
-rw-r--r--docshell/test/mochitest/bug530396-noref.sjs22
-rw-r--r--docshell/test/mochitest/bug530396-subframe.html7
-rw-r--r--docshell/test/mochitest/bug570341_recordevents.html21
-rw-r--r--docshell/test/mochitest/bug668513_redirect.html1
-rw-r--r--docshell/test/mochitest/bug668513_redirect.html^headers^2
-rw-r--r--docshell/test/mochitest/bug691547_frame.html12
-rw-r--r--docshell/test/mochitest/clicker.html7
-rw-r--r--docshell/test/mochitest/double_submit.sjs78
-rw-r--r--docshell/test/mochitest/dummy_page.html6
-rw-r--r--docshell/test/mochitest/file_anchor_scroll_after_document_open.html15
-rw-r--r--docshell/test/mochitest/file_bfcache_plus_hash_1.html24
-rw-r--r--docshell/test/mochitest/file_bfcache_plus_hash_2.html17
-rw-r--r--docshell/test/mochitest/file_bug1121701_1.html27
-rw-r--r--docshell/test/mochitest/file_bug1121701_2.html23
-rw-r--r--docshell/test/mochitest/file_bug1151421.html19
-rw-r--r--docshell/test/mochitest/file_bug1186774.html1
-rw-r--r--docshell/test/mochitest/file_bug1450164.html16
-rw-r--r--docshell/test/mochitest/file_bug1729662.html8
-rw-r--r--docshell/test/mochitest/file_bug1740516_1.html29
-rw-r--r--docshell/test/mochitest/file_bug1740516_1_inner.html15
-rw-r--r--docshell/test/mochitest/file_bug1740516_2.html11
-rw-r--r--docshell/test/mochitest/file_bug1741132.html29
-rw-r--r--docshell/test/mochitest/file_bug1742865.sjs75
-rw-r--r--docshell/test/mochitest/file_bug1742865_outer.sjs23
-rw-r--r--docshell/test/mochitest/file_bug1743353.html37
-rw-r--r--docshell/test/mochitest/file_bug1747033.sjs110
-rw-r--r--docshell/test/mochitest/file_bug1773192_1.html13
-rw-r--r--docshell/test/mochitest/file_bug1773192_2.html19
-rw-r--r--docshell/test/mochitest/file_bug1773192_3.sjs3
-rw-r--r--docshell/test/mochitest/file_bug1850335_1.html23
-rw-r--r--docshell/test/mochitest/file_bug1850335_2.html18
-rw-r--r--docshell/test/mochitest/file_bug1850335_3.html7
-rw-r--r--docshell/test/mochitest/file_bug385434_1.html29
-rw-r--r--docshell/test/mochitest/file_bug385434_2.html26
-rw-r--r--docshell/test/mochitest/file_bug385434_3.html22
-rw-r--r--docshell/test/mochitest/file_bug475636.sjs97
-rw-r--r--docshell/test/mochitest/file_bug509055.html9
-rw-r--r--docshell/test/mochitest/file_bug511449.html6
-rw-r--r--docshell/test/mochitest/file_bug540462.html25
-rw-r--r--docshell/test/mochitest/file_bug580069_1.html8
-rw-r--r--docshell/test/mochitest/file_bug580069_2.sjs8
-rw-r--r--docshell/test/mochitest/file_bug590573_1.html7
-rw-r--r--docshell/test/mochitest/file_bug590573_2.html8
-rw-r--r--docshell/test/mochitest/file_bug598895_1.html1
-rw-r--r--docshell/test/mochitest/file_bug598895_2.html1
-rw-r--r--docshell/test/mochitest/file_bug634834.html5
-rw-r--r--docshell/test/mochitest/file_bug637644_1.html1
-rw-r--r--docshell/test/mochitest/file_bug637644_2.html1
-rw-r--r--docshell/test/mochitest/file_bug640387.html26
-rw-r--r--docshell/test/mochitest/file_bug653741.html13
-rw-r--r--docshell/test/mochitest/file_bug66040413
-rw-r--r--docshell/test/mochitest/file_bug660404-1.html12
-rw-r--r--docshell/test/mochitest/file_bug660404^headers^1
-rw-r--r--docshell/test/mochitest/file_bug662170.html13
-rw-r--r--docshell/test/mochitest/file_bug668513.html101
-rw-r--r--docshell/test/mochitest/file_bug669671.sjs17
-rw-r--r--docshell/test/mochitest/file_bug675587.html1
-rw-r--r--docshell/test/mochitest/file_bug680257.html16
-rw-r--r--docshell/test/mochitest/file_bug703855.html2
-rw-r--r--docshell/test/mochitest/file_bug728939.html3
-rw-r--r--docshell/test/mochitest/file_close_onpagehide1.html5
-rw-r--r--docshell/test/mochitest/file_close_onpagehide2.html5
-rw-r--r--docshell/test/mochitest/file_compressed_multipartbin0 -> 111 bytes
-rw-r--r--docshell/test/mochitest/file_compressed_multipart^headers^2
-rw-r--r--docshell/test/mochitest/file_content_javascript_loads_frame.html17
-rw-r--r--docshell/test/mochitest/file_content_javascript_loads_root.html42
-rw-r--r--docshell/test/mochitest/file_form_restoration_no_store.html38
-rw-r--r--docshell/test/mochitest/file_form_restoration_no_store.html^headers^1
-rw-r--r--docshell/test/mochitest/file_framedhistoryframes.html16
-rw-r--r--docshell/test/mochitest/file_load_during_reload.html12
-rw-r--r--docshell/test/mochitest/file_pushState_after_document_open.html11
-rw-r--r--docshell/test/mochitest/file_redirect_history.html18
-rw-r--r--docshell/test/mochitest/form_submit.sjs40
-rw-r--r--docshell/test/mochitest/form_submit_redirect.sjs13
-rw-r--r--docshell/test/mochitest/historyframes.html176
-rw-r--r--docshell/test/mochitest/mochitest.toml321
-rw-r--r--docshell/test/mochitest/ping.html6
-rw-r--r--docshell/test/mochitest/start_historyframe.html1
-rw-r--r--docshell/test/mochitest/test_anchor_scroll_after_document_open.html55
-rw-r--r--docshell/test/mochitest/test_bfcache_plus_hash.html153
-rw-r--r--docshell/test/mochitest/test_bug1045096.html29
-rw-r--r--docshell/test/mochitest/test_bug1121701.html108
-rw-r--r--docshell/test/mochitest/test_bug1151421.html61
-rw-r--r--docshell/test/mochitest/test_bug1186774.html51
-rw-r--r--docshell/test/mochitest/test_bug1422334.html40
-rw-r--r--docshell/test/mochitest/test_bug1450164.html31
-rw-r--r--docshell/test/mochitest/test_bug1507702.html57
-rw-r--r--docshell/test/mochitest/test_bug1645781.html90
-rw-r--r--docshell/test/mochitest/test_bug1729662.html76
-rw-r--r--docshell/test/mochitest/test_bug1740516.html79
-rw-r--r--docshell/test/mochitest/test_bug1741132.html79
-rw-r--r--docshell/test/mochitest/test_bug1742865.html137
-rw-r--r--docshell/test/mochitest/test_bug1743353.html57
-rw-r--r--docshell/test/mochitest/test_bug1747033.html97
-rw-r--r--docshell/test/mochitest/test_bug1773192.html61
-rw-r--r--docshell/test/mochitest/test_bug1850335.html72
-rw-r--r--docshell/test/mochitest/test_bug385434.html211
-rw-r--r--docshell/test/mochitest/test_bug387979.html52
-rw-r--r--docshell/test/mochitest/test_bug402210.html50
-rw-r--r--docshell/test/mochitest/test_bug404548.html39
-rw-r--r--docshell/test/mochitest/test_bug413310.html106
-rw-r--r--docshell/test/mochitest/test_bug475636.html52
-rw-r--r--docshell/test/mochitest/test_bug509055.html115
-rw-r--r--docshell/test/mochitest/test_bug511449.html56
-rw-r--r--docshell/test/mochitest/test_bug529119-1.html110
-rw-r--r--docshell/test/mochitest/test_bug529119-2.html116
-rw-r--r--docshell/test/mochitest/test_bug530396.html56
-rw-r--r--docshell/test/mochitest/test_bug540462.html44
-rw-r--r--docshell/test/mochitest/test_bug551225.html32
-rw-r--r--docshell/test/mochitest/test_bug570341.html142
-rw-r--r--docshell/test/mochitest/test_bug580069.html58
-rw-r--r--docshell/test/mochitest/test_bug590573.html198
-rw-r--r--docshell/test/mochitest/test_bug598895.html52
-rw-r--r--docshell/test/mochitest/test_bug634834.html52
-rw-r--r--docshell/test/mochitest/test_bug637644.html52
-rw-r--r--docshell/test/mochitest/test_bug640387_1.html107
-rw-r--r--docshell/test/mochitest/test_bug640387_2.html89
-rw-r--r--docshell/test/mochitest/test_bug653741.html49
-rw-r--r--docshell/test/mochitest/test_bug660404.html76
-rw-r--r--docshell/test/mochitest/test_bug662170.html51
-rw-r--r--docshell/test/mochitest/test_bug668513.html28
-rw-r--r--docshell/test/mochitest/test_bug669671.html145
-rw-r--r--docshell/test/mochitest/test_bug675587.html33
-rw-r--r--docshell/test/mochitest/test_bug680257.html76
-rw-r--r--docshell/test/mochitest/test_bug691547.html59
-rw-r--r--docshell/test/mochitest/test_bug694612.html34
-rw-r--r--docshell/test/mochitest/test_bug703855.html79
-rw-r--r--docshell/test/mochitest/test_bug728939.html37
-rw-r--r--docshell/test/mochitest/test_bug797909.html66
-rw-r--r--docshell/test/mochitest/test_close_onpagehide_by_history_back.html24
-rw-r--r--docshell/test/mochitest/test_close_onpagehide_by_window_close.html20
-rw-r--r--docshell/test/mochitest/test_compressed_multipart.html41
-rw-r--r--docshell/test/mochitest/test_content_javascript_loads.html163
-rw-r--r--docshell/test/mochitest/test_double_submit.html98
-rw-r--r--docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html57
-rw-r--r--docshell/test/mochitest/test_form_restoration.html77
-rw-r--r--docshell/test/mochitest/test_framedhistoryframes.html32
-rw-r--r--docshell/test/mochitest/test_iframe_srcdoc_to_remote.html45
-rw-r--r--docshell/test/mochitest/test_javascript_sandboxed_popup.html27
-rw-r--r--docshell/test/mochitest/test_load_during_reload.html49
-rw-r--r--docshell/test/mochitest/test_navigate_after_pagehide.html34
-rw-r--r--docshell/test/mochitest/test_pushState_after_document_open.html39
-rw-r--r--docshell/test/mochitest/test_redirect_history.html58
-rw-r--r--docshell/test/mochitest/test_triggeringprincipal_location_seturi.html105
-rw-r--r--docshell/test/mochitest/test_windowedhistoryframes.html32
-rw-r--r--docshell/test/mochitest/url1_historyframe.html1
-rw-r--r--docshell/test/mochitest/url2_historyframe.html1
-rw-r--r--docshell/test/moz.build134
-rw-r--r--docshell/test/navigation/NavigationUtils.js203
-rw-r--r--docshell/test/navigation/blank.html1
-rw-r--r--docshell/test/navigation/bluebox_bug430723.html6
-rw-r--r--docshell/test/navigation/browser.toml28
-rw-r--r--docshell/test/navigation/browser_bug1757458.js46
-rw-r--r--docshell/test/navigation/browser_bug343515.js276
-rw-r--r--docshell/test/navigation/browser_ghistorymaxsize_is_0.js82
-rw-r--r--docshell/test/navigation/browser_test-content-chromeflags.js54
-rw-r--r--docshell/test/navigation/browser_test_bfcache_eviction.js102
-rw-r--r--docshell/test/navigation/browser_test_shentry_wireframe.js128
-rw-r--r--docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js57
-rw-r--r--docshell/test/navigation/bug343515_pg1.html5
-rw-r--r--docshell/test/navigation/bug343515_pg2.html7
-rw-r--r--docshell/test/navigation/bug343515_pg3.html7
-rw-r--r--docshell/test/navigation/bug343515_pg3_1.html6
-rw-r--r--docshell/test/navigation/bug343515_pg3_1_1.html1
-rw-r--r--docshell/test/navigation/bug343515_pg3_2.html1
-rw-r--r--docshell/test/navigation/cache_control_max_age_3600.sjs20
-rw-r--r--docshell/test/navigation/file_beforeunload_and_bfcache.html31
-rw-r--r--docshell/test/navigation/file_blockBFCache.html33
-rw-r--r--docshell/test/navigation/file_bug1300461.html61
-rw-r--r--docshell/test/navigation/file_bug1300461_back.html37
-rw-r--r--docshell/test/navigation/file_bug1300461_redirect.html10
-rw-r--r--docshell/test/navigation/file_bug1300461_redirect.html^headers^2
-rw-r--r--docshell/test/navigation/file_bug1326251.html212
-rw-r--r--docshell/test/navigation/file_bug1326251_evict_cache.html17
-rw-r--r--docshell/test/navigation/file_bug1364364-1.html33
-rw-r--r--docshell/test/navigation/file_bug1364364-2.html14
-rw-r--r--docshell/test/navigation/file_bug1375833-frame1.html8
-rw-r--r--docshell/test/navigation/file_bug1375833-frame2.html8
-rw-r--r--docshell/test/navigation/file_bug1375833.html22
-rw-r--r--docshell/test/navigation/file_bug1379762-1.html35
-rw-r--r--docshell/test/navigation/file_bug1536471.html8
-rw-r--r--docshell/test/navigation/file_bug1583110.html26
-rw-r--r--docshell/test/navigation/file_bug1609475.html51
-rw-r--r--docshell/test/navigation/file_bug1706090.html40
-rw-r--r--docshell/test/navigation/file_bug1745638.html15
-rw-r--r--docshell/test/navigation/file_bug1750973.html45
-rw-r--r--docshell/test/navigation/file_bug1758664.html32
-rw-r--r--docshell/test/navigation/file_bug386782_contenteditable.html1
-rw-r--r--docshell/test/navigation/file_bug386782_designmode.html1
-rw-r--r--docshell/test/navigation/file_bug462076_1.html55
-rw-r--r--docshell/test/navigation/file_bug462076_2.html52
-rw-r--r--docshell/test/navigation/file_bug462076_3.html52
-rw-r--r--docshell/test/navigation/file_bug508537_1.html33
-rw-r--r--docshell/test/navigation/file_bug534178.html30
-rw-r--r--docshell/test/navigation/file_contentpolicy_block_window.html5
-rw-r--r--docshell/test/navigation/file_docshell_gotoindex.html42
-rw-r--r--docshell/test/navigation/file_document_write_1.html18
-rw-r--r--docshell/test/navigation/file_evict_from_bfcache.html29
-rw-r--r--docshell/test/navigation/file_fragment_handling_during_load.html27
-rw-r--r--docshell/test/navigation/file_fragment_handling_during_load_frame1.html6
-rw-r--r--docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs20
-rw-r--r--docshell/test/navigation/file_load_history_entry_page_with_one_link.html7
-rw-r--r--docshell/test/navigation/file_load_history_entry_page_with_two_links.html9
-rw-r--r--docshell/test/navigation/file_meta_refresh.html39
-rw-r--r--docshell/test/navigation/file_navigation_type.html25
-rw-r--r--docshell/test/navigation/file_nested_frames.html27
-rw-r--r--docshell/test/navigation/file_nested_frames_innerframe.html1
-rw-r--r--docshell/test/navigation/file_nested_srcdoc.html3
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_1.html5
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^1
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_2.html10
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^1
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_3.html22
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^1
-rw-r--r--docshell/test/navigation/file_new_shentry_during_history_navigation_4.html16
-rw-r--r--docshell/test/navigation/file_online_offline_bfcache.html41
-rw-r--r--docshell/test/navigation/file_reload.html23
-rw-r--r--docshell/test/navigation/file_reload_large_postdata.sjs44
-rw-r--r--docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs27
-rw-r--r--docshell/test/navigation/file_same_url.html24
-rw-r--r--docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html30
-rw-r--r--docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html35
-rw-r--r--docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^1
-rw-r--r--docshell/test/navigation/file_scrollRestoration_navigate.html17
-rw-r--r--docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html63
-rw-r--r--docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^1
-rw-r--r--docshell/test/navigation/file_scrollRestoration_part2_bfcache.html57
-rw-r--r--docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html157
-rw-r--r--docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^1
-rw-r--r--docshell/test/navigation/file_session_history_on_redirect.html16
-rw-r--r--docshell/test/navigation/file_session_history_on_redirect.html^headers^1
-rw-r--r--docshell/test/navigation/file_session_history_on_redirect_2.html16
-rw-r--r--docshell/test/navigation/file_session_history_on_redirect_2.html^headers^1
-rw-r--r--docshell/test/navigation/file_sessionhistory_iframe_removal.html37
-rw-r--r--docshell/test/navigation/file_sessionstorage_across_coop.html12
-rw-r--r--docshell/test/navigation/file_sessionstorage_across_coop.html^headers^1
-rw-r--r--docshell/test/navigation/file_shiftReload_and_pushState.html28
-rw-r--r--docshell/test/navigation/file_ship_beforeunload_fired.html37
-rw-r--r--docshell/test/navigation/file_static_and_dynamic_1.html31
-rw-r--r--docshell/test/navigation/file_tell_opener.html8
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_frame_1.html27
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_frame_2.html8
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html6
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html6
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html15
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html6
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html6
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_subframe.html15
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_subframe_nav.html21
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html20
-rw-r--r--docshell/test/navigation/file_triggeringprincipal_window_open.html6
-rw-r--r--docshell/test/navigation/frame0.html3
-rw-r--r--docshell/test/navigation/frame1.html3
-rw-r--r--docshell/test/navigation/frame2.html3
-rw-r--r--docshell/test/navigation/frame3.html3
-rw-r--r--docshell/test/navigation/frame_1_out_of_6.html6
-rw-r--r--docshell/test/navigation/frame_2_out_of_6.html6
-rw-r--r--docshell/test/navigation/frame_3_out_of_6.html6
-rw-r--r--docshell/test/navigation/frame_4_out_of_6.html6
-rw-r--r--docshell/test/navigation/frame_5_out_of_6.html6
-rw-r--r--docshell/test/navigation/frame_6_out_of_6.html6
-rw-r--r--docshell/test/navigation/frame_load_as_example_com.html6
-rw-r--r--docshell/test/navigation/frame_load_as_example_org.html6
-rw-r--r--docshell/test/navigation/frame_load_as_host1.html6
-rw-r--r--docshell/test/navigation/frame_load_as_host2.html6
-rw-r--r--docshell/test/navigation/frame_load_as_host3.html6
-rw-r--r--docshell/test/navigation/frame_recursive.html6
-rw-r--r--docshell/test/navigation/goback.html5
-rw-r--r--docshell/test/navigation/iframe.html8
-rw-r--r--docshell/test/navigation/iframe_slow_onload.html5
-rw-r--r--docshell/test/navigation/iframe_slow_onload_inner.html19
-rw-r--r--docshell/test/navigation/iframe_static.html8
-rw-r--r--docshell/test/navigation/mochitest.toml359
-rw-r--r--docshell/test/navigation/navigate.html37
-rw-r--r--docshell/test/navigation/navigation_target_popup_url.html1
-rw-r--r--docshell/test/navigation/navigation_target_url.html1
-rw-r--r--docshell/test/navigation/object_recursive_load.html6
-rw-r--r--docshell/test/navigation/open.html9
-rw-r--r--docshell/test/navigation/parent.html14
-rw-r--r--docshell/test/navigation/redbox_bug430723.html6
-rw-r--r--docshell/test/navigation/redirect_handlers.sjs29
-rw-r--r--docshell/test/navigation/redirect_to_blank.sjs6
-rw-r--r--docshell/test/navigation/slow.sjs16
-rw-r--r--docshell/test/navigation/test_aboutblank_change_process.html46
-rw-r--r--docshell/test/navigation/test_beforeunload_and_bfcache.html97
-rw-r--r--docshell/test/navigation/test_blockBFCache.html294
-rw-r--r--docshell/test/navigation/test_bug1300461.html70
-rw-r--r--docshell/test/navigation/test_bug1326251.html47
-rw-r--r--docshell/test/navigation/test_bug1364364.html65
-rw-r--r--docshell/test/navigation/test_bug1375833.html131
-rw-r--r--docshell/test/navigation/test_bug1379762.html67
-rw-r--r--docshell/test/navigation/test_bug13871.html85
-rw-r--r--docshell/test/navigation/test_bug145971.html29
-rw-r--r--docshell/test/navigation/test_bug1536471.html75
-rw-r--r--docshell/test/navigation/test_bug1583110.html36
-rw-r--r--docshell/test/navigation/test_bug1609475.html35
-rw-r--r--docshell/test/navigation/test_bug1699721.html121
-rw-r--r--docshell/test/navigation/test_bug1706090.html49
-rw-r--r--docshell/test/navigation/test_bug1745638.html40
-rw-r--r--docshell/test/navigation/test_bug1747019.html48
-rw-r--r--docshell/test/navigation/test_bug1750973.html20
-rw-r--r--docshell/test/navigation/test_bug1758664.html21
-rw-r--r--docshell/test/navigation/test_bug270414.html95
-rw-r--r--docshell/test/navigation/test_bug278916.html37
-rw-r--r--docshell/test/navigation/test_bug279495.html44
-rw-r--r--docshell/test/navigation/test_bug344861.html35
-rw-r--r--docshell/test/navigation/test_bug386782.html122
-rw-r--r--docshell/test/navigation/test_bug430624.html57
-rw-r--r--docshell/test/navigation/test_bug430723.html124
-rw-r--r--docshell/test/navigation/test_child.html47
-rw-r--r--docshell/test/navigation/test_contentpolicy_block_window.html97
-rw-r--r--docshell/test/navigation/test_docshell_gotoindex.html29
-rw-r--r--docshell/test/navigation/test_dynamic_frame_forward_back.html35
-rw-r--r--docshell/test/navigation/test_evict_from_bfcache.html63
-rw-r--r--docshell/test/navigation/test_fragment_handling_during_load.html35
-rw-r--r--docshell/test/navigation/test_grandchild.html47
-rw-r--r--docshell/test/navigation/test_load_history_entry.html196
-rw-r--r--docshell/test/navigation/test_meta_refresh.html42
-rw-r--r--docshell/test/navigation/test_navigation_type.html47
-rw-r--r--docshell/test/navigation/test_nested_frames.html35
-rw-r--r--docshell/test/navigation/test_new_shentry_during_history_navigation.html90
-rw-r--r--docshell/test/navigation/test_not-opener.html56
-rw-r--r--docshell/test/navigation/test_online_offline_bfcache.html101
-rw-r--r--docshell/test/navigation/test_open_javascript_noopener.html44
-rw-r--r--docshell/test/navigation/test_opener.html56
-rw-r--r--docshell/test/navigation/test_performance_navigation.html41
-rw-r--r--docshell/test/navigation/test_popup-navigates-children.html69
-rw-r--r--docshell/test/navigation/test_rate_limit_location_change.html100
-rw-r--r--docshell/test/navigation/test_recursive_frames.html167
-rw-r--r--docshell/test/navigation/test_reload.html42
-rw-r--r--docshell/test/navigation/test_reload_large_postdata.html61
-rw-r--r--docshell/test/navigation/test_reload_nonbfcached_srcdoc.html40
-rw-r--r--docshell/test/navigation/test_reserved.html92
-rw-r--r--docshell/test/navigation/test_same_url.html56
-rw-r--r--docshell/test/navigation/test_scrollRestoration.html214
-rw-r--r--docshell/test/navigation/test_session_history_entry_cleanup.html35
-rw-r--r--docshell/test/navigation/test_session_history_on_redirect.html92
-rw-r--r--docshell/test/navigation/test_sessionhistory.html48
-rw-r--r--docshell/test/navigation/test_sessionhistory_document_write.html34
-rw-r--r--docshell/test/navigation/test_sessionhistory_iframe_removal.html33
-rw-r--r--docshell/test/navigation/test_sessionstorage_across_coop.html56
-rw-r--r--docshell/test/navigation/test_shiftReload_and_pushState.html35
-rw-r--r--docshell/test/navigation/test_ship_beforeunload_fired.html63
-rw-r--r--docshell/test/navigation/test_ship_beforeunload_fired_2.html65
-rw-r--r--docshell/test/navigation/test_ship_beforeunload_fired_3.html65
-rw-r--r--docshell/test/navigation/test_sibling-matching-parent.html46
-rw-r--r--docshell/test/navigation/test_sibling-off-domain.html46
-rw-r--r--docshell/test/navigation/test_state_size.html32
-rw-r--r--docshell/test/navigation/test_static_and_dynamic.html36
-rw-r--r--docshell/test/navigation/test_triggeringprincipal_frame_nav.html74
-rw-r--r--docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html63
-rw-r--r--docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html87
-rw-r--r--docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html70
-rw-r--r--docshell/test/navigation/test_triggeringprincipal_window_open.html79
-rw-r--r--docshell/test/unit/AllowJavascriptChild.sys.mjs41
-rw-r--r--docshell/test/unit/AllowJavascriptParent.sys.mjs28
-rw-r--r--docshell/test/unit/data/engine.xml10
-rw-r--r--docshell/test/unit/data/enginePost.xml10
-rw-r--r--docshell/test/unit/data/enginePrivate.xml10
-rw-r--r--docshell/test/unit/head_docshell.js103
-rw-r--r--docshell/test/unit/test_URIFixup.js169
-rw-r--r--docshell/test/unit/test_URIFixup_check_host.js183
-rw-r--r--docshell/test/unit/test_URIFixup_external_protocol_fallback.js106
-rw-r--r--docshell/test/unit/test_URIFixup_forced.js159
-rw-r--r--docshell/test/unit/test_URIFixup_info.js1079
-rw-r--r--docshell/test/unit/test_URIFixup_search.js143
-rw-r--r--docshell/test/unit/test_allowJavascript.js291
-rw-r--r--docshell/test/unit/test_browsing_context_structured_clone.js70
-rw-r--r--docshell/test/unit/test_bug442584.js35
-rw-r--r--docshell/test/unit/test_pb_notification.js18
-rw-r--r--docshell/test/unit/test_privacy_transition.js21
-rw-r--r--docshell/test/unit/test_subframe_stop_after_parent_error.js145
-rw-r--r--docshell/test/unit/xpcshell.toml43
-rw-r--r--docshell/test/unit_ipc/test_pb_notification_ipc.js15
-rw-r--r--docshell/test/unit_ipc/xpcshell.toml8
796 files changed, 81726 insertions, 0 deletions
diff --git a/docshell/base/BaseHistory.cpp b/docshell/base/BaseHistory.cpp
new file mode 100644
index 0000000000..3932711b5b
--- /dev/null
+++ b/docshell/base/BaseHistory.cpp
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BaseHistory.h"
+#include "nsThreadUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Link.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_layout.h"
+
+namespace mozilla {
+
+using mozilla::dom::ContentParent;
+using mozilla::dom::Link;
+
+BaseHistory::BaseHistory() : mTrackedURIs(kTrackedUrisInitialSize) {}
+
+BaseHistory::~BaseHistory() = default;
+
+static constexpr nsLiteralCString kDisallowedSchemes[] = {
+ "about"_ns, "blob"_ns, "cached-favicon"_ns,
+ "chrome"_ns, "data"_ns, "imap"_ns,
+ "javascript"_ns, "mailbox"_ns, "news"_ns,
+ "page-icon"_ns, "resource"_ns, "view-source"_ns,
+ "moz-extension"_ns, "moz-page-thumb"_ns,
+};
+
+bool BaseHistory::CanStore(nsIURI* aURI) {
+ nsAutoCString scheme;
+ if (NS_WARN_IF(NS_FAILED(aURI->GetScheme(scheme)))) {
+ return false;
+ }
+
+ if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("https")) {
+ for (const nsLiteralCString& disallowed : kDisallowedSchemes) {
+ if (scheme.Equals(disallowed)) {
+ return false;
+ }
+ }
+ }
+
+ nsAutoCString spec;
+ aURI->GetSpec(spec);
+ return spec.Length() <= StaticPrefs::browser_history_maxUrlLength();
+}
+
+void BaseHistory::ScheduleVisitedQuery(nsIURI* aURI,
+ dom::ContentParent* aForProcess) {
+ mPendingQueries.WithEntryHandle(aURI, [&](auto&& entry) {
+ auto& set = entry.OrInsertWith([] { return ContentParentSet(); });
+ if (aForProcess) {
+ set.Insert(aForProcess);
+ }
+ });
+ if (mStartPendingVisitedQueriesScheduled) {
+ return;
+ }
+ mStartPendingVisitedQueriesScheduled =
+ NS_SUCCEEDED(NS_DispatchToMainThreadQueue(
+ NS_NewRunnableFunction(
+ "BaseHistory::StartPendingVisitedQueries",
+ [self = RefPtr<BaseHistory>(this)] {
+ self->mStartPendingVisitedQueriesScheduled = false;
+ auto queries = std::move(self->mPendingQueries);
+ self->StartPendingVisitedQueries(std::move(queries));
+ MOZ_DIAGNOSTIC_ASSERT(self->mPendingQueries.IsEmpty());
+ }),
+ EventQueuePriority::Idle));
+}
+
+void BaseHistory::CancelVisitedQueryIfPossible(nsIURI* aURI) {
+ mPendingQueries.Remove(aURI);
+ // TODO(bug 1591393): It could be worth to make this virtual and allow places
+ // to stop the existing database query? Needs some measurement.
+}
+
+void BaseHistory::RegisterVisitedCallback(nsIURI* aURI, Link* aLink) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aURI, "Must pass a non-null URI!");
+ MOZ_ASSERT(aLink, "Must pass a non-null Link!");
+
+ if (!CanStore(aURI)) {
+ aLink->VisitedQueryFinished(/* visited = */ false);
+ return;
+ }
+
+ // Obtain our array of observers for this URI.
+ auto* const links =
+ mTrackedURIs.WithEntryHandle(aURI, [&](auto&& entry) -> ObservingLinks* {
+ MOZ_DIAGNOSTIC_ASSERT(!entry || !entry->mLinks.IsEmpty(),
+ "An empty key was kept around in our hashtable!");
+ if (!entry) {
+ ScheduleVisitedQuery(aURI, nullptr);
+ }
+
+ return &entry.OrInsertWith([] { return ObservingLinks{}; });
+ });
+
+ if (!links) {
+ return;
+ }
+
+ // Sanity check that Links are not registered more than once for a given URI.
+ // This will not catch a case where it is registered for two different URIs.
+ MOZ_DIAGNOSTIC_ASSERT(!links->mLinks.Contains(aLink),
+ "Already tracking this Link object!");
+
+ links->mLinks.AppendElement(aLink);
+
+ // If this link has already been queried and we should notify, do so now.
+ switch (links->mStatus) {
+ case VisitedStatus::Unknown:
+ break;
+ case VisitedStatus::Unvisited:
+ [[fallthrough]];
+ case VisitedStatus::Visited:
+ aLink->VisitedQueryFinished(links->mStatus == VisitedStatus::Visited);
+ break;
+ }
+}
+
+void BaseHistory::UnregisterVisitedCallback(nsIURI* aURI, Link* aLink) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aURI, "Must pass a non-null URI!");
+ MOZ_ASSERT(aLink, "Must pass a non-null Link object!");
+
+ // Get the array, and remove the item from it.
+ auto entry = mTrackedURIs.Lookup(aURI);
+ if (!entry) {
+ MOZ_ASSERT(!CanStore(aURI),
+ "Trying to unregister URI that wasn't registered, "
+ "and that could be visited!");
+ return;
+ }
+
+ ObserverArray& observers = entry->mLinks;
+ if (!observers.RemoveElement(aLink)) {
+ MOZ_ASSERT_UNREACHABLE("Trying to unregister node that wasn't registered!");
+ return;
+ }
+
+ // If the array is now empty, we should remove it from the hashtable.
+ if (observers.IsEmpty()) {
+ entry.Remove();
+ CancelVisitedQueryIfPossible(aURI);
+ }
+}
+
+void BaseHistory::NotifyVisited(
+ nsIURI* aURI, VisitedStatus aStatus,
+ const ContentParentSet* aListOfProcessesToNotify) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aStatus != VisitedStatus::Unknown);
+
+ NotifyVisitedInThisProcess(aURI, aStatus);
+ if (XRE_IsParentProcess()) {
+ NotifyVisitedFromParent(aURI, aStatus, aListOfProcessesToNotify);
+ }
+}
+
+void BaseHistory::NotifyVisitedInThisProcess(nsIURI* aURI,
+ VisitedStatus aStatus) {
+ if (NS_WARN_IF(!aURI)) {
+ return;
+ }
+
+ auto entry = mTrackedURIs.Lookup(aURI);
+ if (!entry) {
+ // If we have no observers for this URI, we have nothing to notify about.
+ return;
+ }
+
+ ObservingLinks& links = entry.Data();
+ links.mStatus = aStatus;
+
+ // If we have a key, it should have at least one observer.
+ MOZ_ASSERT(!links.mLinks.IsEmpty());
+
+ // Dispatch an event to each document which has a Link observing this URL.
+ // These will fire asynchronously in the correct DocGroup.
+
+ const bool visited = aStatus == VisitedStatus::Visited;
+ for (Link* link : links.mLinks.BackwardRange()) {
+ link->VisitedQueryFinished(visited);
+ }
+}
+
+void BaseHistory::SendPendingVisitedResultsToChildProcesses() {
+ MOZ_ASSERT(!mPendingResults.IsEmpty());
+
+ mStartPendingResultsScheduled = false;
+
+ auto results = std::move(mPendingResults);
+ MOZ_ASSERT(mPendingResults.IsEmpty());
+
+ nsTArray<ContentParent*> cplist;
+ nsTArray<dom::VisitedQueryResult> resultsForProcess;
+ ContentParent::GetAll(cplist);
+ for (ContentParent* cp : cplist) {
+ resultsForProcess.ClearAndRetainStorage();
+ for (auto& result : results) {
+ if (result.mProcessesToNotify.IsEmpty() ||
+ result.mProcessesToNotify.Contains(cp)) {
+ resultsForProcess.AppendElement(result.mResult);
+ }
+ }
+ if (!resultsForProcess.IsEmpty()) {
+ Unused << NS_WARN_IF(!cp->SendNotifyVisited(resultsForProcess));
+ }
+ }
+}
+
+void BaseHistory::NotifyVisitedFromParent(
+ nsIURI* aURI, VisitedStatus aStatus,
+ const ContentParentSet* aListOfProcessesToNotify) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (aListOfProcessesToNotify && aListOfProcessesToNotify->IsEmpty()) {
+ return;
+ }
+
+ auto& result = *mPendingResults.AppendElement();
+ result.mResult.visited() = aStatus == VisitedStatus::Visited;
+ result.mResult.uri() = aURI;
+ if (aListOfProcessesToNotify) {
+ for (auto* entry : *aListOfProcessesToNotify) {
+ result.mProcessesToNotify.Insert(entry);
+ }
+ }
+
+ if (mStartPendingResultsScheduled) {
+ return;
+ }
+
+ mStartPendingResultsScheduled = NS_SUCCEEDED(NS_DispatchToMainThreadQueue(
+ NewRunnableMethod(
+ "BaseHistory::SendPendingVisitedResultsToChildProcesses", this,
+ &BaseHistory::SendPendingVisitedResultsToChildProcesses),
+ EventQueuePriority::Idle));
+}
+
+} // namespace mozilla
diff --git a/docshell/base/BaseHistory.h b/docshell/base/BaseHistory.h
new file mode 100644
index 0000000000..f0fa36db99
--- /dev/null
+++ b/docshell/base/BaseHistory.h
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_BaseHistory_h
+#define mozilla_BaseHistory_h
+
+#include "IHistory.h"
+#include "mozilla/dom/ContentParent.h"
+#include "nsTHashSet.h"
+
+/* A base class for history implementations that implement link coloring. */
+
+namespace mozilla {
+
+class BaseHistory : public IHistory {
+ public:
+ void RegisterVisitedCallback(nsIURI*, dom::Link*) final;
+ void ScheduleVisitedQuery(nsIURI*, dom::ContentParent*) final;
+ void UnregisterVisitedCallback(nsIURI*, dom::Link*) final;
+ void NotifyVisited(nsIURI*, VisitedStatus,
+ const ContentParentSet* = nullptr) final;
+
+ // Some URIs like data-uris are never going to be stored in history, so we can
+ // avoid doing IPC roundtrips for them or what not.
+ static bool CanStore(nsIURI*);
+
+ protected:
+ void NotifyVisitedInThisProcess(nsIURI*, VisitedStatus);
+ void NotifyVisitedFromParent(nsIURI*, VisitedStatus, const ContentParentSet*);
+ static constexpr const size_t kTrackedUrisInitialSize = 64;
+
+ BaseHistory();
+ ~BaseHistory();
+
+ using ObserverArray = nsTObserverArray<dom::Link*>;
+ struct ObservingLinks {
+ ObserverArray mLinks;
+ VisitedStatus mStatus = VisitedStatus::Unknown;
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mLinks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ };
+
+ using PendingVisitedQueries = nsTHashMap<nsURIHashKey, ContentParentSet>;
+ struct PendingVisitedResult {
+ dom::VisitedQueryResult mResult;
+ ContentParentSet mProcessesToNotify;
+ };
+ using PendingVisitedResults = nsTArray<PendingVisitedResult>;
+
+ // Starts all the queries in the pending queries list, potentially at the same
+ // time.
+ virtual void StartPendingVisitedQueries(PendingVisitedQueries&&) = 0;
+
+ private:
+ // Cancels a visited query, if it is at all possible, because we know we won't
+ // use the results anymore.
+ void CancelVisitedQueryIfPossible(nsIURI*);
+
+ void SendPendingVisitedResultsToChildProcesses();
+
+ protected:
+ // A map from URI to links that depend on that URI, and whether that URI is
+ // known-to-be-visited-or-unvisited already.
+ nsTHashMap<nsURIHashKey, ObservingLinks> mTrackedURIs;
+
+ private:
+ // The set of pending URIs that we haven't queried yet but need to.
+ PendingVisitedQueries mPendingQueries;
+ // The set of pending query results that we still haven't dispatched to child
+ // processes.
+ PendingVisitedResults mPendingResults;
+ // Whether we've successfully scheduled a runnable to call
+ // StartPendingVisitedQueries already.
+ bool mStartPendingVisitedQueriesScheduled = false;
+ // Whether we've successfully scheduled a runnable to call
+ // SendPendingVisitedResultsToChildProcesses already.
+ bool mStartPendingResultsScheduled = false;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp
new file mode 100644
index 0000000000..141036a86c
--- /dev/null
+++ b/docshell/base/BrowsingContext.cpp
@@ -0,0 +1,3905 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BrowsingContext.h"
+
+#include "ipc/IPCMessageUtils.h"
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/DocAccessibleParent.h"
+# include "mozilla/a11y/Platform.h"
+# include "nsAccessibilityService.h"
+# if defined(XP_WIN)
+# include "mozilla/a11y/AccessibleWrap.h"
+# include "mozilla/a11y/Compatibility.h"
+# include "mozilla/a11y/nsWinUtils.h"
+# endif
+#endif
+#include "mozilla/AppShutdown.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/BrowserHost.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/BrowsingContextBinding.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLEmbedElement.h"
+#include "mozilla/dom/HTMLIFrameElement.h"
+#include "mozilla/dom/Location.h"
+#include "mozilla/dom/LocationBinding.h"
+#include "mozilla/dom/MediaDevices.h"
+#include "mozilla/dom/PopupBlocker.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/SessionStoreChild.h"
+#include "mozilla/dom/SessionStorageManager.h"
+#include "mozilla/dom/StructuredCloneTags.h"
+#include "mozilla/dom/UserActivationIPCUtils.h"
+#include "mozilla/dom/WindowBinding.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/dom/WindowProxyHolder.h"
+#include "mozilla/dom/SyncedContextInlines.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/net/DocumentLoadListener.h"
+#include "mozilla/net/RequestContextService.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MediaFeatureChange.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPrefs_page_load.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/URLQueryStringStripper.h"
+#include "mozilla/EventStateManager.h"
+#include "nsIURIFixup.h"
+#include "nsIXULRuntime.h"
+
+#include "nsDocShell.h"
+#include "nsDocShellLoadState.h"
+#include "nsFocusManager.h"
+#include "nsGlobalWindowInner.h"
+#include "nsGlobalWindowOuter.h"
+#include "PresShell.h"
+#include "nsIObserverService.h"
+#include "nsISHistory.h"
+#include "nsContentUtils.h"
+#include "nsQueryObject.h"
+#include "nsSandboxFlags.h"
+#include "nsScriptError.h"
+#include "nsThreadUtils.h"
+#include "xpcprivate.h"
+
+#include "AutoplayPolicy.h"
+#include "GVAutoplayRequestStatusIPC.h"
+
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
+extern mozilla::LazyLogModule gTimeoutDeferralLog;
+
+#define AUTOPLAY_LOG(msg, ...) \
+ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace IPC {
+// Allow serialization and deserialization of OrientationType over IPC
+template <>
+struct ParamTraits<mozilla::dom::OrientationType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::OrientationType,
+ mozilla::dom::OrientationType::Portrait_primary,
+ mozilla::dom::OrientationType::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::DisplayMode>
+ : public ContiguousEnumSerializer<mozilla::dom::DisplayMode,
+ mozilla::dom::DisplayMode::Browser,
+ mozilla::dom::DisplayMode::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::PrefersColorSchemeOverride>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::PrefersColorSchemeOverride,
+ mozilla::dom::PrefersColorSchemeOverride::None,
+ mozilla::dom::PrefersColorSchemeOverride::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::ExplicitActiveStatus>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::ExplicitActiveStatus,
+ mozilla::dom::ExplicitActiveStatus::None,
+ mozilla::dom::ExplicitActiveStatus::EndGuard_> {};
+
+// Allow serialization and deserialization of TouchEventsOverride over IPC
+template <>
+struct ParamTraits<mozilla::dom::TouchEventsOverride>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::TouchEventsOverride,
+ mozilla::dom::TouchEventsOverride::Disabled,
+ mozilla::dom::TouchEventsOverride::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::EmbedderColorSchemes> {
+ using paramType = mozilla::dom::EmbedderColorSchemes;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mUsed);
+ WriteParam(aWriter, aParam.mPreferred);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mUsed) &&
+ ReadParam(aReader, &aResult->mPreferred);
+ }
+};
+
+} // namespace IPC
+
+namespace mozilla {
+namespace dom {
+
+// Explicit specialization of the `Transaction` type. Required by the `extern
+// template class` declaration in the header.
+template class syncedcontext::Transaction<BrowsingContext>;
+
+extern mozilla::LazyLogModule gUserInteractionPRLog;
+
+#define USER_ACTIVATION_LOG(msg, ...) \
+ MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+static LazyLogModule gBrowsingContextLog("BrowsingContext");
+static LazyLogModule gBrowsingContextSyncLog("BrowsingContextSync");
+
+typedef nsTHashMap<nsUint64HashKey, BrowsingContext*> BrowsingContextMap;
+
+// All BrowsingContexts indexed by Id
+static StaticAutoPtr<BrowsingContextMap> sBrowsingContexts;
+// Top-level Content BrowsingContexts only, indexed by BrowserId instead of Id
+static StaticAutoPtr<BrowsingContextMap> sCurrentTopByBrowserId;
+
+static void UnregisterBrowserId(BrowsingContext* aBrowsingContext) {
+ if (!aBrowsingContext->IsTopContent() || !sCurrentTopByBrowserId) {
+ return;
+ }
+
+ // Avoids an extra lookup
+ auto browserIdEntry =
+ sCurrentTopByBrowserId->Lookup(aBrowsingContext->BrowserId());
+ if (browserIdEntry && browserIdEntry.Data() == aBrowsingContext) {
+ browserIdEntry.Remove();
+ }
+}
+
+static void Register(BrowsingContext* aBrowsingContext) {
+ sBrowsingContexts->InsertOrUpdate(aBrowsingContext->Id(), aBrowsingContext);
+ if (aBrowsingContext->IsTopContent()) {
+ sCurrentTopByBrowserId->InsertOrUpdate(aBrowsingContext->BrowserId(),
+ aBrowsingContext);
+ }
+
+ aBrowsingContext->Group()->Register(aBrowsingContext);
+}
+
+// static
+void BrowsingContext::UpdateCurrentTopByBrowserId(
+ BrowsingContext* aNewBrowsingContext) {
+ if (aNewBrowsingContext->IsTopContent()) {
+ sCurrentTopByBrowserId->InsertOrUpdate(aNewBrowsingContext->BrowserId(),
+ aNewBrowsingContext);
+ }
+}
+
+BrowsingContext* BrowsingContext::GetParent() const {
+ return mParentWindow ? mParentWindow->GetBrowsingContext() : nullptr;
+}
+
+bool BrowsingContext::IsInSubtreeOf(BrowsingContext* aContext) {
+ BrowsingContext* bc = this;
+ do {
+ if (bc == aContext) {
+ return true;
+ }
+ } while ((bc = bc->GetParent()));
+ return false;
+}
+
+BrowsingContext* BrowsingContext::Top() {
+ BrowsingContext* bc = this;
+ while (bc->mParentWindow) {
+ bc = bc->GetParent();
+ }
+ return bc;
+}
+
+const BrowsingContext* BrowsingContext::Top() const {
+ const BrowsingContext* bc = this;
+ while (bc->mParentWindow) {
+ bc = bc->GetParent();
+ }
+ return bc;
+}
+
+int32_t BrowsingContext::IndexOf(BrowsingContext* aChild) {
+ int32_t index = -1;
+ for (BrowsingContext* child : Children()) {
+ ++index;
+ if (child == aChild) {
+ break;
+ }
+ }
+ return index;
+}
+
+WindowContext* BrowsingContext::GetTopWindowContext() const {
+ if (mParentWindow) {
+ return mParentWindow->TopWindowContext();
+ }
+ return mCurrentWindowContext;
+}
+
+/* static */
+void BrowsingContext::Init() {
+ if (!sBrowsingContexts) {
+ sBrowsingContexts = new BrowsingContextMap();
+ sCurrentTopByBrowserId = new BrowsingContextMap();
+ ClearOnShutdown(&sBrowsingContexts);
+ ClearOnShutdown(&sCurrentTopByBrowserId);
+ }
+}
+
+/* static */
+LogModule* BrowsingContext::GetLog() { return gBrowsingContextLog; }
+
+/* static */
+LogModule* BrowsingContext::GetSyncLog() { return gBrowsingContextSyncLog; }
+
+/* static */
+already_AddRefed<BrowsingContext> BrowsingContext::Get(uint64_t aId) {
+ return do_AddRef(sBrowsingContexts->Get(aId));
+}
+
+/* static */
+already_AddRefed<BrowsingContext> BrowsingContext::GetCurrentTopByBrowserId(
+ uint64_t aBrowserId) {
+ return do_AddRef(sCurrentTopByBrowserId->Get(aBrowserId));
+}
+
+/* static */
+already_AddRefed<BrowsingContext> BrowsingContext::GetFromWindow(
+ WindowProxyHolder& aProxy) {
+ return do_AddRef(aProxy.get());
+}
+
+CanonicalBrowsingContext* BrowsingContext::Canonical() {
+ return CanonicalBrowsingContext::Cast(this);
+}
+
+bool BrowsingContext::IsOwnedByProcess() const {
+ return mIsInProcess && mDocShell &&
+ !nsDocShell::Cast(mDocShell)->WillChangeProcess();
+}
+
+bool BrowsingContext::SameOriginWithTop() {
+ MOZ_ASSERT(IsInProcess());
+ // If the top BrowsingContext is not same-process to us, it is cross-origin
+ if (!Top()->IsInProcess()) {
+ return false;
+ }
+
+ nsIDocShell* docShell = GetDocShell();
+ if (!docShell) {
+ return false;
+ }
+ Document* doc = docShell->GetDocument();
+ if (!doc) {
+ return false;
+ }
+ nsIPrincipal* principal = doc->NodePrincipal();
+
+ nsIDocShell* topDocShell = Top()->GetDocShell();
+ if (!topDocShell) {
+ return false;
+ }
+ Document* topDoc = topDocShell->GetDocument();
+ if (!topDoc) {
+ return false;
+ }
+ nsIPrincipal* topPrincipal = topDoc->NodePrincipal();
+
+ return principal->Equals(topPrincipal);
+}
+
+/* static */
+already_AddRefed<BrowsingContext> BrowsingContext::CreateDetached(
+ nsGlobalWindowInner* aParent, BrowsingContext* aOpener,
+ BrowsingContextGroup* aSpecificGroup, const nsAString& aName, Type aType,
+ bool aIsPopupRequested, bool aCreatedDynamically) {
+ if (aParent) {
+ MOZ_DIAGNOSTIC_ASSERT(aParent->GetWindowContext());
+ MOZ_DIAGNOSTIC_ASSERT(aParent->GetBrowsingContext()->mType == aType);
+ MOZ_DIAGNOSTIC_ASSERT(aParent->GetBrowsingContext()->GetBrowserId() != 0);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aType != Type::Chrome || XRE_IsParentProcess());
+
+ uint64_t id = nsContentUtils::GenerateBrowsingContextId();
+
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("Creating 0x%08" PRIx64 " in %s", id,
+ XRE_IsParentProcess() ? "Parent" : "Child"));
+
+ RefPtr<BrowsingContext> parentBC =
+ aParent ? aParent->GetBrowsingContext() : nullptr;
+ RefPtr<WindowContext> parentWC =
+ aParent ? aParent->GetWindowContext() : nullptr;
+ BrowsingContext* inherit = parentBC ? parentBC.get() : aOpener;
+
+ // Determine which BrowsingContextGroup this context should be created in.
+ RefPtr<BrowsingContextGroup> group = aSpecificGroup;
+ if (aType == Type::Chrome) {
+ MOZ_DIAGNOSTIC_ASSERT(!group);
+ group = BrowsingContextGroup::GetChromeGroup();
+ } else if (!group) {
+ group = BrowsingContextGroup::Select(parentWC, aOpener);
+ }
+
+ // Configure initial values for synced fields.
+ FieldValues fields;
+ fields.Get<IDX_Name>() = aName;
+
+ if (aOpener) {
+ MOZ_DIAGNOSTIC_ASSERT(!aParent,
+ "new BC with both initial opener and parent");
+ MOZ_DIAGNOSTIC_ASSERT(aOpener->Group() == group);
+ MOZ_DIAGNOSTIC_ASSERT(aOpener->mType == aType);
+ fields.Get<IDX_OpenerId>() = aOpener->Id();
+ fields.Get<IDX_HadOriginalOpener>() = true;
+
+ if (aType == Type::Chrome && !aParent) {
+ // See SetOpener for why we do this inheritance.
+ fields.Get<IDX_PrefersColorSchemeOverride>() =
+ aOpener->Top()->GetPrefersColorSchemeOverride();
+ }
+ }
+
+ if (aParent) {
+ MOZ_DIAGNOSTIC_ASSERT(parentBC->Group() == group);
+ MOZ_DIAGNOSTIC_ASSERT(parentBC->mType == aType);
+ fields.Get<IDX_EmbedderInnerWindowId>() = aParent->WindowID();
+ // Non-toplevel content documents are always embededed within content.
+ fields.Get<IDX_EmbeddedInContentDocument>() =
+ parentBC->mType == Type::Content;
+
+ // XXX(farre): Can/Should we check aParent->IsLoading() here? (Bug
+ // 1608448) Check if the parent was itself loading already
+ auto readystate = aParent->GetDocument()->GetReadyStateEnum();
+ fields.Get<IDX_AncestorLoading>() =
+ parentBC->GetAncestorLoading() ||
+ readystate == Document::ReadyState::READYSTATE_LOADING ||
+ readystate == Document::ReadyState::READYSTATE_INTERACTIVE;
+ }
+
+ fields.Get<IDX_BrowserId>() =
+ parentBC ? parentBC->GetBrowserId() : nsContentUtils::GenerateBrowserId();
+
+ fields.Get<IDX_OpenerPolicy>() = nsILoadInfo::OPENER_POLICY_UNSAFE_NONE;
+ if (aOpener && aOpener->SameOriginWithTop()) {
+ // We inherit the opener policy if there is a creator and if the creator's
+ // origin is same origin with the creator's top-level origin.
+ // If it is cross origin we should not inherit the CrossOriginOpenerPolicy
+ fields.Get<IDX_OpenerPolicy>() = aOpener->Top()->GetOpenerPolicy();
+
+ // If we inherit a policy which is potentially cross-origin isolated, we
+ // must be in a potentially cross-origin isolated BCG.
+ bool isPotentiallyCrossOriginIsolated =
+ fields.Get<IDX_OpenerPolicy>() ==
+ nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP;
+ MOZ_RELEASE_ASSERT(isPotentiallyCrossOriginIsolated ==
+ group->IsPotentiallyCrossOriginIsolated());
+ } else if (aOpener) {
+ // They are not same origin
+ auto topPolicy = aOpener->Top()->GetOpenerPolicy();
+ MOZ_RELEASE_ASSERT(topPolicy == nsILoadInfo::OPENER_POLICY_UNSAFE_NONE ||
+ topPolicy ==
+ nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_ALLOW_POPUPS);
+ } else if (!aParent && group->IsPotentiallyCrossOriginIsolated()) {
+ // If we're creating a brand-new toplevel BC in a potentially cross-origin
+ // isolated group, it should start out with a strict opener policy.
+ fields.Get<IDX_OpenerPolicy>() =
+ nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP;
+ }
+
+ fields.Get<IDX_HistoryID>() = nsID::GenerateUUID();
+ fields.Get<IDX_ExplicitActive>() = [&] {
+ if (parentBC || aType == Type::Content) {
+ // Non-root browsing-contexts inherit their status from its parent.
+ // Top-content either gets managed by the top chrome, or gets manually
+ // managed by the front-end (see ManuallyManagesActiveness). In any case
+ // we want to start off as inactive.
+ return ExplicitActiveStatus::None;
+ }
+ // Chrome starts as active.
+ return ExplicitActiveStatus::Active;
+ }();
+
+ fields.Get<IDX_FullZoom>() = parentBC ? parentBC->FullZoom() : 1.0f;
+ fields.Get<IDX_TextZoom>() = parentBC ? parentBC->TextZoom() : 1.0f;
+
+ bool allowContentRetargeting =
+ inherit ? inherit->GetAllowContentRetargetingOnChildren() : true;
+ fields.Get<IDX_AllowContentRetargeting>() = allowContentRetargeting;
+ fields.Get<IDX_AllowContentRetargetingOnChildren>() = allowContentRetargeting;
+
+ // Assume top allows fullscreen for its children unless otherwise stated.
+ // Subframes start with it false unless otherwise noted in SetEmbedderElement.
+ fields.Get<IDX_FullscreenAllowedByOwner>() = !aParent;
+
+ fields.Get<IDX_DefaultLoadFlags>() =
+ inherit ? inherit->GetDefaultLoadFlags() : nsIRequest::LOAD_NORMAL;
+
+ fields.Get<IDX_OrientationLock>() = mozilla::hal::ScreenOrientation::None;
+
+ fields.Get<IDX_UseGlobalHistory>() =
+ inherit ? inherit->GetUseGlobalHistory() : false;
+
+ fields.Get<IDX_UseErrorPages>() = true;
+
+ fields.Get<IDX_TouchEventsOverrideInternal>() = TouchEventsOverride::None;
+
+ fields.Get<IDX_AllowJavascript>() =
+ inherit ? inherit->GetAllowJavascript() : true;
+
+ fields.Get<IDX_IsPopupRequested>() = aIsPopupRequested;
+
+ if (!parentBC) {
+ fields.Get<IDX_ShouldDelayMediaFromStart>() =
+ StaticPrefs::media_block_autoplay_until_in_foreground();
+ }
+
+ RefPtr<BrowsingContext> context;
+ if (XRE_IsParentProcess()) {
+ context = new CanonicalBrowsingContext(parentWC, group, id,
+ /* aOwnerProcessId */ 0,
+ /* aEmbedderProcessId */ 0, aType,
+ std::move(fields));
+ } else {
+ context =
+ new BrowsingContext(parentWC, group, id, aType, std::move(fields));
+ }
+
+ context->mEmbeddedByThisProcess = XRE_IsParentProcess() || aParent;
+ context->mCreatedDynamically = aCreatedDynamically;
+ if (inherit) {
+ context->mPrivateBrowsingId = inherit->mPrivateBrowsingId;
+ context->mUseRemoteTabs = inherit->mUseRemoteTabs;
+ context->mUseRemoteSubframes = inherit->mUseRemoteSubframes;
+ context->mOriginAttributes = inherit->mOriginAttributes;
+ }
+
+ nsCOMPtr<nsIRequestContextService> rcsvc =
+ net::RequestContextService::GetOrCreate();
+ if (rcsvc) {
+ nsCOMPtr<nsIRequestContext> requestContext;
+ nsresult rv = rcsvc->NewRequestContext(getter_AddRefs(requestContext));
+ if (NS_SUCCEEDED(rv) && requestContext) {
+ context->mRequestContextId = requestContext->GetID();
+ }
+ }
+
+ return context.forget();
+}
+
+already_AddRefed<BrowsingContext> BrowsingContext::CreateIndependent(
+ Type aType) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "BCs created in the content process must be related to "
+ "some BrowserChild");
+ RefPtr<BrowsingContext> bc(
+ CreateDetached(nullptr, nullptr, nullptr, u""_ns, aType, false));
+ bc->mWindowless = bc->IsContent();
+ bc->mEmbeddedByThisProcess = true;
+ bc->EnsureAttached();
+ return bc.forget();
+}
+
+void BrowsingContext::EnsureAttached() {
+ if (!mEverAttached) {
+ Register(this);
+
+ // Attach the browsing context to the tree.
+ Attach(/* aFromIPC */ false, /* aOriginProcess */ nullptr);
+ }
+}
+
+/* static */
+mozilla::ipc::IPCResult BrowsingContext::CreateFromIPC(
+ BrowsingContext::IPCInitializer&& aInit, BrowsingContextGroup* aGroup,
+ ContentParent* aOriginProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(aOriginProcess || XRE_IsContentProcess());
+ MOZ_DIAGNOSTIC_ASSERT(aGroup);
+
+ uint64_t originId = 0;
+ if (aOriginProcess) {
+ originId = aOriginProcess->ChildID();
+ aGroup->EnsureHostProcess(aOriginProcess);
+ }
+
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("Creating 0x%08" PRIx64 " from IPC (origin=0x%08" PRIx64 ")",
+ aInit.mId, originId));
+
+ RefPtr<WindowContext> parent = aInit.GetParent();
+
+ RefPtr<BrowsingContext> context;
+ if (XRE_IsParentProcess()) {
+ // If the new BrowsingContext has a parent, it is a sub-frame embedded in
+ // whatever process sent the message. If it doesn't, and is not windowless,
+ // it is a new window or tab, and will be embedded in the parent process.
+ uint64_t embedderProcessId = (aInit.mWindowless || parent) ? originId : 0;
+ context = new CanonicalBrowsingContext(parent, aGroup, aInit.mId, originId,
+ embedderProcessId, Type::Content,
+ std::move(aInit.mFields));
+ } else {
+ context = new BrowsingContext(parent, aGroup, aInit.mId, Type::Content,
+ std::move(aInit.mFields));
+ }
+
+ context->mWindowless = aInit.mWindowless;
+ context->mCreatedDynamically = aInit.mCreatedDynamically;
+ context->mChildOffset = aInit.mChildOffset;
+ if (context->GetHasSessionHistory()) {
+ context->CreateChildSHistory();
+ if (mozilla::SessionHistoryInParent()) {
+ context->GetChildSessionHistory()->SetIndexAndLength(
+ aInit.mSessionHistoryIndex, aInit.mSessionHistoryCount, nsID());
+ }
+ }
+
+ // NOTE: Call through the `Set` methods for these values to ensure that any
+ // relevant process-local state is also updated.
+ context->SetOriginAttributes(aInit.mOriginAttributes);
+ context->SetRemoteTabs(aInit.mUseRemoteTabs);
+ context->SetRemoteSubframes(aInit.mUseRemoteSubframes);
+ context->mRequestContextId = aInit.mRequestContextId;
+ // NOTE: Private browsing ID is set by `SetOriginAttributes`.
+
+ Register(context);
+
+ return context->Attach(/* aFromIPC */ true, aOriginProcess);
+}
+
+BrowsingContext::BrowsingContext(WindowContext* aParentWindow,
+ BrowsingContextGroup* aGroup,
+ uint64_t aBrowsingContextId, Type aType,
+ FieldValues&& aInit)
+ : mFields(std::move(aInit)),
+ mType(aType),
+ mBrowsingContextId(aBrowsingContextId),
+ mGroup(aGroup),
+ mParentWindow(aParentWindow),
+ mPrivateBrowsingId(0),
+ mEverAttached(false),
+ mIsInProcess(false),
+ mIsDiscarded(false),
+ mWindowless(false),
+ mDanglingRemoteOuterProxies(false),
+ mEmbeddedByThisProcess(false),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mCreatedDynamically(false),
+ mIsInBFCache(false),
+ mCanExecuteScripts(true),
+ mChildOffset(0) {
+ MOZ_RELEASE_ASSERT(!mParentWindow || mParentWindow->Group() == mGroup);
+ MOZ_RELEASE_ASSERT(mBrowsingContextId != 0);
+ MOZ_RELEASE_ASSERT(mGroup);
+}
+
+void BrowsingContext::SetDocShell(nsIDocShell* aDocShell) {
+ // XXX(nika): We should communicate that we are now an active BrowsingContext
+ // process to the parent & do other validation here.
+ MOZ_DIAGNOSTIC_ASSERT(mEverAttached);
+ MOZ_RELEASE_ASSERT(aDocShell->GetBrowsingContext() == this);
+ mDocShell = aDocShell;
+ mDanglingRemoteOuterProxies = !mIsInProcess;
+ mIsInProcess = true;
+ if (mChildSessionHistory) {
+ mChildSessionHistory->SetIsInProcess(true);
+ }
+
+ RecomputeCanExecuteScripts();
+ ClearCachedValuesOfLocations();
+}
+
+// This class implements a callback that will return the remote window proxy for
+// mBrowsingContext in that compartment, if it has one. It also removes the
+// proxy from the map, because the object will be transplanted into another kind
+// of object.
+class MOZ_STACK_CLASS CompartmentRemoteProxyTransplantCallback
+ : public js::CompartmentTransplantCallback {
+ public:
+ explicit CompartmentRemoteProxyTransplantCallback(
+ BrowsingContext* aBrowsingContext)
+ : mBrowsingContext(aBrowsingContext) {}
+
+ virtual JSObject* getObjectToTransplant(
+ JS::Compartment* compartment) override {
+ auto* priv = xpc::CompartmentPrivate::Get(compartment);
+ if (!priv) {
+ return nullptr;
+ }
+
+ auto& map = priv->GetRemoteProxyMap();
+ auto result = map.lookup(mBrowsingContext);
+ if (!result) {
+ return nullptr;
+ }
+ JSObject* resultObject = result->value();
+ map.remove(result);
+
+ return resultObject;
+ }
+
+ private:
+ BrowsingContext* mBrowsingContext;
+};
+
+void BrowsingContext::CleanUpDanglingRemoteOuterWindowProxies(
+ JSContext* aCx, JS::MutableHandle<JSObject*> aOuter) {
+ if (!mDanglingRemoteOuterProxies) {
+ return;
+ }
+ mDanglingRemoteOuterProxies = false;
+
+ CompartmentRemoteProxyTransplantCallback cb(this);
+ js::RemapRemoteWindowProxies(aCx, &cb, aOuter);
+}
+
+bool BrowsingContext::IsActive() const {
+ const BrowsingContext* current = this;
+ do {
+ auto explicit_ = current->GetExplicitActive();
+ if (explicit_ != ExplicitActiveStatus::None) {
+ return explicit_ == ExplicitActiveStatus::Active;
+ }
+ if (mParentWindow && !mParentWindow->IsCurrent()) {
+ return false;
+ }
+ } while ((current = current->GetParent()));
+
+ return false;
+}
+
+bool BrowsingContext::GetIsActiveBrowserWindow() {
+ if (!XRE_IsParentProcess()) {
+ return Top()->GetIsActiveBrowserWindowInternal();
+ }
+
+ // chrome:// urls loaded in the parent won't receive
+ // their own activation so we defer to the top chrome
+ // Browsing Context when in the parent process.
+ return Canonical()
+ ->TopCrossChromeBoundary()
+ ->GetIsActiveBrowserWindowInternal();
+}
+
+void BrowsingContext::SetIsActiveBrowserWindow(bool aActive) {
+ Unused << SetIsActiveBrowserWindowInternal(aActive);
+}
+
+bool BrowsingContext::FullscreenAllowed() const {
+ for (auto* current = this; current; current = current->GetParent()) {
+ if (!current->GetFullscreenAllowedByOwner()) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool OwnerAllowsFullscreen(const Element& aEmbedder) {
+ if (aEmbedder.IsXULElement()) {
+ return !aEmbedder.HasAttr(nsGkAtoms::disablefullscreen);
+ }
+ if (aEmbedder.IsHTMLElement(nsGkAtoms::iframe)) {
+ // This is controlled by feature policy.
+ return true;
+ }
+ if (const auto* embed = HTMLEmbedElement::FromNode(aEmbedder)) {
+ return embed->AllowFullscreen();
+ }
+ return false;
+}
+
+void BrowsingContext::SetEmbedderElement(Element* aEmbedder) {
+ mEmbeddedByThisProcess = true;
+
+ // Update embedder-element-specific fields in a shared transaction.
+ // Don't do this when clearing our embedder, as we're being destroyed either
+ // way.
+ if (aEmbedder) {
+ Transaction txn;
+ txn.SetEmbedderElementType(Some(aEmbedder->LocalName()));
+ txn.SetEmbeddedInContentDocument(
+ aEmbedder->OwnerDoc()->IsContentDocument());
+ if (nsCOMPtr<nsPIDOMWindowInner> inner =
+ do_QueryInterface(aEmbedder->GetOwnerGlobal())) {
+ txn.SetEmbedderInnerWindowId(inner->WindowID());
+ }
+ txn.SetFullscreenAllowedByOwner(OwnerAllowsFullscreen(*aEmbedder));
+ if (XRE_IsParentProcess() && aEmbedder->IsXULElement() && IsTopContent()) {
+ nsAutoString messageManagerGroup;
+ aEmbedder->GetAttr(nsGkAtoms::messagemanagergroup, messageManagerGroup);
+ txn.SetMessageManagerGroup(messageManagerGroup);
+ txn.SetUseGlobalHistory(
+ !aEmbedder->HasAttr(nsGkAtoms::disableglobalhistory));
+ if (!aEmbedder->HasAttr(nsGkAtoms::manualactiveness)) {
+ // We're active iff the parent cross-chrome-boundary is active. Note we
+ // can't just use this->Canonical()->GetParentCrossChromeBoundary here,
+ // since mEmbedderElement is still null at this point.
+ RefPtr bc = aEmbedder->OwnerDoc()->GetBrowsingContext();
+ const bool isActive = bc && bc->IsActive();
+ txn.SetExplicitActive(isActive ? ExplicitActiveStatus::Active
+ : ExplicitActiveStatus::Inactive);
+ if (auto* bp = Canonical()->GetBrowserParent()) {
+ bp->SetRenderLayers(isActive);
+ }
+ }
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(txn.Commit(this));
+ }
+
+ if (XRE_IsParentProcess() && IsTopContent()) {
+ Canonical()->MaybeSetPermanentKey(aEmbedder);
+ }
+
+ mEmbedderElement = aEmbedder;
+
+ if (mEmbedderElement) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyWhenScriptSafe(ToSupports(this),
+ "browsing-context-did-set-embedder", nullptr);
+ }
+
+ if (IsEmbedderTypeObjectOrEmbed()) {
+ Unused << SetSyntheticDocumentContainer(true);
+ }
+ }
+}
+
+bool BrowsingContext::IsEmbedderTypeObjectOrEmbed() {
+ if (const Maybe<nsString>& type = GetEmbedderElementType()) {
+ return nsGkAtoms::object->Equals(*type) || nsGkAtoms::embed->Equals(*type);
+ }
+ return false;
+}
+
+void BrowsingContext::Embed() {
+ if (auto* frame = HTMLIFrameElement::FromNode(mEmbedderElement)) {
+ frame->BindToBrowsingContext(this);
+ }
+}
+
+mozilla::ipc::IPCResult BrowsingContext::Attach(bool aFromIPC,
+ ContentParent* aOriginProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(!mEverAttached);
+ MOZ_DIAGNOSTIC_ASSERT_IF(aFromIPC, aOriginProcess || XRE_IsContentProcess());
+ mEverAttached = true;
+
+ if (MOZ_LOG_TEST(GetLog(), LogLevel::Debug)) {
+ nsAutoCString suffix;
+ mOriginAttributes.CreateSuffix(suffix);
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("%s: Connecting 0x%08" PRIx64 " to 0x%08" PRIx64
+ " (private=%d, remote=%d, fission=%d, oa=%s)",
+ XRE_IsParentProcess() ? "Parent" : "Child", Id(),
+ GetParent() ? GetParent()->Id() : 0, (int)mPrivateBrowsingId,
+ (int)mUseRemoteTabs, (int)mUseRemoteSubframes, suffix.get()));
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mGroup);
+ MOZ_DIAGNOSTIC_ASSERT(!mIsDiscarded);
+
+ if (mGroup->IsPotentiallyCrossOriginIsolated() !=
+ (Top()->GetOpenerPolicy() ==
+ nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP)) {
+ MOZ_DIAGNOSTIC_ASSERT(aFromIPC);
+ if (aFromIPC) {
+ auto* actor = aOriginProcess
+ ? static_cast<mozilla::ipc::IProtocol*>(aOriginProcess)
+ : static_cast<mozilla::ipc::IProtocol*>(
+ ContentChild::GetSingleton());
+ return IPC_FAIL(
+ actor,
+ "Invalid CrossOriginIsolated state in BrowsingContext::Attach call");
+ } else {
+ MOZ_CRASH(
+ "Invalid CrossOriginIsolated state in BrowsingContext::Attach call");
+ }
+ }
+
+ AssertCoherentLoadContext();
+
+ // Add ourselves either to our parent or BrowsingContextGroup's child list.
+ // Important: We shouldn't return IPC_FAIL after this point, since the
+ // BrowsingContext will have already been added to the tree.
+ if (mParentWindow) {
+ if (!aFromIPC) {
+ MOZ_DIAGNOSTIC_ASSERT(!mParentWindow->IsDiscarded(),
+ "local attach in discarded window");
+ MOZ_DIAGNOSTIC_ASSERT(!GetParent()->IsDiscarded(),
+ "local attach call in discarded bc");
+ MOZ_DIAGNOSTIC_ASSERT(mParentWindow->GetWindowGlobalChild(),
+ "local attach call with oop parent window");
+ MOZ_DIAGNOSTIC_ASSERT(mParentWindow->GetWindowGlobalChild()->CanSend(),
+ "local attach call with dead parent window");
+ }
+ mChildOffset =
+ mCreatedDynamically ? -1 : mParentWindow->Children().Length();
+ mParentWindow->AppendChildBrowsingContext(this);
+ RecomputeCanExecuteScripts();
+ } else {
+ mGroup->Toplevels().AppendElement(this);
+ }
+
+ if (GetIsPopupSpam()) {
+ PopupBlocker::RegisterOpenPopupSpam();
+ }
+
+ if (IsTop() && GetHasSessionHistory() && !mChildSessionHistory) {
+ CreateChildSHistory();
+ }
+
+ // Why the context is being attached. This will always be "attach" in the
+ // content process, but may be "replace" if it's known the context being
+ // replaced in the parent process.
+ const char16_t* why = u"attach";
+
+ if (XRE_IsContentProcess() && !aFromIPC) {
+ // Send attach to our parent if we need to.
+ ContentChild::GetSingleton()->SendCreateBrowsingContext(
+ mGroup->Id(), GetIPCInitializer());
+ } else if (XRE_IsParentProcess()) {
+ // If this window was created as a subframe by a content process, it must be
+ // being hosted within the same BrowserParent as its mParentWindow.
+ // Toplevel BrowsingContexts created by content have their BrowserParent
+ // configured during `RecvConstructPopupBrowser`.
+ if (mParentWindow && aOriginProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mParentWindow->Canonical()->GetContentParent() == aOriginProcess,
+ "Creator process isn't the same as our embedder?");
+ Canonical()->SetCurrentBrowserParent(
+ mParentWindow->Canonical()->GetBrowserParent());
+ }
+
+ mGroup->EachOtherParent(aOriginProcess, [&](ContentParent* aParent) {
+ MOZ_DIAGNOSTIC_ASSERT(IsContent(),
+ "chrome BCG cannot be synced to content process");
+ if (!Canonical()->IsEmbeddedInProcess(aParent->ChildID())) {
+ Unused << aParent->SendCreateBrowsingContext(mGroup->Id(),
+ GetIPCInitializer());
+ }
+ });
+
+ if (IsTop() && IsContent() && Canonical()->GetWebProgress()) {
+ why = u"replace";
+ }
+
+ // We want to create a BrowsingContextWebProgress for all content
+ // BrowsingContexts.
+ if (IsContent() && !Canonical()->mWebProgress) {
+ Canonical()->mWebProgress = new BrowsingContextWebProgress(Canonical());
+ }
+ }
+
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyWhenScriptSafe(ToSupports(this), "browsing-context-attached",
+ why);
+ }
+
+ if (XRE_IsParentProcess()) {
+ Canonical()->CanonicalAttach();
+ }
+ return IPC_OK();
+}
+
+void BrowsingContext::Detach(bool aFromIPC) {
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("%s: Detaching 0x%08" PRIx64 " from 0x%08" PRIx64,
+ XRE_IsParentProcess() ? "Parent" : "Child", Id(),
+ GetParent() ? GetParent()->Id() : 0));
+
+ MOZ_DIAGNOSTIC_ASSERT(mEverAttached);
+ MOZ_DIAGNOSTIC_ASSERT(!mIsDiscarded);
+
+ if (XRE_IsParentProcess()) {
+ Canonical()->AddPendingDiscard();
+ }
+ auto callListeners =
+ MakeScopeExit([&, listeners = std::move(mDiscardListeners), id = Id()] {
+ for (const auto& listener : listeners) {
+ listener(id);
+ }
+ if (XRE_IsParentProcess()) {
+ Canonical()->RemovePendingDiscard();
+ }
+ });
+
+ nsCOMPtr<nsIRequestContextService> rcsvc =
+ net::RequestContextService::GetOrCreate();
+ if (rcsvc) {
+ rcsvc->RemoveRequestContext(GetRequestContextId());
+ }
+
+ // This will only ever be null if the cycle-collector has unlinked us. Don't
+ // try to detach ourselves in that case.
+ if (NS_WARN_IF(!mGroup)) {
+ MOZ_ASSERT_UNREACHABLE();
+ return;
+ }
+
+ if (mParentWindow) {
+ mParentWindow->RemoveChildBrowsingContext(this);
+ } else {
+ mGroup->Toplevels().RemoveElement(this);
+ }
+
+ if (XRE_IsParentProcess()) {
+ RefPtr<CanonicalBrowsingContext> self{Canonical()};
+ Group()->EachParent([&](ContentParent* aParent) {
+ // Only the embedder process is allowed to initiate a BrowsingContext
+ // detach, so if we've gotten here, the host process already knows we've
+ // been detached, and there's no need to tell it again.
+ //
+ // If the owner process is not the same as the embedder process, its
+ // BrowsingContext will be detached when its nsWebBrowser instance is
+ // destroyed.
+ bool doDiscard = !Canonical()->IsEmbeddedInProcess(aParent->ChildID()) &&
+ !Canonical()->IsOwnedByProcess(aParent->ChildID());
+
+ // Hold a strong reference to ourself, and keep our BrowsingContextGroup
+ // alive, until the responses comes back to ensure we don't die while
+ // messages relating to this context are in-flight.
+ //
+ // When the callback is called, the keepalive on our group will be
+ // destroyed, and the reference to the BrowsingContext will be dropped,
+ // which may cause it to be fully destroyed.
+ mGroup->AddKeepAlive();
+ self->AddPendingDiscard();
+ auto callback = [self](auto) {
+ self->mGroup->RemoveKeepAlive();
+ self->RemovePendingDiscard();
+ };
+
+ aParent->SendDiscardBrowsingContext(this, doDiscard, callback, callback);
+ });
+ } else {
+ // Hold a strong reference to ourself until the responses come back to
+ // ensure the BrowsingContext isn't cleaned up before the parent process
+ // acknowledges the discard request.
+ auto callback = [self = RefPtr{this}](auto) {};
+ ContentChild::GetSingleton()->SendDiscardBrowsingContext(
+ this, !aFromIPC, callback, callback);
+ }
+
+ mGroup->Unregister(this);
+ UnregisterBrowserId(this);
+ mIsDiscarded = true;
+
+ if (XRE_IsParentProcess()) {
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm) {
+ fm->BrowsingContextDetached(this);
+ }
+ }
+
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ // Why the context is being discarded. This will always be "discard" in the
+ // content process, but may be "replace" if it's known the context being
+ // replaced in the parent process.
+ const char16_t* why = u"discard";
+ if (XRE_IsParentProcess() && IsTop() && !Canonical()->GetWebProgress()) {
+ why = u"replace";
+ }
+ obs->NotifyObservers(ToSupports(this), "browsing-context-discarded", why);
+ }
+
+ // NOTE: Doesn't use SetClosed, as it will be set in all processes
+ // automatically by calls to Detach()
+ mFields.SetWithoutSyncing<IDX_Closed>(true);
+
+ if (GetIsPopupSpam()) {
+ PopupBlocker::UnregisterOpenPopupSpam();
+ // NOTE: Doesn't use SetIsPopupSpam, as it will be set all processes
+ // automatically.
+ mFields.SetWithoutSyncing<IDX_IsPopupSpam>(false);
+ }
+
+ AssertOriginAttributesMatchPrivateBrowsing();
+
+ if (XRE_IsParentProcess()) {
+ Canonical()->CanonicalDiscard();
+ }
+}
+
+void BrowsingContext::AddDiscardListener(
+ std::function<void(uint64_t)>&& aListener) {
+ if (mIsDiscarded) {
+ aListener(Id());
+ return;
+ }
+ mDiscardListeners.AppendElement(std::move(aListener));
+}
+
+void BrowsingContext::PrepareForProcessChange() {
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("%s: Preparing 0x%08" PRIx64 " for a process change",
+ XRE_IsParentProcess() ? "Parent" : "Child", Id()));
+
+ MOZ_ASSERT(mIsInProcess, "Must currently be an in-process frame");
+ MOZ_ASSERT(!mIsDiscarded, "We're already closed?");
+
+ mIsInProcess = false;
+ mUserGestureStart = TimeStamp();
+
+ ClearCachedValuesOfLocations();
+
+ // NOTE: For now, clear our nsDocShell reference, as we're primarily in a
+ // different process now. This may need to change in the future with
+ // Cross-Process BFCache.
+ mDocShell = nullptr;
+ if (mChildSessionHistory) {
+ // This can be removed once session history is stored exclusively in the
+ // parent process.
+ mChildSessionHistory->SetIsInProcess(false);
+ }
+
+ if (!mWindowProxy) {
+ return;
+ }
+
+ // We have to go through mWindowProxy rather than calling GetDOMWindow() on
+ // mDocShell because the mDocshell reference gets cleared immediately after
+ // the window is closed.
+ nsGlobalWindowOuter::PrepareForProcessChange(mWindowProxy);
+ MOZ_ASSERT(!mWindowProxy);
+}
+
+bool BrowsingContext::IsTargetable() const {
+ return !GetClosed() && AncestorsAreCurrent();
+}
+
+void BrowsingContext::SetOpener(BrowsingContext* aOpener) {
+ MOZ_DIAGNOSTIC_ASSERT(!aOpener || aOpener->Group() == Group());
+ MOZ_DIAGNOSTIC_ASSERT(!aOpener || aOpener->mType == mType);
+
+ MOZ_ALWAYS_SUCCEEDS(SetOpenerId(aOpener ? aOpener->Id() : 0));
+
+ if (IsChrome() && IsTop() && aOpener) {
+ // Inherit color scheme overrides from parent window. This is to inherit the
+ // color scheme of dark themed PBM windows in dialogs opened by such
+ // windows.
+ auto openerOverride = aOpener->Top()->PrefersColorSchemeOverride();
+ if (openerOverride != PrefersColorSchemeOverride()) {
+ MOZ_ALWAYS_SUCCEEDS(SetPrefersColorSchemeOverride(openerOverride));
+ }
+ }
+}
+
+bool BrowsingContext::HasOpener() const {
+ return sBrowsingContexts->Contains(GetOpenerId());
+}
+
+bool BrowsingContext::AncestorsAreCurrent() const {
+ const BrowsingContext* bc = this;
+ while (true) {
+ if (bc->IsDiscarded()) {
+ return false;
+ }
+
+ if (WindowContext* wc = bc->GetParentWindowContext()) {
+ if (!wc->IsCurrent() || wc->IsDiscarded()) {
+ return false;
+ }
+
+ bc = wc->GetBrowsingContext();
+ } else {
+ return true;
+ }
+ }
+}
+
+bool BrowsingContext::IsInBFCache() const {
+ if (mozilla::SessionHistoryInParent()) {
+ return mIsInBFCache;
+ }
+ return mParentWindow &&
+ mParentWindow->TopWindowContext()->GetWindowStateSaved();
+}
+
+Span<RefPtr<BrowsingContext>> BrowsingContext::Children() const {
+ if (WindowContext* current = mCurrentWindowContext) {
+ return current->Children();
+ }
+ return Span<RefPtr<BrowsingContext>>();
+}
+
+void BrowsingContext::GetChildren(
+ nsTArray<RefPtr<BrowsingContext>>& aChildren) {
+ aChildren.AppendElements(Children());
+}
+
+Span<RefPtr<BrowsingContext>> BrowsingContext::NonSyntheticChildren() const {
+ if (WindowContext* current = mCurrentWindowContext) {
+ return current->NonSyntheticChildren();
+ }
+ return Span<RefPtr<BrowsingContext>>();
+}
+
+void BrowsingContext::GetWindowContexts(
+ nsTArray<RefPtr<WindowContext>>& aWindows) {
+ aWindows.AppendElements(mWindowContexts);
+}
+
+void BrowsingContext::RegisterWindowContext(WindowContext* aWindow) {
+ MOZ_ASSERT(!mWindowContexts.Contains(aWindow),
+ "WindowContext already registered!");
+ MOZ_ASSERT(aWindow->GetBrowsingContext() == this);
+
+ mWindowContexts.AppendElement(aWindow);
+
+ // If the newly registered WindowContext is for our current inner window ID,
+ // re-run the `DidSet` handler to re-establish the relationship.
+ if (aWindow->InnerWindowId() == GetCurrentInnerWindowId()) {
+ DidSet(FieldIndex<IDX_CurrentInnerWindowId>());
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentWindowContext == aWindow);
+ }
+}
+
+void BrowsingContext::UnregisterWindowContext(WindowContext* aWindow) {
+ MOZ_ASSERT(mWindowContexts.Contains(aWindow),
+ "WindowContext not registered!");
+ mWindowContexts.RemoveElement(aWindow);
+
+ // If our currently active window was unregistered, clear our reference to it.
+ if (aWindow == mCurrentWindowContext) {
+ // Re-read our `CurrentInnerWindowId` value and use it to set
+ // `mCurrentWindowContext`. As `aWindow` is now unregistered and discarded,
+ // we won't find it, and the value will be cleared back to `nullptr`.
+ DidSet(FieldIndex<IDX_CurrentInnerWindowId>());
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentWindowContext == nullptr);
+ }
+}
+
+void BrowsingContext::PreOrderWalkVoid(
+ const std::function<void(BrowsingContext*)>& aCallback) {
+ aCallback(this);
+
+ AutoTArray<RefPtr<BrowsingContext>, 8> children;
+ children.AppendElements(Children());
+
+ for (auto& child : children) {
+ child->PreOrderWalkVoid(aCallback);
+ }
+}
+
+BrowsingContext::WalkFlag BrowsingContext::PreOrderWalkFlag(
+ const std::function<WalkFlag(BrowsingContext*)>& aCallback) {
+ switch (aCallback(this)) {
+ case WalkFlag::Skip:
+ return WalkFlag::Next;
+ case WalkFlag::Stop:
+ return WalkFlag::Stop;
+ case WalkFlag::Next:
+ default:
+ break;
+ }
+
+ AutoTArray<RefPtr<BrowsingContext>, 8> children;
+ children.AppendElements(Children());
+
+ for (auto& child : children) {
+ switch (child->PreOrderWalkFlag(aCallback)) {
+ case WalkFlag::Stop:
+ return WalkFlag::Stop;
+ default:
+ break;
+ }
+ }
+
+ return WalkFlag::Next;
+}
+
+void BrowsingContext::PostOrderWalk(
+ const std::function<void(BrowsingContext*)>& aCallback) {
+ AutoTArray<RefPtr<BrowsingContext>, 8> children;
+ children.AppendElements(Children());
+
+ for (auto& child : children) {
+ child->PostOrderWalk(aCallback);
+ }
+
+ aCallback(this);
+}
+
+void BrowsingContext::GetAllBrowsingContextsInSubtree(
+ nsTArray<RefPtr<BrowsingContext>>& aBrowsingContexts) {
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ aBrowsingContexts.AppendElement(aContext);
+ });
+}
+
+BrowsingContext* BrowsingContext::FindChildWithName(
+ const nsAString& aName, WindowGlobalChild& aRequestingWindow) {
+ if (aName.IsEmpty()) {
+ // You can't find a browsing context with the empty name.
+ return nullptr;
+ }
+
+ for (BrowsingContext* child : NonSyntheticChildren()) {
+ if (child->NameEquals(aName) && aRequestingWindow.CanNavigate(child) &&
+ child->IsTargetable()) {
+ return child;
+ }
+ }
+
+ return nullptr;
+}
+
+BrowsingContext* BrowsingContext::FindWithSpecialName(
+ const nsAString& aName, WindowGlobalChild& aRequestingWindow) {
+ // TODO(farre): Neither BrowsingContext nor nsDocShell checks if the
+ // browsing context pointed to by a special name is active. Should
+ // it be? See Bug 1527913.
+ if (aName.LowerCaseEqualsLiteral("_self")) {
+ return this;
+ }
+
+ if (aName.LowerCaseEqualsLiteral("_parent")) {
+ if (BrowsingContext* parent = GetParent()) {
+ return aRequestingWindow.CanNavigate(parent) ? parent : nullptr;
+ }
+ return this;
+ }
+
+ if (aName.LowerCaseEqualsLiteral("_top")) {
+ BrowsingContext* top = Top();
+
+ return aRequestingWindow.CanNavigate(top) ? top : nullptr;
+ }
+
+ return nullptr;
+}
+
+BrowsingContext* BrowsingContext::FindWithNameInSubtree(
+ const nsAString& aName, WindowGlobalChild* aRequestingWindow) {
+ MOZ_DIAGNOSTIC_ASSERT(!aName.IsEmpty());
+
+ if (NameEquals(aName) &&
+ (!aRequestingWindow || aRequestingWindow->CanNavigate(this)) &&
+ IsTargetable()) {
+ return this;
+ }
+
+ for (BrowsingContext* child : NonSyntheticChildren()) {
+ if (BrowsingContext* found =
+ child->FindWithNameInSubtree(aName, aRequestingWindow)) {
+ return found;
+ }
+ }
+
+ return nullptr;
+}
+
+bool BrowsingContext::IsSandboxedFrom(BrowsingContext* aTarget) {
+ // If no target then not sandboxed.
+ if (!aTarget) {
+ return false;
+ }
+
+ // We cannot be sandboxed from ourselves.
+ if (aTarget == this) {
+ return false;
+ }
+
+ // Default the sandbox flags to our flags, so that if we can't retrieve the
+ // active document, we will still enforce our own.
+ uint32_t sandboxFlags = GetSandboxFlags();
+ if (mDocShell) {
+ if (RefPtr<Document> doc = mDocShell->GetExtantDocument()) {
+ sandboxFlags = doc->GetSandboxFlags();
+ }
+ }
+
+ // If no flags, we are not sandboxed at all.
+ if (!sandboxFlags) {
+ return false;
+ }
+
+ // If aTarget has an ancestor, it is not top level.
+ if (RefPtr<BrowsingContext> ancestorOfTarget = aTarget->GetParent()) {
+ do {
+ // We are not sandboxed if we are an ancestor of target.
+ if (ancestorOfTarget == this) {
+ return false;
+ }
+ ancestorOfTarget = ancestorOfTarget->GetParent();
+ } while (ancestorOfTarget);
+
+ // Otherwise, we are sandboxed from aTarget.
+ return true;
+ }
+
+ // aTarget is top level, are we the "one permitted sandboxed
+ // navigator", i.e. did we open aTarget?
+ if (aTarget->GetOnePermittedSandboxedNavigatorId() == Id()) {
+ return false;
+ }
+
+ // If SANDBOXED_TOPLEVEL_NAVIGATION flag is not on, we are not sandboxed
+ // from our top.
+ if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION) && aTarget == Top()) {
+ return false;
+ }
+
+ // If SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION flag is not on, we are not
+ // sandboxed from our top if we have user interaction. We assume there is a
+ // valid transient user gesture interaction if this check happens in the
+ // target process given that we have checked in the triggering process
+ // already.
+ if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) &&
+ mCurrentWindowContext &&
+ (!mCurrentWindowContext->IsInProcess() ||
+ mCurrentWindowContext->HasValidTransientUserGestureActivation()) &&
+ aTarget == Top()) {
+ return false;
+ }
+
+ // Otherwise, we are sandboxed from aTarget.
+ return true;
+}
+
+RefPtr<SessionStorageManager> BrowsingContext::GetSessionStorageManager() {
+ RefPtr<SessionStorageManager>& manager = Top()->mSessionStorageManager;
+ if (!manager) {
+ manager = new SessionStorageManager(this);
+ }
+ return manager;
+}
+
+bool BrowsingContext::CrossOriginIsolated() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return StaticPrefs::
+ dom_postMessage_sharedArrayBuffer_withCOOP_COEP_AtStartup() &&
+ Top()->GetOpenerPolicy() ==
+ nsILoadInfo::
+ OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP &&
+ XRE_IsContentProcess() &&
+ StringBeginsWith(ContentChild::GetSingleton()->GetRemoteType(),
+ WITH_COOP_COEP_REMOTE_TYPE_PREFIX);
+}
+
+void BrowsingContext::SetTriggeringAndInheritPrincipals(
+ nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit,
+ uint64_t aLoadIdentifier) {
+ mTriggeringPrincipal = Some(
+ PrincipalWithLoadIdentifierTuple(aTriggeringPrincipal, aLoadIdentifier));
+ if (aPrincipalToInherit) {
+ mPrincipalToInherit = Some(
+ PrincipalWithLoadIdentifierTuple(aPrincipalToInherit, aLoadIdentifier));
+ }
+}
+
+std::tuple<nsCOMPtr<nsIPrincipal>, nsCOMPtr<nsIPrincipal>>
+BrowsingContext::GetTriggeringAndInheritPrincipalsForCurrentLoad() {
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal =
+ GetSavedPrincipal(mTriggeringPrincipal);
+ nsCOMPtr<nsIPrincipal> principalToInherit =
+ GetSavedPrincipal(mPrincipalToInherit);
+ return std::make_tuple(triggeringPrincipal, principalToInherit);
+}
+
+nsIPrincipal* BrowsingContext::GetSavedPrincipal(
+ Maybe<PrincipalWithLoadIdentifierTuple> aPrincipalTuple) {
+ if (aPrincipalTuple) {
+ nsCOMPtr<nsIPrincipal> principal;
+ uint64_t loadIdentifier;
+ std::tie(principal, loadIdentifier) = *aPrincipalTuple;
+ // We want to return a principal only if the load identifier for it
+ // matches the current one for this BC.
+ if (auto current = GetCurrentLoadIdentifier();
+ current && *current == loadIdentifier) {
+ return principal;
+ }
+ }
+ return nullptr;
+}
+
+BrowsingContext::~BrowsingContext() {
+ MOZ_DIAGNOSTIC_ASSERT(!mParentWindow ||
+ !mParentWindow->mChildren.Contains(this));
+ MOZ_DIAGNOSTIC_ASSERT(!mGroup || !mGroup->Toplevels().Contains(this));
+
+ mDeprioritizedLoadRunner.clear();
+
+ if (sBrowsingContexts) {
+ sBrowsingContexts->Remove(Id());
+ }
+ UnregisterBrowserId(this);
+
+ ClearCachedValuesOfLocations();
+ mLocations.clear();
+}
+
+/* static */
+void BrowsingContext::DiscardFromContentParent(ContentParent* aCP) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (sBrowsingContexts) {
+ AutoTArray<RefPtr<BrowsingContext>, 8> toDiscard;
+ for (const auto& data : sBrowsingContexts->Values()) {
+ auto* bc = data->Canonical();
+ if (!bc->IsDiscarded() && bc->IsEmbeddedInProcess(aCP->ChildID())) {
+ toDiscard.AppendElement(bc);
+ }
+ }
+
+ for (BrowsingContext* bc : toDiscard) {
+ bc->Detach(/* aFromIPC */ true);
+ }
+ }
+}
+
+nsISupports* BrowsingContext::GetParentObject() const {
+ return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+}
+
+JSObject* BrowsingContext::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return BrowsingContext_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool BrowsingContext::WriteStructuredClone(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter,
+ StructuredCloneHolder* aHolder) {
+ MOZ_DIAGNOSTIC_ASSERT(mEverAttached);
+ return (JS_WriteUint32Pair(aWriter, SCTAG_DOM_BROWSING_CONTEXT, 0) &&
+ JS_WriteUint32Pair(aWriter, uint32_t(Id()), uint32_t(Id() >> 32)));
+}
+
+/* static */
+JSObject* BrowsingContext::ReadStructuredClone(JSContext* aCx,
+ JSStructuredCloneReader* aReader,
+ StructuredCloneHolder* aHolder) {
+ uint32_t idLow = 0;
+ uint32_t idHigh = 0;
+ if (!JS_ReadUint32Pair(aReader, &idLow, &idHigh)) {
+ return nullptr;
+ }
+ uint64_t id = uint64_t(idHigh) << 32 | idLow;
+
+ // Note: Do this check after reading our ID data. Returning null will abort
+ // the decode operation anyway, but we should at least be as safe as possible.
+ if (NS_WARN_IF(!NS_IsMainThread())) {
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "We shouldn't be trying to decode a BrowsingContext "
+ "on a background thread.");
+ return nullptr;
+ }
+
+ JS::Rooted<JS::Value> val(aCx, JS::NullValue());
+ // We'll get rooting hazard errors from the RefPtr destructor if it isn't
+ // destroyed before we try to return a raw JSObject*, so create it in its own
+ // scope.
+ if (RefPtr<BrowsingContext> context = Get(id)) {
+ if (!GetOrCreateDOMReflector(aCx, context, &val) || !val.isObject()) {
+ return nullptr;
+ }
+ }
+ return val.toObjectOrNull();
+}
+
+bool BrowsingContext::CanSetOriginAttributes() {
+ // A discarded BrowsingContext has already been destroyed, and cannot modify
+ // its OriginAttributes.
+ if (NS_WARN_IF(IsDiscarded())) {
+ return false;
+ }
+
+ // Before attaching is the safest time to set OriginAttributes, and the only
+ // allowed time for content BrowsingContexts.
+ if (!EverAttached()) {
+ return true;
+ }
+
+ // Attached content BrowsingContexts may have been synced to other processes.
+ if (NS_WARN_IF(IsContent())) {
+ MOZ_CRASH();
+ return false;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ // Cannot set OriginAttributes after we've created our child BrowsingContext.
+ if (NS_WARN_IF(!Children().IsEmpty())) {
+ return false;
+ }
+
+ // Only allow setting OriginAttributes if we have no associated document, or
+ // the document is still `about:blank`.
+ // TODO: Bug 1273058 - should have no document when setting origin attributes.
+ if (WindowGlobalParent* window = Canonical()->GetCurrentWindowGlobal()) {
+ if (nsIURI* uri = window->GetDocumentURI()) {
+ MOZ_ASSERT(NS_IsAboutBlank(uri));
+ return NS_IsAboutBlank(uri);
+ }
+ }
+ return true;
+}
+
+Nullable<WindowProxyHolder> BrowsingContext::GetAssociatedWindow() {
+ // nsILoadContext usually only returns same-process windows,
+ // so we intentionally return nullptr if this BC is out of
+ // process.
+ if (IsInProcess()) {
+ return WindowProxyHolder(this);
+ }
+ return nullptr;
+}
+
+Nullable<WindowProxyHolder> BrowsingContext::GetTopWindow() {
+ return Top()->GetAssociatedWindow();
+}
+
+Element* BrowsingContext::GetTopFrameElement() {
+ return Top()->GetEmbedderElement();
+}
+
+void BrowsingContext::SetUsePrivateBrowsing(bool aUsePrivateBrowsing,
+ ErrorResult& aError) {
+ nsresult rv = SetUsePrivateBrowsing(aUsePrivateBrowsing);
+ if (NS_FAILED(rv)) {
+ aError.Throw(rv);
+ }
+}
+
+void BrowsingContext::SetUseTrackingProtectionWebIDL(
+ bool aUseTrackingProtection, ErrorResult& aRv) {
+ SetForceEnableTrackingProtection(aUseTrackingProtection, aRv);
+}
+
+void BrowsingContext::GetOriginAttributes(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aVal,
+ ErrorResult& aError) {
+ AssertOriginAttributesMatchPrivateBrowsing();
+
+ if (!ToJSValue(aCx, mOriginAttributes, aVal)) {
+ aError.NoteJSContextException(aCx);
+ }
+}
+
+NS_IMETHODIMP BrowsingContext::GetAssociatedWindow(
+ mozIDOMWindowProxy** aAssociatedWindow) {
+ nsCOMPtr<mozIDOMWindowProxy> win = GetDOMWindow();
+ win.forget(aAssociatedWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::GetTopWindow(mozIDOMWindowProxy** aTopWindow) {
+ return Top()->GetAssociatedWindow(aTopWindow);
+}
+
+NS_IMETHODIMP BrowsingContext::GetTopFrameElement(Element** aTopFrameElement) {
+ RefPtr<Element> topFrameElement = GetTopFrameElement();
+ topFrameElement.forget(aTopFrameElement);
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::GetIsContent(bool* aIsContent) {
+ *aIsContent = IsContent();
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::GetUsePrivateBrowsing(
+ bool* aUsePrivateBrowsing) {
+ *aUsePrivateBrowsing = mPrivateBrowsingId > 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) {
+ if (!CanSetOriginAttributes()) {
+ bool changed = aUsePrivateBrowsing != (mPrivateBrowsingId > 0);
+ if (changed) {
+ NS_WARNING("SetUsePrivateBrowsing when !CanSetOriginAttributes()");
+ }
+ return changed ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ return SetPrivateBrowsing(aUsePrivateBrowsing);
+}
+
+NS_IMETHODIMP BrowsingContext::SetPrivateBrowsing(bool aPrivateBrowsing) {
+ if (!CanSetOriginAttributes()) {
+ NS_WARNING("Attempt to set PrivateBrowsing when !CanSetOriginAttributes");
+ return NS_ERROR_FAILURE;
+ }
+
+ bool changed = aPrivateBrowsing != (mPrivateBrowsingId > 0);
+ if (changed) {
+ mPrivateBrowsingId = aPrivateBrowsing ? 1 : 0;
+ if (IsContent()) {
+ mOriginAttributes.SyncAttributesWithPrivateBrowsing(aPrivateBrowsing);
+ }
+
+ if (XRE_IsParentProcess()) {
+ Canonical()->AdjustPrivateBrowsingCount(aPrivateBrowsing);
+ }
+ }
+ AssertOriginAttributesMatchPrivateBrowsing();
+
+ if (changed && mDocShell) {
+ nsDocShell::Cast(mDocShell)->NotifyPrivateBrowsingChanged();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::GetUseRemoteTabs(bool* aUseRemoteTabs) {
+ *aUseRemoteTabs = mUseRemoteTabs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::SetRemoteTabs(bool aUseRemoteTabs) {
+ if (!CanSetOriginAttributes()) {
+ NS_WARNING("Attempt to set RemoteTabs when !CanSetOriginAttributes");
+ return NS_ERROR_FAILURE;
+ }
+
+ static bool annotated = false;
+ if (aUseRemoteTabs && !annotated) {
+ annotated = true;
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::DOMIPCEnabled,
+ true);
+ }
+
+ // Don't allow non-remote tabs with remote subframes.
+ if (NS_WARN_IF(!aUseRemoteTabs && mUseRemoteSubframes)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mUseRemoteTabs = aUseRemoteTabs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::GetUseRemoteSubframes(
+ bool* aUseRemoteSubframes) {
+ *aUseRemoteSubframes = mUseRemoteSubframes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::SetRemoteSubframes(bool aUseRemoteSubframes) {
+ if (!CanSetOriginAttributes()) {
+ NS_WARNING("Attempt to set RemoteSubframes when !CanSetOriginAttributes");
+ return NS_ERROR_FAILURE;
+ }
+
+ static bool annotated = false;
+ if (aUseRemoteSubframes && !annotated) {
+ annotated = true;
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::DOMFissionEnabled, true);
+ }
+
+ // Don't allow non-remote tabs with remote subframes.
+ if (NS_WARN_IF(aUseRemoteSubframes && !mUseRemoteTabs)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mUseRemoteSubframes = aUseRemoteSubframes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::GetUseTrackingProtection(
+ bool* aUseTrackingProtection) {
+ *aUseTrackingProtection = false;
+
+ if (GetForceEnableTrackingProtection() ||
+ StaticPrefs::privacy_trackingprotection_enabled() ||
+ (UsePrivateBrowsing() &&
+ StaticPrefs::privacy_trackingprotection_pbmode_enabled())) {
+ *aUseTrackingProtection = true;
+ return NS_OK;
+ }
+
+ if (GetParent()) {
+ return GetParent()->GetUseTrackingProtection(aUseTrackingProtection);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContext::SetUseTrackingProtection(
+ bool aUseTrackingProtection) {
+ return SetForceEnableTrackingProtection(aUseTrackingProtection);
+}
+
+NS_IMETHODIMP BrowsingContext::GetScriptableOriginAttributes(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
+ AssertOriginAttributesMatchPrivateBrowsing();
+
+ bool ok = ToJSValue(aCx, mOriginAttributes, aVal);
+ NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+BrowsingContext::GetOriginAttributes(OriginAttributes& aAttrs) {
+ aAttrs = mOriginAttributes;
+ AssertOriginAttributesMatchPrivateBrowsing();
+}
+
+nsresult BrowsingContext::SetOriginAttributes(const OriginAttributes& aAttrs) {
+ if (!CanSetOriginAttributes()) {
+ NS_WARNING("Attempt to set OriginAttributes when !CanSetOriginAttributes");
+ return NS_ERROR_FAILURE;
+ }
+
+ AssertOriginAttributesMatchPrivateBrowsing();
+ mOriginAttributes = aAttrs;
+
+ bool isPrivate = mOriginAttributes.mPrivateBrowsingId !=
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID;
+ // Chrome Browsing Context can not contain OriginAttributes.mPrivateBrowsingId
+ if (IsChrome() && isPrivate) {
+ mOriginAttributes.mPrivateBrowsingId =
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID;
+ }
+ SetPrivateBrowsing(isPrivate);
+ AssertOriginAttributesMatchPrivateBrowsing();
+
+ return NS_OK;
+}
+
+void BrowsingContext::AssertCoherentLoadContext() {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ // LoadContext should generally match our opener or parent.
+ if (IsContent()) {
+ if (RefPtr<BrowsingContext> opener = GetOpener()) {
+ MOZ_DIAGNOSTIC_ASSERT(opener->mType == mType);
+ MOZ_DIAGNOSTIC_ASSERT(opener->mGroup == mGroup);
+ MOZ_DIAGNOSTIC_ASSERT(opener->mUseRemoteTabs == mUseRemoteTabs);
+ MOZ_DIAGNOSTIC_ASSERT(opener->mUseRemoteSubframes == mUseRemoteSubframes);
+ MOZ_DIAGNOSTIC_ASSERT(opener->mPrivateBrowsingId == mPrivateBrowsingId);
+ MOZ_DIAGNOSTIC_ASSERT(
+ opener->mOriginAttributes.EqualsIgnoringFPD(mOriginAttributes));
+ }
+ }
+ if (RefPtr<BrowsingContext> parent = GetParent()) {
+ MOZ_DIAGNOSTIC_ASSERT(parent->mType == mType);
+ MOZ_DIAGNOSTIC_ASSERT(parent->mGroup == mGroup);
+ MOZ_DIAGNOSTIC_ASSERT(parent->mUseRemoteTabs == mUseRemoteTabs);
+ MOZ_DIAGNOSTIC_ASSERT(parent->mUseRemoteSubframes == mUseRemoteSubframes);
+ MOZ_DIAGNOSTIC_ASSERT(parent->mPrivateBrowsingId == mPrivateBrowsingId);
+ MOZ_DIAGNOSTIC_ASSERT(
+ parent->mOriginAttributes.EqualsIgnoringFPD(mOriginAttributes));
+ }
+
+ // UseRemoteSubframes and UseRemoteTabs must match.
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mUseRemoteSubframes || mUseRemoteTabs,
+ "Cannot set useRemoteSubframes without also setting useRemoteTabs");
+
+ // Double-check OriginAttributes/Private Browsing
+ AssertOriginAttributesMatchPrivateBrowsing();
+#endif
+}
+
+void BrowsingContext::AssertOriginAttributesMatchPrivateBrowsing() {
+ // Chrome browsing contexts must not have a private browsing OriginAttribute
+ // Content browsing contexts must maintain the equality:
+ // mOriginAttributes.mPrivateBrowsingId == mPrivateBrowsingId
+ if (IsChrome()) {
+ MOZ_DIAGNOSTIC_ASSERT(mOriginAttributes.mPrivateBrowsingId == 0);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(mOriginAttributes.mPrivateBrowsingId ==
+ mPrivateBrowsingId);
+ }
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BrowsingContext)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsILoadContext)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(BrowsingContext)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(BrowsingContext)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(BrowsingContext)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(BrowsingContext)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(BrowsingContext)
+ if (sBrowsingContexts) {
+ sBrowsingContexts->Remove(tmp->Id());
+ }
+ UnregisterBrowserId(tmp);
+
+ if (tmp->GetIsPopupSpam()) {
+ PopupBlocker::UnregisterOpenPopupSpam();
+ // NOTE: Doesn't use SetIsPopupSpam, as it will be set all processes
+ // automatically.
+ tmp->mFields.SetWithoutSyncing<IDX_IsPopupSpam>(false);
+ }
+
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(
+ mDocShell, mParentWindow, mGroup, mEmbedderElement, mWindowContexts,
+ mCurrentWindowContext, mSessionStorageManager, mChildSessionHistory)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(BrowsingContext)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
+ mDocShell, mParentWindow, mGroup, mEmbedderElement, mWindowContexts,
+ mCurrentWindowContext, mSessionStorageManager, mChildSessionHistory)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+static bool IsCertainlyAliveForCC(BrowsingContext* aContext) {
+ return aContext->HasKnownLiveWrapper() ||
+ (AppShutdown::GetCurrentShutdownPhase() ==
+ ShutdownPhase::NotInShutdown &&
+ aContext->EverAttached() && !aContext->IsDiscarded());
+}
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(BrowsingContext)
+ if (IsCertainlyAliveForCC(tmp)) {
+ if (tmp->PreservingWrapper()) {
+ tmp->MarkWrapperLive();
+ }
+ return true;
+ }
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(BrowsingContext)
+ return IsCertainlyAliveForCC(tmp) && tmp->HasNothingToTrace(tmp);
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
+
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(BrowsingContext)
+ return IsCertainlyAliveForCC(tmp);
+NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
+
+class RemoteLocationProxy
+ : public RemoteObjectProxy<BrowsingContext::LocationProxy,
+ Location_Binding::sCrossOriginProperties> {
+ public:
+ typedef RemoteObjectProxy Base;
+
+ constexpr RemoteLocationProxy()
+ : RemoteObjectProxy(prototypes::id::Location) {}
+
+ void NoteChildren(JSObject* aProxy,
+ nsCycleCollectionTraversalCallback& aCb) const override {
+ auto location =
+ static_cast<BrowsingContext::LocationProxy*>(GetNative(aProxy));
+ CycleCollectionNoteChild(aCb, location->GetBrowsingContext(),
+ "JS::GetPrivate(obj)->GetBrowsingContext()");
+ }
+};
+
+static const RemoteLocationProxy sSingleton;
+
+// Give RemoteLocationProxy 2 reserved slots, like the other wrappers,
+// so JSObject::swap can swap it with CrossCompartmentWrappers without requiring
+// malloc.
+template <>
+const JSClass RemoteLocationProxy::Base::sClass =
+ PROXY_CLASS_DEF("Proxy", JSCLASS_HAS_RESERVED_SLOTS(2));
+
+void BrowsingContext::Location(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aLocation,
+ ErrorResult& aError) {
+ aError.MightThrowJSException();
+ sSingleton.GetProxyObject(aCx, &mLocation, /* aTransplantTo = */ nullptr,
+ aLocation);
+ if (!aLocation) {
+ aError.StealExceptionFromJSContext(aCx);
+ }
+}
+
+bool BrowsingContext::RemoveRootFromBFCacheSync() {
+ if (WindowContext* wc = GetParentWindowContext()) {
+ if (RefPtr<Document> doc = wc->TopWindowContext()->GetDocument()) {
+ return doc->RemoveFromBFCacheSync();
+ }
+ }
+ return false;
+}
+
+nsresult BrowsingContext::CheckSandboxFlags(nsDocShellLoadState* aLoadState) {
+ const auto& sourceBC = aLoadState->SourceBrowsingContext();
+ if (sourceBC.IsNull()) {
+ return NS_OK;
+ }
+
+ // We might be called after the source BC has been discarded, but before we've
+ // destroyed our in-process instance of the BrowsingContext object in some
+ // situations (e.g. after creating a new pop-up with window.open while the
+ // window is being closed). In these situations we want to still perform the
+ // sandboxing check against our in-process copy. If we've forgotten about the
+ // context already, assume it is sanboxed. (bug 1643450)
+ BrowsingContext* bc = sourceBC.GetMaybeDiscarded();
+ if (!bc || bc->IsSandboxedFrom(this)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ return NS_OK;
+}
+
+nsresult BrowsingContext::LoadURI(nsDocShellLoadState* aLoadState,
+ bool aSetNavigating) {
+ // Per spec, most load attempts are silently ignored when a BrowsingContext is
+ // null (which in our code corresponds to discarded), so we simply fail
+ // silently in those cases. Regardless, we cannot trigger loads in/from
+ // discarded BrowsingContexts via IPC, so we need to abort in any case.
+ if (IsDiscarded()) {
+ return NS_OK;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aLoadState->TargetBrowsingContext().IsNull(),
+ "Targeting occurs in InternalLoad");
+ aLoadState->AssertProcessCouldTriggerLoadIfSystem();
+
+ if (mDocShell) {
+ nsCOMPtr<nsIDocShell> docShell = mDocShell;
+ return docShell->LoadURI(aLoadState, aSetNavigating);
+ }
+
+ // Note: We do this check both here and in `nsDocShell::InternalLoad`, since
+ // document-specific sandbox flags are only available in the process
+ // triggering the load, and we don't want the target process to have to trust
+ // the triggering process to do the appropriate checks for the
+ // BrowsingContext's sandbox flags.
+ MOZ_TRY(CheckSandboxFlags(aLoadState));
+ SetTriggeringAndInheritPrincipals(aLoadState->TriggeringPrincipal(),
+ aLoadState->PrincipalToInherit(),
+ aLoadState->GetLoadIdentifier());
+
+ const auto& sourceBC = aLoadState->SourceBrowsingContext();
+
+ if (net::SchemeIsJavascript(aLoadState->URI())) {
+ if (!XRE_IsParentProcess()) {
+ // Web content should only be able to load javascript: URIs into documents
+ // whose principals the caller principal subsumes, which by definition
+ // excludes any document in a cross-process BrowsingContext.
+ return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(!sourceBC,
+ "Should never see a cross-process javascript: load "
+ "triggered from content");
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!sourceBC || sourceBC->Group() == Group());
+ if (sourceBC && sourceBC->IsInProcess()) {
+ nsCOMPtr<nsPIDOMWindowOuter> win(sourceBC->GetDOMWindow());
+ if (WindowGlobalChild* wgc =
+ win->GetCurrentInnerWindow()->GetWindowGlobalChild()) {
+ if (!wgc->CanNavigate(this)) {
+ return NS_ERROR_DOM_PROP_ACCESS_DENIED;
+ }
+ wgc->SendLoadURI(this, mozilla::WrapNotNull(aLoadState), aSetNavigating);
+ }
+ } else if (XRE_IsParentProcess()) {
+ if (Canonical()->LoadInParent(aLoadState, aSetNavigating)) {
+ return NS_OK;
+ }
+
+ if (ContentParent* cp = Canonical()->GetContentParent()) {
+ // Attempt to initiate this load immediately in the parent, if it succeeds
+ // it'll return a unique identifier so that we can find it later.
+ uint64_t loadIdentifier = 0;
+ if (Canonical()->AttemptSpeculativeLoadInParent(aLoadState)) {
+ MOZ_DIAGNOSTIC_ASSERT(GetCurrentLoadIdentifier().isSome());
+ loadIdentifier = GetCurrentLoadIdentifier().value();
+ aLoadState->SetChannelInitialized(true);
+ }
+
+ cp->TransmitBlobDataIfBlobURL(aLoadState->URI());
+
+ // Setup a confirmation callback once the content process receives this
+ // load. Normally we'd expect a PDocumentChannel actor to have been
+ // created to claim the load identifier by that time. If not, then it
+ // won't be coming, so make sure we clean up and deregister.
+ cp->SendLoadURI(this, mozilla::WrapNotNull(aLoadState), aSetNavigating)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [loadIdentifier](
+ const PContentParent::LoadURIPromise::ResolveOrRejectValue&
+ aValue) {
+ if (loadIdentifier) {
+ net::DocumentLoadListener::CleanupParentLoadAttempt(
+ loadIdentifier);
+ }
+ });
+ }
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(sourceBC);
+ if (!sourceBC) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ // If we're in a content process and the source BC is no longer in-process,
+ // just fail silently.
+ }
+ return NS_OK;
+}
+
+nsresult BrowsingContext::InternalLoad(nsDocShellLoadState* aLoadState) {
+ if (IsDiscarded()) {
+ return NS_OK;
+ }
+ SetTriggeringAndInheritPrincipals(aLoadState->TriggeringPrincipal(),
+ aLoadState->PrincipalToInherit(),
+ aLoadState->GetLoadIdentifier());
+
+ MOZ_DIAGNOSTIC_ASSERT(aLoadState->Target().IsEmpty(),
+ "should already have retargeted");
+ MOZ_DIAGNOSTIC_ASSERT(!aLoadState->TargetBrowsingContext().IsNull(),
+ "should have target bc set");
+ MOZ_DIAGNOSTIC_ASSERT(aLoadState->TargetBrowsingContext() == this,
+ "must be targeting this BrowsingContext");
+ aLoadState->AssertProcessCouldTriggerLoadIfSystem();
+
+ if (mDocShell) {
+ RefPtr<nsDocShell> docShell = nsDocShell::Cast(mDocShell);
+ return docShell->InternalLoad(aLoadState);
+ }
+
+ // Note: We do this check both here and in `nsDocShell::InternalLoad`, since
+ // document-specific sandbox flags are only available in the process
+ // triggering the load, and we don't want the target process to have to trust
+ // the triggering process to do the appropriate checks for the
+ // BrowsingContext's sandbox flags.
+ MOZ_TRY(CheckSandboxFlags(aLoadState));
+
+ const auto& sourceBC = aLoadState->SourceBrowsingContext();
+
+ if (net::SchemeIsJavascript(aLoadState->URI())) {
+ if (!XRE_IsParentProcess()) {
+ // Web content should only be able to load javascript: URIs into documents
+ // whose principals the caller principal subsumes, which by definition
+ // excludes any document in a cross-process BrowsingContext.
+ return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(!sourceBC,
+ "Should never see a cross-process javascript: load "
+ "triggered from content");
+ }
+
+ if (XRE_IsParentProcess()) {
+ ContentParent* cp = Canonical()->GetContentParent();
+ if (!cp || !cp->CanSend()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SetCurrentLoadIdentifier(Some(aLoadState->GetLoadIdentifier())));
+ Unused << cp->SendInternalLoad(mozilla::WrapNotNull(aLoadState));
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(sourceBC);
+ MOZ_DIAGNOSTIC_ASSERT(sourceBC->Group() == Group());
+
+ nsCOMPtr<nsPIDOMWindowOuter> win(sourceBC->GetDOMWindow());
+ WindowGlobalChild* wgc =
+ win->GetCurrentInnerWindow()->GetWindowGlobalChild();
+ if (!wgc || !wgc->CanSend()) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!wgc->CanNavigate(this)) {
+ return NS_ERROR_DOM_PROP_ACCESS_DENIED;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SetCurrentLoadIdentifier(Some(aLoadState->GetLoadIdentifier())));
+ wgc->SendInternalLoad(mozilla::WrapNotNull(aLoadState));
+ }
+
+ return NS_OK;
+}
+
+void BrowsingContext::DisplayLoadError(const nsAString& aURI) {
+ MOZ_LOG(GetLog(), LogLevel::Debug, ("DisplayLoadError"));
+ MOZ_DIAGNOSTIC_ASSERT(!IsDiscarded());
+ MOZ_DIAGNOSTIC_ASSERT(mDocShell || XRE_IsParentProcess());
+
+ if (mDocShell) {
+ bool didDisplayLoadError = false;
+ nsCOMPtr<nsIDocShell> docShell = mDocShell;
+ docShell->DisplayLoadError(NS_ERROR_MALFORMED_URI, nullptr,
+ PromiseFlatString(aURI).get(), nullptr,
+ &didDisplayLoadError);
+ } else {
+ if (ContentParent* cp = Canonical()->GetContentParent()) {
+ Unused << cp->SendDisplayLoadError(this, PromiseFlatString(aURI));
+ }
+ }
+}
+
+WindowProxyHolder BrowsingContext::Window() {
+ return WindowProxyHolder(Self());
+}
+
+WindowProxyHolder BrowsingContext::GetFrames(ErrorResult& aError) {
+ return Window();
+}
+
+void BrowsingContext::Close(CallerType aCallerType, ErrorResult& aError) {
+ if (mIsDiscarded) {
+ return;
+ }
+
+ if (IsSubframe()) {
+ // .close() on frames is a no-op.
+ return;
+ }
+
+ if (GetDOMWindow()) {
+ nsGlobalWindowOuter::Cast(GetDOMWindow())
+ ->CloseOuter(aCallerType == CallerType::System);
+ return;
+ }
+
+ // This is a bit of a hack for webcompat. Content needs to see an updated
+ // |window.closed| value as early as possible, so we set this before we
+ // actually send the DOMWindowClose event, which happens in the process where
+ // the document for this browsing context is loaded.
+ MOZ_ALWAYS_SUCCEEDS(SetClosed(true));
+
+ if (ContentChild* cc = ContentChild::GetSingleton()) {
+ cc->SendWindowClose(this, aCallerType == CallerType::System);
+ } else if (ContentParent* cp = Canonical()->GetContentParent()) {
+ Unused << cp->SendWindowClose(this, aCallerType == CallerType::System);
+ }
+}
+
+template <typename FuncT>
+inline bool ApplyToDocumentsForPopup(Document* doc, FuncT func) {
+ // HACK: Some pages using bogus library + UA sniffing call window.open()
+ // from a blank iframe, only on Firefox, see bug 1685056.
+ //
+ // This is a hack-around to preserve behavior in that particular and
+ // specific case, by consuming activation on the parent document, so we
+ // don't care about the InProcessParent bits not being fission-safe or what
+ // not.
+ if (func(doc)) {
+ return true;
+ }
+ if (!doc->IsInitialDocument()) {
+ return false;
+ }
+ Document* parentDoc = doc->GetInProcessParentDocument();
+ if (!parentDoc || !parentDoc->NodePrincipal()->Equals(doc->NodePrincipal())) {
+ return false;
+ }
+ return func(parentDoc);
+}
+
+PopupBlocker::PopupControlState BrowsingContext::RevisePopupAbuseLevel(
+ PopupBlocker::PopupControlState aControl) {
+ if (!IsContent()) {
+ return PopupBlocker::openAllowed;
+ }
+
+ RefPtr<Document> doc = GetExtantDocument();
+ PopupBlocker::PopupControlState abuse = aControl;
+ switch (abuse) {
+ case PopupBlocker::openControlled:
+ case PopupBlocker::openBlocked:
+ case PopupBlocker::openOverridden:
+ if (IsPopupAllowed()) {
+ abuse = PopupBlocker::PopupControlState(abuse - 1);
+ }
+ break;
+ case PopupBlocker::openAbused:
+ if (IsPopupAllowed() ||
+ (doc && doc->HasValidTransientUserGestureActivation())) {
+ // Skip PopupBlocker::openBlocked
+ abuse = PopupBlocker::openControlled;
+ }
+ break;
+ case PopupBlocker::openAllowed:
+ break;
+ default:
+ NS_WARNING("Strange PopupControlState!");
+ }
+
+ // limit the number of simultaneously open popups
+ if (abuse == PopupBlocker::openAbused || abuse == PopupBlocker::openBlocked ||
+ abuse == PopupBlocker::openControlled) {
+ int32_t popupMax = StaticPrefs::dom_popup_maximum();
+ if (popupMax >= 0 &&
+ PopupBlocker::GetOpenPopupSpamCount() >= (uint32_t)popupMax) {
+ abuse = PopupBlocker::openOverridden;
+ }
+ }
+
+ // If we're currently in-process, attempt to consume transient user gesture
+ // activations.
+ if (doc) {
+ auto ConsumeTransientUserActivationForMultiplePopupBlocking =
+ [&]() -> bool {
+ return ApplyToDocumentsForPopup(doc, [](Document* doc) {
+ return doc->ConsumeTransientUserGestureActivation();
+ });
+ };
+
+ // If this popup is allowed, let's block any other for this event, forcing
+ // PopupBlocker::openBlocked state.
+ if ((abuse == PopupBlocker::openAllowed ||
+ abuse == PopupBlocker::openControlled) &&
+ StaticPrefs::dom_block_multiple_popups() && !IsPopupAllowed() &&
+ !ConsumeTransientUserActivationForMultiplePopupBlocking()) {
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
+ doc, nsContentUtils::eDOM_PROPERTIES,
+ "MultiplePopupsBlockedNoUserActivation");
+ abuse = PopupBlocker::openBlocked;
+ }
+ }
+
+ return abuse;
+}
+
+void BrowsingContext::GetUserActivationModifiersForPopup(
+ UserActivation::Modifiers* aModifiers) {
+ RefPtr<Document> doc = GetExtantDocument();
+ if (doc) {
+ // Unlike RevisePopupAbuseLevel, modifiers can always be used regardless
+ // of PopupControlState.
+ (void)ApplyToDocumentsForPopup(doc, [&](Document* doc) {
+ return doc->GetTransientUserGestureActivationModifiers(aModifiers);
+ });
+ }
+}
+
+void BrowsingContext::IncrementHistoryEntryCountForBrowsingContext() {
+ Unused << SetHistoryEntryCount(GetHistoryEntryCount() + 1);
+}
+
+std::tuple<bool, bool> BrowsingContext::CanFocusCheck(CallerType aCallerType) {
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (!fm) {
+ return {false, false};
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> caller = do_QueryInterface(GetEntryGlobal());
+ BrowsingContext* callerBC = caller ? caller->GetBrowsingContext() : nullptr;
+ RefPtr<BrowsingContext> openerBC = GetOpener();
+ MOZ_DIAGNOSTIC_ASSERT(!openerBC || openerBC->Group() == Group());
+
+ // Enforce dom.disable_window_flip (for non-chrome), but still allow the
+ // window which opened us to raise us at times when popups are allowed
+ // (bugs 355482 and 369306).
+ bool canFocus = aCallerType == CallerType::System ||
+ !Preferences::GetBool("dom.disable_window_flip", true);
+ if (!canFocus && openerBC == callerBC) {
+ canFocus =
+ (callerBC ? callerBC : this)
+ ->RevisePopupAbuseLevel(PopupBlocker::GetPopupControlState()) <
+ PopupBlocker::openBlocked;
+ }
+
+ bool isActive = false;
+ if (XRE_IsParentProcess()) {
+ CanonicalBrowsingContext* chromeTop = Canonical()->TopCrossChromeBoundary();
+ nsCOMPtr<nsPIDOMWindowOuter> activeWindow = fm->GetActiveWindow();
+ isActive = activeWindow == chromeTop->GetDOMWindow();
+ } else {
+ isActive = fm->GetActiveBrowsingContext() == Top();
+ }
+
+ return {canFocus, isActive};
+}
+
+void BrowsingContext::Focus(CallerType aCallerType, ErrorResult& aError) {
+ // These checks need to happen before the RequestFrameFocus call, which
+ // is why they are done in an untrusted process. If we wanted to enforce
+ // these in the parent, we'd need to do the checks there _also_.
+ // These should be kept in sync with nsGlobalWindowOuter::FocusOuter.
+
+ auto [canFocus, isActive] = CanFocusCheck(aCallerType);
+
+ if (!(canFocus || isActive)) {
+ return;
+ }
+
+ // Permission check passed
+
+ if (mEmbedderElement) {
+ // Make the activeElement in this process update synchronously.
+ nsContentUtils::RequestFrameFocus(*mEmbedderElement, true, aCallerType);
+ }
+ uint64_t actionId = nsFocusManager::GenerateFocusActionId();
+ if (ContentChild* cc = ContentChild::GetSingleton()) {
+ cc->SendWindowFocus(this, aCallerType, actionId);
+ } else if (ContentParent* cp = Canonical()->GetContentParent()) {
+ Unused << cp->SendWindowFocus(this, aCallerType, actionId);
+ }
+}
+
+bool BrowsingContext::CanBlurCheck(CallerType aCallerType) {
+ // If dom.disable_window_flip == true, then content should not be allowed
+ // to do blur (this would allow popunders, bug 369306)
+ return aCallerType == CallerType::System ||
+ !Preferences::GetBool("dom.disable_window_flip", true);
+}
+
+void BrowsingContext::Blur(CallerType aCallerType, ErrorResult& aError) {
+ if (!CanBlurCheck(aCallerType)) {
+ return;
+ }
+
+ if (ContentChild* cc = ContentChild::GetSingleton()) {
+ cc->SendWindowBlur(this, aCallerType);
+ } else if (ContentParent* cp = Canonical()->GetContentParent()) {
+ Unused << cp->SendWindowBlur(this, aCallerType);
+ }
+}
+
+Nullable<WindowProxyHolder> BrowsingContext::GetWindow() {
+ if (XRE_IsParentProcess() && !IsInProcess()) {
+ return nullptr;
+ }
+ return WindowProxyHolder(this);
+}
+
+Nullable<WindowProxyHolder> BrowsingContext::GetTop(ErrorResult& aError) {
+ if (mIsDiscarded) {
+ return nullptr;
+ }
+
+ // We never return null or throw an error, but the implementation in
+ // nsGlobalWindow does and we need to use the same signature.
+ return WindowProxyHolder(Top());
+}
+
+void BrowsingContext::GetOpener(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aOpener,
+ ErrorResult& aError) const {
+ RefPtr<BrowsingContext> opener = GetOpener();
+ if (!opener) {
+ aOpener.setNull();
+ return;
+ }
+
+ if (!ToJSValue(aCx, WindowProxyHolder(opener), aOpener)) {
+ aError.NoteJSContextException(aCx);
+ }
+}
+
+// We never throw an error, but the implementation in nsGlobalWindow does and
+// we need to use the same signature.
+Nullable<WindowProxyHolder> BrowsingContext::GetParent(ErrorResult& aError) {
+ if (mIsDiscarded) {
+ return nullptr;
+ }
+
+ if (GetParent()) {
+ return WindowProxyHolder(GetParent());
+ }
+ return WindowProxyHolder(this);
+}
+
+void BrowsingContext::PostMessageMoz(JSContext* aCx,
+ JS::Handle<JS::Value> aMessage,
+ const nsAString& aTargetOrigin,
+ const Sequence<JSObject*>& aTransfer,
+ nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ if (mIsDiscarded) {
+ return;
+ }
+
+ RefPtr<BrowsingContext> sourceBc;
+ PostMessageData data;
+ data.targetOrigin() = aTargetOrigin;
+ data.subjectPrincipal() = &aSubjectPrincipal;
+ RefPtr<nsGlobalWindowInner> callerInnerWindow;
+ nsAutoCString scriptLocation;
+ // We don't need to get the caller's agentClusterId since that is used for
+ // checking whether it's okay to sharing memory (and it's not allowed to share
+ // memory cross processes)
+ if (!nsGlobalWindowOuter::GatherPostMessageData(
+ aCx, aTargetOrigin, getter_AddRefs(sourceBc), data.origin(),
+ getter_AddRefs(data.targetOriginURI()),
+ getter_AddRefs(data.callerPrincipal()),
+ getter_AddRefs(callerInnerWindow), getter_AddRefs(data.callerURI()),
+ /* aCallerAgentClusterId */ nullptr, &scriptLocation, aError)) {
+ return;
+ }
+ if (sourceBc && sourceBc->IsDiscarded()) {
+ return;
+ }
+ data.source() = sourceBc;
+ data.isFromPrivateWindow() =
+ callerInnerWindow &&
+ nsScriptErrorBase::ComputeIsFromPrivateWindow(callerInnerWindow);
+ data.innerWindowId() = callerInnerWindow ? callerInnerWindow->WindowID() : 0;
+ data.scriptLocation() = scriptLocation;
+ JS::Rooted<JS::Value> transferArray(aCx);
+ aError = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransfer,
+ &transferArray);
+ if (NS_WARN_IF(aError.Failed())) {
+ return;
+ }
+
+ JS::CloneDataPolicy clonePolicy;
+ if (callerInnerWindow && callerInnerWindow->IsSharedMemoryAllowed()) {
+ clonePolicy.allowSharedMemoryObjects();
+ }
+
+ // We will see if the message is required to be in the same process or it can
+ // be in the different process after Write().
+ ipc::StructuredCloneData message = ipc::StructuredCloneData(
+ StructuredCloneHolder::StructuredCloneScope::UnknownDestination,
+ StructuredCloneHolder::TransferringSupported);
+ message.Write(aCx, aMessage, transferArray, clonePolicy, aError);
+ if (NS_WARN_IF(aError.Failed())) {
+ return;
+ }
+
+ ClonedOrErrorMessageData messageData;
+ if (ContentChild* cc = ContentChild::GetSingleton()) {
+ // The clone scope gets set when we write the message data based on the
+ // requirements of that data that we're writing.
+ // If the message data contains a shared memory object, then CloneScope
+ // would return SameProcess. Otherwise, it returns DifferentProcess.
+ if (message.CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::DifferentProcess) {
+ ClonedMessageData clonedMessageData;
+ if (!message.BuildClonedMessageData(clonedMessageData)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ messageData = std::move(clonedMessageData);
+ } else {
+ MOZ_ASSERT(message.CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::SameProcess);
+
+ messageData = ErrorMessageData();
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "DOM Window"_ns,
+ callerInnerWindow ? callerInnerWindow->GetDocument() : nullptr,
+ nsContentUtils::eDOM_PROPERTIES,
+ "PostMessageSharedMemoryObjectToCrossOriginWarning");
+ }
+
+ cc->SendWindowPostMessage(this, messageData, data);
+ } else if (ContentParent* cp = Canonical()->GetContentParent()) {
+ if (message.CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::DifferentProcess) {
+ ClonedMessageData clonedMessageData;
+ if (!message.BuildClonedMessageData(clonedMessageData)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ messageData = std::move(clonedMessageData);
+ } else {
+ MOZ_ASSERT(message.CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::SameProcess);
+
+ messageData = ErrorMessageData();
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "DOM Window"_ns,
+ callerInnerWindow ? callerInnerWindow->GetDocument() : nullptr,
+ nsContentUtils::eDOM_PROPERTIES,
+ "PostMessageSharedMemoryObjectToCrossOriginWarning");
+ }
+
+ Unused << cp->SendWindowPostMessage(this, messageData, data);
+ }
+}
+
+void BrowsingContext::PostMessageMoz(JSContext* aCx,
+ JS::Handle<JS::Value> aMessage,
+ const WindowPostMessageOptions& aOptions,
+ nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ PostMessageMoz(aCx, aMessage, aOptions.mTargetOrigin, aOptions.mTransfer,
+ aSubjectPrincipal, aError);
+}
+
+void BrowsingContext::SendCommitTransaction(ContentParent* aParent,
+ const BaseTransaction& aTxn,
+ uint64_t aEpoch) {
+ Unused << aParent->SendCommitBrowsingContextTransaction(this, aTxn, aEpoch);
+}
+
+void BrowsingContext::SendCommitTransaction(ContentChild* aChild,
+ const BaseTransaction& aTxn,
+ uint64_t aEpoch) {
+ aChild->SendCommitBrowsingContextTransaction(this, aTxn, aEpoch);
+}
+
+BrowsingContext::IPCInitializer BrowsingContext::GetIPCInitializer() {
+ MOZ_DIAGNOSTIC_ASSERT(mEverAttached);
+ MOZ_DIAGNOSTIC_ASSERT(mType == Type::Content);
+
+ IPCInitializer init;
+ init.mId = Id();
+ init.mParentId = mParentWindow ? mParentWindow->Id() : 0;
+ init.mWindowless = mWindowless;
+ init.mUseRemoteTabs = mUseRemoteTabs;
+ init.mUseRemoteSubframes = mUseRemoteSubframes;
+ init.mCreatedDynamically = mCreatedDynamically;
+ init.mChildOffset = mChildOffset;
+ init.mOriginAttributes = mOriginAttributes;
+ if (mChildSessionHistory && mozilla::SessionHistoryInParent()) {
+ init.mSessionHistoryIndex = mChildSessionHistory->Index();
+ init.mSessionHistoryCount = mChildSessionHistory->Count();
+ }
+ init.mRequestContextId = mRequestContextId;
+ init.mFields = mFields.RawValues();
+ return init;
+}
+
+already_AddRefed<WindowContext> BrowsingContext::IPCInitializer::GetParent() {
+ RefPtr<WindowContext> parent;
+ if (mParentId != 0) {
+ parent = WindowContext::GetById(mParentId);
+ MOZ_RELEASE_ASSERT(parent);
+ }
+ return parent.forget();
+}
+
+already_AddRefed<BrowsingContext> BrowsingContext::IPCInitializer::GetOpener() {
+ RefPtr<BrowsingContext> opener;
+ if (GetOpenerId() != 0) {
+ opener = BrowsingContext::Get(GetOpenerId());
+ MOZ_RELEASE_ASSERT(opener);
+ }
+ return opener.forget();
+}
+
+void BrowsingContext::StartDelayedAutoplayMediaComponents() {
+ if (!mDocShell) {
+ return;
+ }
+ AUTOPLAY_LOG("%s : StartDelayedAutoplayMediaComponents for bc 0x%08" PRIx64,
+ XRE_IsParentProcess() ? "Parent" : "Child", Id());
+ mDocShell->StartDelayedAutoplayMediaComponents();
+}
+
+nsresult BrowsingContext::ResetGVAutoplayRequestStatus() {
+ MOZ_ASSERT(IsTop(),
+ "Should only set GVAudibleAutoplayRequestStatus in the top-level "
+ "browsing context");
+
+ Transaction txn;
+ txn.SetGVAudibleAutoplayRequestStatus(GVAutoplayRequestStatus::eUNKNOWN);
+ txn.SetGVInaudibleAutoplayRequestStatus(GVAutoplayRequestStatus::eUNKNOWN);
+ return txn.Commit(this);
+}
+
+template <typename Callback>
+void BrowsingContext::WalkPresContexts(Callback&& aCallback) {
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ if (nsIDocShell* shell = aContext->GetDocShell()) {
+ if (RefPtr pc = shell->GetPresContext()) {
+ aCallback(pc.get());
+ }
+ }
+ });
+}
+
+void BrowsingContext::PresContextAffectingFieldChanged() {
+ WalkPresContexts([&](nsPresContext* aPc) {
+ aPc->RecomputeBrowsingContextDependentData();
+ });
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_SessionStoreEpoch>,
+ uint32_t aOldValue) {
+ if (!mCurrentWindowContext) {
+ return;
+ }
+ SessionStoreChild* sessionStoreChild =
+ SessionStoreChild::From(mCurrentWindowContext->GetWindowGlobalChild());
+ if (!sessionStoreChild) {
+ return;
+ }
+
+ sessionStoreChild->SetEpoch(GetSessionStoreEpoch());
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_GVAudibleAutoplayRequestStatus>) {
+ MOZ_ASSERT(IsTop(),
+ "Should only set GVAudibleAutoplayRequestStatus in the top-level "
+ "browsing context");
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_GVInaudibleAutoplayRequestStatus>) {
+ MOZ_ASSERT(IsTop(),
+ "Should only set GVAudibleAutoplayRequestStatus in the top-level "
+ "browsing context");
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_ExplicitActive>,
+ const ExplicitActiveStatus&,
+ ContentParent* aSource) {
+ return XRE_IsParentProcess() && IsTop() && !aSource;
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_ExplicitActive>,
+ ExplicitActiveStatus aOldValue) {
+ MOZ_ASSERT(IsTop());
+
+ const bool isActive = IsActive();
+ const bool wasActive = [&] {
+ if (aOldValue != ExplicitActiveStatus::None) {
+ return aOldValue == ExplicitActiveStatus::Active;
+ }
+ return GetParent() && GetParent()->IsActive();
+ }();
+
+ if (isActive == wasActive) {
+ return;
+ }
+
+ Group()->UpdateToplevelsSuspendedIfNeeded();
+ if (XRE_IsParentProcess()) {
+ if (BrowserParent* bp = Canonical()->GetBrowserParent()) {
+ bp->RecomputeProcessPriority();
+#if defined(XP_WIN) && defined(ACCESSIBILITY)
+ if (a11y::Compatibility::IsDolphin()) {
+ // update active accessible documents on windows
+ if (a11y::DocAccessibleParent* tabDoc =
+ bp->GetTopLevelDocAccessible()) {
+ HWND window = tabDoc->GetEmulatedWindowHandle();
+ MOZ_ASSERT(window);
+ if (window) {
+ if (isActive) {
+ a11y::nsWinUtils::ShowNativeWindow(window);
+ } else {
+ a11y::nsWinUtils::HideNativeWindow(window);
+ }
+ }
+ }
+ }
+#endif
+ }
+
+ // NOTE(emilio): Ideally we'd want to reuse the ExplicitActiveStatus::None
+ // set-up, but that's non-trivial to do because in content processes we
+ // can't access the top-cross-chrome-boundary bc.
+ auto manageTopDescendant = [&](auto* aChild) {
+ if (!aChild->ManuallyManagesActiveness()) {
+ aChild->SetIsActiveInternal(isActive, IgnoreErrors());
+ if (BrowserParent* bp = aChild->GetBrowserParent()) {
+ bp->SetRenderLayers(isActive);
+ }
+ }
+ return CallState::Continue;
+ };
+ Canonical()->CallOnAllTopDescendants(manageTopDescendant,
+ /* aIncludeNestedBrowsers = */ false);
+ }
+
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ if (nsCOMPtr<nsIDocShell> ds = aContext->GetDocShell()) {
+ nsDocShell::Cast(ds)->ActivenessMaybeChanged();
+ }
+ });
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_InRDMPane>, bool aOldValue) {
+ MOZ_ASSERT(IsTop(),
+ "Should only set InRDMPane in the top-level browsing context");
+ if (GetInRDMPane() == aOldValue) {
+ return;
+ }
+ PresContextAffectingFieldChanged();
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_PageAwakeRequestCount>,
+ uint32_t aNewValue, ContentParent* aSource) {
+ return IsTop() && XRE_IsParentProcess() && !aSource;
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_PageAwakeRequestCount>,
+ uint32_t aOldValue) {
+ if (!IsTop() || aOldValue == GetPageAwakeRequestCount()) {
+ return;
+ }
+ Group()->UpdateToplevelsSuspendedIfNeeded();
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+ ContentParent* aSource) -> CanSetResult {
+ if (mozilla::SessionHistoryInParent()) {
+ return XRE_IsParentProcess() && !aSource ? CanSetResult::Allow
+ : CanSetResult::Deny;
+ }
+
+ // Without Session History in Parent, session restore code still needs to set
+ // this from content processes.
+ return LegacyRevertIfNotOwningOrParentProcess(aSource);
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue) {
+ RecomputeCanExecuteScripts();
+}
+
+void BrowsingContext::RecomputeCanExecuteScripts() {
+ const bool old = mCanExecuteScripts;
+ if (!AllowJavascript()) {
+ // Scripting has been explicitly disabled on our BrowsingContext.
+ mCanExecuteScripts = false;
+ } else if (GetParentWindowContext()) {
+ // Otherwise, inherit parent.
+ mCanExecuteScripts = GetParentWindowContext()->CanExecuteScripts();
+ } else {
+ // Otherwise, we're the root of the tree, and we haven't explicitly disabled
+ // script. Allow.
+ mCanExecuteScripts = true;
+ }
+
+ if (old != mCanExecuteScripts) {
+ for (WindowContext* wc : GetWindowContexts()) {
+ wc->RecomputeCanExecuteScripts();
+ }
+ }
+}
+
+bool BrowsingContext::InactiveForSuspend() const {
+ if (!StaticPrefs::dom_suspend_inactive_enabled()) {
+ return false;
+ }
+ // We should suspend a page only when it's inactive and doesn't have any awake
+ // request that is used to prevent page from being suspended because web page
+ // might still need to run their script. Eg. waiting for media keys to resume
+ // media, playing web audio, waiting in a video call conference room.
+ return !IsActive() && GetPageAwakeRequestCount() == 0;
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_TouchEventsOverrideInternal>,
+ dom::TouchEventsOverride, ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource;
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_TouchEventsOverrideInternal>,
+ dom::TouchEventsOverride&& aOldValue) {
+ if (GetTouchEventsOverrideInternal() == aOldValue) {
+ return;
+ }
+ WalkPresContexts([&](nsPresContext* aPc) {
+ aPc->MediaFeatureValuesChanged(
+ {MediaFeatureChangeReason::SystemMetricsChange},
+ // We're already iterating through sub documents, so we don't need to
+ // propagate the change again.
+ MediaFeatureChangePropagation::JustThisDocument);
+ });
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_EmbedderColorSchemes>,
+ EmbedderColorSchemes&& aOldValue) {
+ if (GetEmbedderColorSchemes() == aOldValue) {
+ return;
+ }
+ PresContextAffectingFieldChanged();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_PrefersColorSchemeOverride>,
+ dom::PrefersColorSchemeOverride aOldValue) {
+ MOZ_ASSERT(IsTop());
+ if (PrefersColorSchemeOverride() == aOldValue) {
+ return;
+ }
+ PresContextAffectingFieldChanged();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_MediumOverride>,
+ nsString&& aOldValue) {
+ MOZ_ASSERT(IsTop());
+ if (GetMediumOverride() == aOldValue) {
+ return;
+ }
+ PresContextAffectingFieldChanged();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_DisplayMode>,
+ enum DisplayMode aOldValue) {
+ MOZ_ASSERT(IsTop());
+
+ if (GetDisplayMode() == aOldValue) {
+ return;
+ }
+
+ WalkPresContexts([&](nsPresContext* aPc) {
+ aPc->MediaFeatureValuesChanged(
+ {MediaFeatureChangeReason::DisplayModeChange},
+ // We're already iterating through sub documents, so we don't need
+ // to propagate the change again.
+ //
+ // Images and other resources don't change their display-mode
+ // evaluation, display-mode is a property of the browsing context.
+ MediaFeatureChangePropagation::JustThisDocument);
+ });
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_Muted>) {
+ MOZ_ASSERT(IsTop(), "Set muted flag on non top-level context!");
+ USER_ACTIVATION_LOG("Set audio muted %d for %s browsing context 0x%08" PRIx64,
+ GetMuted(), XRE_IsParentProcess() ? "Parent" : "Child",
+ Id());
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ nsPIDOMWindowOuter* win = aContext->GetDOMWindow();
+ if (win) {
+ win->RefreshMediaElementsVolume();
+ }
+ });
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_IsAppTab>, const bool& aValue,
+ ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource && IsTop();
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_HasSiblings>, const bool& aValue,
+ ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource && IsTop();
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_ShouldDelayMediaFromStart>,
+ const bool& aValue, ContentParent* aSource) {
+ return IsTop();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_ShouldDelayMediaFromStart>,
+ bool aOldValue) {
+ MOZ_ASSERT(IsTop(), "Set attribute on non top-level context!");
+ if (aOldValue == GetShouldDelayMediaFromStart()) {
+ return;
+ }
+ if (!GetShouldDelayMediaFromStart()) {
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ if (nsPIDOMWindowOuter* win = aContext->GetDOMWindow()) {
+ win->ActivateMediaComponents();
+ }
+ });
+ }
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_OverrideDPPX>, const float& aValue,
+ ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource && IsTop();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_OverrideDPPX>, float aOldValue) {
+ MOZ_ASSERT(IsTop());
+ if (GetOverrideDPPX() == aOldValue) {
+ return;
+ }
+ PresContextAffectingFieldChanged();
+}
+
+void BrowsingContext::SetCustomUserAgent(const nsAString& aUserAgent,
+ ErrorResult& aRv) {
+ Top()->SetUserAgentOverride(aUserAgent, aRv);
+}
+
+nsresult BrowsingContext::SetCustomUserAgent(const nsAString& aUserAgent) {
+ return Top()->SetUserAgentOverride(aUserAgent);
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_UserAgentOverride>) {
+ MOZ_ASSERT(IsTop());
+
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ nsIDocShell* shell = aContext->GetDocShell();
+ if (shell) {
+ shell->ClearCachedUserAgent();
+ }
+ });
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_IsInBFCache>, bool,
+ ContentParent* aSource) {
+ return IsTop() && !aSource && mozilla::BFCacheInParent();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_IsInBFCache>) {
+ MOZ_RELEASE_ASSERT(mozilla::BFCacheInParent());
+ MOZ_DIAGNOSTIC_ASSERT(IsTop());
+
+ const bool isInBFCache = GetIsInBFCache();
+ if (!isInBFCache) {
+ UpdateCurrentTopByBrowserId(this);
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ aContext->mIsInBFCache = false;
+ nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell();
+ if (shell) {
+ nsDocShell::Cast(shell)->ThawFreezeNonRecursive(true);
+ }
+ });
+ }
+
+ if (isInBFCache && XRE_IsContentProcess() && mDocShell) {
+ nsDocShell::Cast(mDocShell)->MaybeDisconnectChildListenersOnPageHide();
+ }
+
+ if (isInBFCache) {
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell();
+ if (shell) {
+ nsDocShell::Cast(shell)->FirePageHideShowNonRecursive(false);
+ }
+ });
+ } else {
+ PostOrderWalk([&](BrowsingContext* aContext) {
+ nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell();
+ if (shell) {
+ nsDocShell::Cast(shell)->FirePageHideShowNonRecursive(true);
+ }
+ });
+ }
+
+ if (isInBFCache) {
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell();
+ if (shell) {
+ nsDocShell::Cast(shell)->ThawFreezeNonRecursive(false);
+ if (nsPresContext* pc = shell->GetPresContext()) {
+ pc->EventStateManager()->ResetHoverState();
+ }
+ }
+ aContext->mIsInBFCache = true;
+ Document* doc = aContext->GetDocument();
+ if (doc) {
+ // Notifying needs to happen after mIsInBFCache is set to true.
+ doc->NotifyActivityChanged();
+ }
+ });
+
+ if (XRE_IsParentProcess()) {
+ if (mCurrentWindowContext &&
+ mCurrentWindowContext->Canonical()->Fullscreen()) {
+ mCurrentWindowContext->Canonical()->ExitTopChromeDocumentFullscreen();
+ }
+ }
+ }
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_SyntheticDocumentContainer>) {
+ if (WindowContext* parentWindowContext = GetParentWindowContext()) {
+ parentWindowContext->UpdateChildSynthetic(this,
+ GetSyntheticDocumentContainer());
+ }
+}
+
+void BrowsingContext::SetCustomPlatform(const nsAString& aPlatform,
+ ErrorResult& aRv) {
+ Top()->SetPlatformOverride(aPlatform, aRv);
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_PlatformOverride>) {
+ MOZ_ASSERT(IsTop());
+
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ nsIDocShell* shell = aContext->GetDocShell();
+ if (shell) {
+ shell->ClearCachedPlatform();
+ }
+ });
+}
+
+auto BrowsingContext::LegacyRevertIfNotOwningOrParentProcess(
+ ContentParent* aSource) -> CanSetResult {
+ if (aSource) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (!Canonical()->IsOwnedByProcess(aSource->ChildID())) {
+ return CanSetResult::Revert;
+ }
+ } else if (!IsInProcess() && !XRE_IsParentProcess()) {
+ // Don't allow this to be set from content processes that
+ // don't own the BrowsingContext.
+ return CanSetResult::Deny;
+ }
+
+ return CanSetResult::Allow;
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>,
+ const bool& aValue, ContentParent* aSource) {
+ // Should only be set in the parent process.
+ return XRE_IsParentProcess() && !aSource && IsTop();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>,
+ bool aOldValue) {
+ bool isActivateEvent = GetIsActiveBrowserWindowInternal();
+ // The browser window containing this context has changed
+ // activation state so update window inactive document states
+ // for all in-process documents.
+ PreOrderWalk([isActivateEvent](BrowsingContext* aContext) {
+ if (RefPtr<Document> doc = aContext->GetExtantDocument()) {
+ doc->UpdateDocumentStates(DocumentState::WINDOW_INACTIVE, true);
+
+ RefPtr<nsPIDOMWindowInner> win = doc->GetInnerWindow();
+ if (win) {
+ RefPtr<MediaDevices> devices;
+ if (isActivateEvent && (devices = win->GetExtantMediaDevices())) {
+ devices->BrowserWindowBecameActive();
+ }
+
+ if (XRE_IsContentProcess() &&
+ (!aContext->GetParent() || !aContext->GetParent()->IsInProcess())) {
+ // Send the inner window an activate/deactivate event if
+ // the context is the top of a sub-tree of in-process
+ // contexts.
+ nsContentUtils::DispatchEventOnlyToChrome(
+ doc, nsGlobalWindowInner::Cast(win),
+ isActivateEvent ? u"activate"_ns : u"deactivate"_ns,
+ CanBubble::eYes, Cancelable::eYes, nullptr);
+ }
+ }
+ }
+ });
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_OpenerPolicy>,
+ nsILoadInfo::CrossOriginOpenerPolicy aPolicy,
+ ContentParent* aSource) {
+ // A potentially cross-origin isolated BC can't change opener policy, nor can
+ // a BC become potentially cross-origin isolated. An unchanged policy is
+ // always OK.
+ return GetOpenerPolicy() == aPolicy ||
+ (GetOpenerPolicy() !=
+ nsILoadInfo::
+ OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP &&
+ aPolicy !=
+ nsILoadInfo::
+ OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP);
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_AllowContentRetargeting>,
+ const bool& aAllowContentRetargeting,
+ ContentParent* aSource) -> CanSetResult {
+ return LegacyRevertIfNotOwningOrParentProcess(aSource);
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_AllowContentRetargetingOnChildren>,
+ const bool& aAllowContentRetargetingOnChildren,
+ ContentParent* aSource) -> CanSetResult {
+ return LegacyRevertIfNotOwningOrParentProcess(aSource);
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_FullscreenAllowedByOwner>,
+ const bool& aAllowed, ContentParent* aSource) {
+ return CheckOnlyEmbedderCanSet(aSource);
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_UseErrorPages>,
+ const bool& aUseErrorPages,
+ ContentParent* aSource) {
+ return CheckOnlyEmbedderCanSet(aSource);
+}
+
+TouchEventsOverride BrowsingContext::TouchEventsOverride() const {
+ for (const auto* bc = this; bc; bc = bc->GetParent()) {
+ auto tev = bc->GetTouchEventsOverrideInternal();
+ if (tev != dom::TouchEventsOverride::None) {
+ return tev;
+ }
+ }
+ return dom::TouchEventsOverride::None;
+}
+
+bool BrowsingContext::TargetTopLevelLinkClicksToBlank() const {
+ return Top()->GetTargetTopLevelLinkClicksToBlankInternal();
+}
+
+// We map `watchedByDevTools` WebIDL attribute to `watchedByDevToolsInternal`
+// BC field. And we map it to the top level BrowsingContext.
+bool BrowsingContext::WatchedByDevTools() {
+ return Top()->GetWatchedByDevToolsInternal();
+}
+
+// Enforce that the watchedByDevTools BC field can only be set on the top level
+// Browsing Context.
+bool BrowsingContext::CanSet(FieldIndex<IDX_WatchedByDevToolsInternal>,
+ const bool& aWatchedByDevTools,
+ ContentParent* aSource) {
+ return IsTop();
+}
+void BrowsingContext::SetWatchedByDevTools(bool aWatchedByDevTools,
+ ErrorResult& aRv) {
+ if (!IsTop()) {
+ aRv.ThrowInvalidModificationError(
+ "watchedByDevTools can only be set on top BrowsingContext");
+ return;
+ }
+ SetWatchedByDevToolsInternal(aWatchedByDevTools, aRv);
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_DefaultLoadFlags>,
+ const uint32_t& aDefaultLoadFlags,
+ ContentParent* aSource) -> CanSetResult {
+ // Bug 1623565 - Are these flags only used by the debugger, which makes it
+ // possible that this field can only be settable by the parent process?
+ return LegacyRevertIfNotOwningOrParentProcess(aSource);
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_DefaultLoadFlags>) {
+ auto loadFlags = GetDefaultLoadFlags();
+ if (GetDocShell()) {
+ nsDocShell::Cast(GetDocShell())->SetLoadGroupDefaultLoadFlags(loadFlags);
+ }
+
+ if (XRE_IsParentProcess()) {
+ PreOrderWalk([&](BrowsingContext* aContext) {
+ if (aContext != this) {
+ // Setting load flags on a discarded context has no effect.
+ Unused << aContext->SetDefaultLoadFlags(loadFlags);
+ }
+ });
+ }
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_UseGlobalHistory>,
+ const bool& aUseGlobalHistory,
+ ContentParent* aSource) {
+ // Should only be set in the parent process.
+ // return XRE_IsParentProcess() && !aSource;
+ return true;
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_UserAgentOverride>,
+ const nsString& aUserAgent, ContentParent* aSource)
+ -> CanSetResult {
+ if (!IsTop()) {
+ return CanSetResult::Deny;
+ }
+
+ return LegacyRevertIfNotOwningOrParentProcess(aSource);
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_PlatformOverride>,
+ const nsString& aPlatform, ContentParent* aSource)
+ -> CanSetResult {
+ if (!IsTop()) {
+ return CanSetResult::Deny;
+ }
+
+ return LegacyRevertIfNotOwningOrParentProcess(aSource);
+}
+
+bool BrowsingContext::CheckOnlyEmbedderCanSet(ContentParent* aSource) {
+ if (XRE_IsParentProcess()) {
+ uint64_t childId = aSource ? aSource->ChildID() : 0;
+ return Canonical()->IsEmbeddedInProcess(childId);
+ }
+ return mEmbeddedByThisProcess;
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_EmbedderInnerWindowId>,
+ const uint64_t& aValue, ContentParent* aSource) {
+ // If we have a parent window, our embedder inner window ID must match it.
+ if (mParentWindow) {
+ return mParentWindow->Id() == aValue;
+ }
+
+ // For toplevel BrowsingContext instances, this value may only be set by the
+ // parent process, or initialized to `0`.
+ return CheckOnlyEmbedderCanSet(aSource);
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_EmbedderElementType>,
+ const Maybe<nsString>&, ContentParent* aSource) {
+ return CheckOnlyEmbedderCanSet(aSource);
+}
+
+auto BrowsingContext::CanSet(FieldIndex<IDX_CurrentInnerWindowId>,
+ const uint64_t& aValue, ContentParent* aSource)
+ -> CanSetResult {
+ // Generally allow clearing this. We may want to be more precise about this
+ // check in the future.
+ if (aValue == 0) {
+ return CanSetResult::Allow;
+ }
+
+ // We must have access to the specified context.
+ RefPtr<WindowContext> window = WindowContext::GetById(aValue);
+ if (!window || window->GetBrowsingContext() != this) {
+ return CanSetResult::Deny;
+ }
+
+ if (aSource) {
+ // If the sending process is no longer the current owner, revert
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!Canonical()->IsOwnedByProcess(aSource->ChildID())) {
+ return CanSetResult::Revert;
+ }
+ } else if (XRE_IsContentProcess() && !IsOwnedByProcess()) {
+ return CanSetResult::Deny;
+ }
+
+ return CanSetResult::Allow;
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_ParentInitiatedNavigationEpoch>,
+ const uint64_t& aValue, ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource;
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_CurrentInnerWindowId>) {
+ RefPtr<WindowContext> prevWindowContext = mCurrentWindowContext.forget();
+ mCurrentWindowContext = WindowContext::GetById(GetCurrentInnerWindowId());
+ MOZ_ASSERT(
+ !mCurrentWindowContext || mWindowContexts.Contains(mCurrentWindowContext),
+ "WindowContext not registered?");
+
+ // Clear our cached `children` value, to ensure that JS sees the up-to-date
+ // value.
+ BrowsingContext_Binding::ClearCachedChildrenValue(this);
+
+ if (XRE_IsParentProcess()) {
+ if (prevWindowContext != mCurrentWindowContext) {
+ if (prevWindowContext) {
+ prevWindowContext->Canonical()->DidBecomeCurrentWindowGlobal(false);
+ }
+ if (mCurrentWindowContext) {
+ // We set a timer when we set the current inner window. This
+ // will then flush the session storage to session store to
+ // make sure that we don't miss to store session storage to
+ // session store that is a result of navigation. This is due
+ // to Bug 1700623. We wish to fix this in Bug 1711886, where
+ // making sure to store everything would make this timer
+ // unnecessary.
+ Canonical()->MaybeScheduleSessionStoreUpdate();
+ mCurrentWindowContext->Canonical()->DidBecomeCurrentWindowGlobal(true);
+ }
+ }
+ BrowserParent::UpdateFocusFromBrowsingContext();
+ }
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_IsPopupSpam>, const bool& aValue,
+ ContentParent* aSource) {
+ // Ensure that we only mark a browsing context as popup spam once and never
+ // unmark it.
+ return aValue && !GetIsPopupSpam();
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_IsPopupSpam>) {
+ if (GetIsPopupSpam()) {
+ PopupBlocker::RegisterOpenPopupSpam();
+ }
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_MessageManagerGroup>,
+ const nsString& aMessageManagerGroup,
+ ContentParent* aSource) {
+ // Should only be set in the parent process on toplevel.
+ return XRE_IsParentProcess() && !aSource && IsTopContent();
+}
+
+bool BrowsingContext::CanSet(
+ FieldIndex<IDX_OrientationLock>,
+ const mozilla::hal::ScreenOrientation& aOrientationLock,
+ ContentParent* aSource) {
+ return IsTop();
+}
+
+bool BrowsingContext::IsLoading() {
+ if (GetLoading()) {
+ return true;
+ }
+
+ // If we're in the same process as the page, we're possibly just
+ // updating the flag.
+ nsIDocShell* shell = GetDocShell();
+ if (shell) {
+ Document* doc = shell->GetDocument();
+ return doc && doc->GetReadyStateEnum() < Document::READYSTATE_COMPLETE;
+ }
+
+ return false;
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_Loading>) {
+ if (mFields.Get<IDX_Loading>()) {
+ return;
+ }
+
+ while (!mDeprioritizedLoadRunner.isEmpty()) {
+ nsCOMPtr<nsIRunnable> runner = mDeprioritizedLoadRunner.popFirst();
+ NS_DispatchToCurrentThread(runner.forget());
+ }
+
+ if (IsTop()) {
+ Group()->FlushPostMessageEvents();
+ }
+}
+
+// Inform the Document for this context of the (potential) change in
+// loading state
+void BrowsingContext::DidSet(FieldIndex<IDX_AncestorLoading>) {
+ nsPIDOMWindowOuter* outer = GetDOMWindow();
+ if (!outer) {
+ MOZ_LOG(gTimeoutDeferralLog, mozilla::LogLevel::Debug,
+ ("DidSetAncestorLoading BC: %p -- No outer window", (void*)this));
+ return;
+ }
+ Document* document = nsGlobalWindowOuter::Cast(outer)->GetExtantDoc();
+ if (document) {
+ MOZ_LOG(gTimeoutDeferralLog, mozilla::LogLevel::Debug,
+ ("DidSetAncestorLoading BC: %p -- NotifyLoading(%d, %d, %d)",
+ (void*)this, GetAncestorLoading(), document->GetReadyStateEnum(),
+ document->GetReadyStateEnum()));
+ document->NotifyLoading(GetAncestorLoading(), document->GetReadyStateEnum(),
+ document->GetReadyStateEnum());
+ }
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_AuthorStyleDisabledDefault>) {
+ MOZ_ASSERT(IsTop(),
+ "Should only set AuthorStyleDisabledDefault in the top "
+ "browsing context");
+
+ // We don't need to handle changes to this field, since PageStyleChild.sys.mjs
+ // will respond to the PageStyle:Disable message in all content processes.
+ //
+ // But we store the state here on the top BrowsingContext so that the
+ // docshell has somewhere to look for the current author style disabling
+ // state when new iframes are inserted.
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_TextZoom>, float aOldValue) {
+ if (GetTextZoom() == aOldValue) {
+ return;
+ }
+
+ if (IsInProcess()) {
+ if (nsIDocShell* shell = GetDocShell()) {
+ if (nsPresContext* pc = shell->GetPresContext()) {
+ pc->RecomputeBrowsingContextDependentData();
+ }
+ }
+
+ for (BrowsingContext* child : Children()) {
+ // Setting text zoom on a discarded context has no effect.
+ Unused << child->SetTextZoom(GetTextZoom());
+ }
+ }
+
+ if (IsTop() && XRE_IsParentProcess()) {
+ if (Element* element = GetEmbedderElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(*element, u"TextZoomChange"_ns,
+ CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ }
+ }
+}
+
+// TODO(emilio): It'd be potentially nicer and cheaper to allow to set this only
+// on the Top() browsing context, but there are a lot of tests that rely on
+// zooming a subframe so...
+void BrowsingContext::DidSet(FieldIndex<IDX_FullZoom>, float aOldValue) {
+ if (GetFullZoom() == aOldValue) {
+ return;
+ }
+
+ if (IsInProcess()) {
+ if (nsIDocShell* shell = GetDocShell()) {
+ if (nsPresContext* pc = shell->GetPresContext()) {
+ pc->RecomputeBrowsingContextDependentData();
+ }
+ }
+
+ for (BrowsingContext* child : Children()) {
+ // Setting full zoom on a discarded context has no effect.
+ Unused << child->SetFullZoom(GetFullZoom());
+ }
+ }
+
+ if (IsTop() && XRE_IsParentProcess()) {
+ if (Element* element = GetEmbedderElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(*element, u"FullZoomChange"_ns,
+ CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ }
+ }
+}
+
+void BrowsingContext::AddDeprioritizedLoadRunner(nsIRunnable* aRunner) {
+ MOZ_ASSERT(IsLoading());
+ MOZ_ASSERT(Top() == this);
+
+ RefPtr<DeprioritizedLoadRunner> runner = new DeprioritizedLoadRunner(aRunner);
+ mDeprioritizedLoadRunner.insertBack(runner);
+ NS_DispatchToCurrentThreadQueue(
+ runner.forget(), StaticPrefs::page_load_deprioritization_period(),
+ EventQueuePriority::Idle);
+}
+
+bool BrowsingContext::IsDynamic() const {
+ const BrowsingContext* current = this;
+ do {
+ if (current->CreatedDynamically()) {
+ return true;
+ }
+ } while ((current = current->GetParent()));
+
+ return false;
+}
+
+bool BrowsingContext::GetOffsetPath(nsTArray<uint32_t>& aPath) const {
+ for (const BrowsingContext* current = this; current && current->GetParent();
+ current = current->GetParent()) {
+ if (current->CreatedDynamically()) {
+ return false;
+ }
+ aPath.AppendElement(current->ChildOffset());
+ }
+ return true;
+}
+
+void BrowsingContext::GetHistoryID(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aVal,
+ ErrorResult& aError) {
+ if (!xpc::ID2JSValue(aCx, GetHistoryID(), aVal)) {
+ aError.Throw(NS_ERROR_OUT_OF_MEMORY);
+ }
+}
+
+void BrowsingContext::InitSessionHistory() {
+ MOZ_ASSERT(!IsDiscarded());
+ MOZ_ASSERT(IsTop());
+ MOZ_ASSERT(EverAttached());
+
+ if (!GetHasSessionHistory()) {
+ MOZ_ALWAYS_SUCCEEDS(SetHasSessionHistory(true));
+ }
+}
+
+ChildSHistory* BrowsingContext::GetChildSessionHistory() {
+ if (!mozilla::SessionHistoryInParent()) {
+ // For now we're checking that the session history object for the child
+ // process is available before returning the ChildSHistory object, because
+ // it is the actual implementation that ChildSHistory forwards to. This can
+ // be removed once session history is stored exclusively in the parent
+ // process.
+ return mChildSessionHistory && mChildSessionHistory->IsInProcess()
+ ? mChildSessionHistory.get()
+ : nullptr;
+ }
+
+ return mChildSessionHistory;
+}
+
+void BrowsingContext::CreateChildSHistory() {
+ MOZ_ASSERT(IsTop());
+ MOZ_ASSERT(GetHasSessionHistory());
+ MOZ_ASSERT(!mChildSessionHistory);
+
+ // Because session history is global in a browsing context tree, every process
+ // that has access to a browsing context tree needs access to its session
+ // history. That is why we create the ChildSHistory object in every process
+ // where we have access to this browsing context (which is the top one).
+ mChildSessionHistory = new ChildSHistory(this);
+
+ // If the top browsing context (this one) is loaded in this process then we
+ // also create the session history implementation for the child process.
+ // This can be removed once session history is stored exclusively in the
+ // parent process.
+ mChildSessionHistory->SetIsInProcess(IsInProcess());
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_HasSessionHistory>,
+ bool aOldValue) {
+ MOZ_ASSERT(GetHasSessionHistory() || !aOldValue,
+ "We don't support turning off session history.");
+
+ if (GetHasSessionHistory() && !aOldValue) {
+ CreateChildSHistory();
+ }
+}
+
+bool BrowsingContext::CanSet(
+ FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>,
+ const bool& aTargetTopLevelLinkClicksToBlankInternal,
+ ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource && IsTop();
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
+ ContentParent* aSource) {
+ // We should only be able to set this for toplevel contexts which don't have
+ // an ID yet.
+ return GetBrowserId() == 0 && IsTop() && Children().IsEmpty();
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_PendingInitialization>,
+ bool aNewValue, ContentParent* aSource) {
+ // Can only be cleared from `true` to `false`, and should only ever be set on
+ // the toplevel BrowsingContext.
+ return IsTop() && GetPendingInitialization() && !aNewValue;
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_HasRestoreData>, bool aNewValue,
+ ContentParent* aSource) {
+ return IsTop();
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>,
+ const bool& aIsUnderHiddenEmbedderElement,
+ ContentParent* aSource) {
+ return true;
+}
+
+bool BrowsingContext::CanSet(FieldIndex<IDX_ForceOffline>, bool aNewValue,
+ ContentParent* aSource) {
+ return XRE_IsParentProcess() && !aSource;
+}
+
+void BrowsingContext::DidSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>,
+ bool aOldValue) {
+ nsIDocShell* shell = GetDocShell();
+ if (!shell) {
+ return;
+ }
+ const bool newValue = IsUnderHiddenEmbedderElement();
+ if (NS_WARN_IF(aOldValue == newValue)) {
+ return;
+ }
+
+ if (auto* bc = BrowserChild::GetFrom(shell)) {
+ bc->UpdateVisibility();
+ }
+
+ if (PresShell* presShell = shell->GetPresShell()) {
+ presShell->SetIsUnderHiddenEmbedderElement(newValue);
+ }
+
+ // Propagate to children.
+ for (BrowsingContext* child : Children()) {
+ Element* embedderElement = child->GetEmbedderElement();
+ if (!embedderElement) {
+ // TODO: We shouldn't need to null check here since `child` and the
+ // element returned by `child->GetEmbedderElement()` are in our
+ // process (the actual browsing context represented by `child` may not
+ // be, but that doesn't matter). However, there are currently a very
+ // small number of crashes due to `embedderElement` being null, somehow
+ // - see bug 1551241. For now we wallpaper the crash.
+ continue;
+ }
+
+ bool embedderFrameIsHidden = true;
+ if (auto* embedderFrame = embedderElement->GetPrimaryFrame()) {
+ embedderFrameIsHidden = !embedderFrame->StyleVisibility()->IsVisible();
+ }
+
+ bool hidden = IsUnderHiddenEmbedderElement() || embedderFrameIsHidden;
+ if (child->IsUnderHiddenEmbedderElement() != hidden) {
+ Unused << child->SetIsUnderHiddenEmbedderElement(hidden);
+ }
+ }
+}
+
+bool BrowsingContext::IsPopupAllowed() {
+ for (auto* context = GetCurrentWindowContext(); context;
+ context = context->GetParentWindowContext()) {
+ if (context->CanShowPopup()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* static */
+bool BrowsingContext::ShouldAddEntryForRefresh(
+ nsIURI* aPreviousURI, const SessionHistoryInfo& aInfo) {
+ return ShouldAddEntryForRefresh(aPreviousURI, aInfo.GetURI(),
+ aInfo.HasPostData());
+}
+
+/* static */
+bool BrowsingContext::ShouldAddEntryForRefresh(nsIURI* aPreviousURI,
+ nsIURI* aNewURI,
+ bool aHasPostData) {
+ if (aHasPostData) {
+ return true;
+ }
+
+ bool equalsURI = false;
+ if (aPreviousURI) {
+ aPreviousURI->Equals(aNewURI, &equalsURI);
+ }
+ return !equalsURI;
+}
+
+void BrowsingContext::SessionHistoryCommit(
+ const LoadingSessionHistoryInfo& aInfo, uint32_t aLoadType,
+ nsIURI* aPreviousURI, SessionHistoryInfo* aPreviousActiveEntry,
+ bool aPersist, bool aCloneEntryChildren, bool aChannelExpired,
+ uint32_t aCacheKey) {
+ nsID changeID = {};
+ if (XRE_IsContentProcess()) {
+ RefPtr<ChildSHistory> rootSH = Top()->GetChildSessionHistory();
+ if (rootSH) {
+ if (!aInfo.mLoadIsFromSessionHistory) {
+ // We try to mimic as closely as possible what will happen in
+ // CanonicalBrowsingContext::SessionHistoryCommit. We'll be
+ // incrementing the session history length if we're not replacing,
+ // this is a top-level load or it's not the initial load in an iframe,
+ // ShouldUpdateSessionHistory(loadType) returns true and it's not a
+ // refresh for which ShouldAddEntryForRefresh returns false.
+ // It is possible that this leads to wrong length temporarily, but
+ // so would not having the check for replace.
+ // Note that nsSHistory::AddEntry does a replace load if the current
+ // entry is not marked as a persisted entry. The child process does
+ // not have access to the current entry, so we use the previous active
+ // entry as the best approximation. When that's not the current entry
+ // then the length might be wrong briefly, until the parent process
+ // commits the actual length.
+ if (!LOAD_TYPE_HAS_FLAGS(
+ aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY) &&
+ (IsTop()
+ ? (!aPreviousActiveEntry || aPreviousActiveEntry->GetPersist())
+ : !!aPreviousActiveEntry) &&
+ ShouldUpdateSessionHistory(aLoadType) &&
+ (!LOAD_TYPE_HAS_FLAGS(aLoadType,
+ nsIWebNavigation::LOAD_FLAGS_IS_REFRESH) ||
+ ShouldAddEntryForRefresh(aPreviousURI, aInfo.mInfo))) {
+ changeID = rootSH->AddPendingHistoryChange();
+ }
+ } else {
+ // History load doesn't change the length, only index.
+ changeID = rootSH->AddPendingHistoryChange(aInfo.mOffset, 0);
+ }
+ }
+ ContentChild* cc = ContentChild::GetSingleton();
+ mozilla::Unused << cc->SendHistoryCommit(
+ this, aInfo.mLoadId, changeID, aLoadType, aPersist, aCloneEntryChildren,
+ aChannelExpired, aCacheKey);
+ } else {
+ Canonical()->SessionHistoryCommit(aInfo.mLoadId, changeID, aLoadType,
+ aPersist, aCloneEntryChildren,
+ aChannelExpired, aCacheKey);
+ }
+}
+
+void BrowsingContext::SetActiveSessionHistoryEntry(
+ const Maybe<nsPoint>& aPreviousScrollPos, SessionHistoryInfo* aInfo,
+ uint32_t aLoadType, uint32_t aUpdatedCacheKey, bool aUpdateLength) {
+ if (XRE_IsContentProcess()) {
+ // XXX Why we update cache key only in content process case?
+ if (aUpdatedCacheKey != 0) {
+ aInfo->SetCacheKey(aUpdatedCacheKey);
+ }
+
+ nsID changeID = {};
+ if (aUpdateLength) {
+ RefPtr<ChildSHistory> shistory = Top()->GetChildSessionHistory();
+ if (shistory) {
+ changeID = shistory->AddPendingHistoryChange();
+ }
+ }
+ ContentChild::GetSingleton()->SendSetActiveSessionHistoryEntry(
+ this, aPreviousScrollPos, *aInfo, aLoadType, aUpdatedCacheKey,
+ changeID);
+ } else {
+ Canonical()->SetActiveSessionHistoryEntry(
+ aPreviousScrollPos, aInfo, aLoadType, aUpdatedCacheKey, nsID());
+ }
+}
+
+void BrowsingContext::ReplaceActiveSessionHistoryEntry(
+ SessionHistoryInfo* aInfo) {
+ if (XRE_IsContentProcess()) {
+ ContentChild::GetSingleton()->SendReplaceActiveSessionHistoryEntry(this,
+ *aInfo);
+ } else {
+ Canonical()->ReplaceActiveSessionHistoryEntry(aInfo);
+ }
+}
+
+void BrowsingContext::RemoveDynEntriesFromActiveSessionHistoryEntry() {
+ if (XRE_IsContentProcess()) {
+ ContentChild::GetSingleton()
+ ->SendRemoveDynEntriesFromActiveSessionHistoryEntry(this);
+ } else {
+ Canonical()->RemoveDynEntriesFromActiveSessionHistoryEntry();
+ }
+}
+
+void BrowsingContext::RemoveFromSessionHistory(const nsID& aChangeID) {
+ if (XRE_IsContentProcess()) {
+ ContentChild::GetSingleton()->SendRemoveFromSessionHistory(this, aChangeID);
+ } else {
+ Canonical()->RemoveFromSessionHistory(aChangeID);
+ }
+}
+
+void BrowsingContext::HistoryGo(
+ int32_t aOffset, uint64_t aHistoryEpoch, bool aRequireUserInteraction,
+ bool aUserActivation, std::function<void(Maybe<int32_t>&&)>&& aResolver) {
+ if (XRE_IsContentProcess()) {
+ ContentChild::GetSingleton()->SendHistoryGo(
+ this, aOffset, aHistoryEpoch, aRequireUserInteraction, aUserActivation,
+ std::move(aResolver),
+ [](mozilla::ipc::
+ ResponseRejectReason) { /* FIXME Is ignoring this fine? */ });
+ } else {
+ RefPtr<CanonicalBrowsingContext> self = Canonical();
+ aResolver(self->HistoryGo(
+ aOffset, aHistoryEpoch, aRequireUserInteraction, aUserActivation,
+ Canonical()->GetContentParent()
+ ? Some(Canonical()->GetContentParent()->ChildID())
+ : Nothing()));
+ }
+}
+
+void BrowsingContext::SetChildSHistory(ChildSHistory* aChildSHistory) {
+ mChildSessionHistory = aChildSHistory;
+ mChildSessionHistory->SetBrowsingContext(this);
+ mFields.SetWithoutSyncing<IDX_HasSessionHistory>(true);
+}
+
+bool BrowsingContext::ShouldUpdateSessionHistory(uint32_t aLoadType) {
+ // We don't update session history on reload unless we're loading
+ // an iframe in shift-reload case.
+ return nsDocShell::ShouldUpdateGlobalHistory(aLoadType) &&
+ (!(aLoadType & nsIDocShell::LOAD_CMD_RELOAD) ||
+ (IsForceReloadType(aLoadType) && IsSubframe()));
+}
+
+nsresult BrowsingContext::CheckLocationChangeRateLimit(CallerType aCallerType) {
+ // We only rate limit non system callers
+ if (aCallerType == CallerType::System) {
+ return NS_OK;
+ }
+
+ // Fetch rate limiting preferences
+ uint32_t limitCount =
+ StaticPrefs::dom_navigation_locationChangeRateLimit_count();
+ uint32_t timeSpanSeconds =
+ StaticPrefs::dom_navigation_locationChangeRateLimit_timespan();
+
+ // Disable throttling if either of the preferences is set to 0.
+ if (limitCount == 0 || timeSpanSeconds == 0) {
+ return NS_OK;
+ }
+
+ TimeDuration throttleSpan = TimeDuration::FromSeconds(timeSpanSeconds);
+
+ if (mLocationChangeRateLimitSpanStart.IsNull() ||
+ ((TimeStamp::Now() - mLocationChangeRateLimitSpanStart) > throttleSpan)) {
+ // Initial call or timespan exceeded, reset counter and timespan.
+ mLocationChangeRateLimitSpanStart = TimeStamp::Now();
+ mLocationChangeRateLimitCount = 1;
+ return NS_OK;
+ }
+
+ if (mLocationChangeRateLimitCount >= limitCount) {
+ // Rate limit reached
+
+ Document* doc = GetDocument();
+ if (doc) {
+ nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, "DOM"_ns, doc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "LocChangeFloodingPrevented");
+ }
+
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ mLocationChangeRateLimitCount++;
+ return NS_OK;
+}
+
+void BrowsingContext::ResetLocationChangeRateLimit() {
+ // Resetting the timestamp object will cause the check function to
+ // init again and reset the rate limit.
+ mLocationChangeRateLimitSpanStart = TimeStamp();
+}
+
+void BrowsingContext::LocationCreated(dom::Location* aLocation) {
+ MOZ_ASSERT(!aLocation->isInList());
+ mLocations.insertBack(aLocation);
+}
+
+void BrowsingContext::ClearCachedValuesOfLocations() {
+ for (dom::Location* loc = mLocations.getFirst(); loc; loc = loc->getNext()) {
+ loc->ClearCachedValues();
+ }
+}
+
+} // namespace dom
+
+namespace ipc {
+
+void IPDLParamTraits<dom::MaybeDiscarded<dom::BrowsingContext>>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::MaybeDiscarded<dom::BrowsingContext>& aParam) {
+ MOZ_DIAGNOSTIC_ASSERT(!aParam.GetMaybeDiscarded() ||
+ aParam.GetMaybeDiscarded()->EverAttached());
+ uint64_t id = aParam.ContextId();
+ WriteIPDLParam(aWriter, aActor, id);
+}
+
+bool IPDLParamTraits<dom::MaybeDiscarded<dom::BrowsingContext>>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::MaybeDiscarded<dom::BrowsingContext>* aResult) {
+ uint64_t id = 0;
+ if (!ReadIPDLParam(aReader, aActor, &id)) {
+ return false;
+ }
+
+ if (id == 0) {
+ *aResult = nullptr;
+ } else if (RefPtr<dom::BrowsingContext> bc = dom::BrowsingContext::Get(id)) {
+ *aResult = std::move(bc);
+ } else {
+ aResult->SetDiscarded(id);
+ }
+ return true;
+}
+
+void IPDLParamTraits<dom::BrowsingContext::IPCInitializer>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::BrowsingContext::IPCInitializer& aInit) {
+ // Write actor ID parameters.
+ WriteIPDLParam(aWriter, aActor, aInit.mId);
+ WriteIPDLParam(aWriter, aActor, aInit.mParentId);
+ WriteIPDLParam(aWriter, aActor, aInit.mWindowless);
+ WriteIPDLParam(aWriter, aActor, aInit.mUseRemoteTabs);
+ WriteIPDLParam(aWriter, aActor, aInit.mUseRemoteSubframes);
+ WriteIPDLParam(aWriter, aActor, aInit.mCreatedDynamically);
+ WriteIPDLParam(aWriter, aActor, aInit.mChildOffset);
+ WriteIPDLParam(aWriter, aActor, aInit.mOriginAttributes);
+ WriteIPDLParam(aWriter, aActor, aInit.mRequestContextId);
+ WriteIPDLParam(aWriter, aActor, aInit.mSessionHistoryIndex);
+ WriteIPDLParam(aWriter, aActor, aInit.mSessionHistoryCount);
+ WriteIPDLParam(aWriter, aActor, aInit.mFields);
+}
+
+bool IPDLParamTraits<dom::BrowsingContext::IPCInitializer>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::BrowsingContext::IPCInitializer* aInit) {
+ // Read actor ID parameters.
+ if (!ReadIPDLParam(aReader, aActor, &aInit->mId) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mParentId) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mWindowless) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mUseRemoteTabs) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mUseRemoteSubframes) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mCreatedDynamically) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mChildOffset) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mOriginAttributes) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mRequestContextId) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mSessionHistoryIndex) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mSessionHistoryCount) ||
+ !ReadIPDLParam(aReader, aActor, &aInit->mFields)) {
+ return false;
+ }
+ return true;
+}
+
+template struct IPDLParamTraits<dom::BrowsingContext::BaseTransaction>;
+
+} // namespace ipc
+} // namespace mozilla
diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h
new file mode 100644
index 0000000000..5ec95a61e4
--- /dev/null
+++ b/docshell/base/BrowsingContext.h
@@ -0,0 +1,1469 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BrowsingContext_h
+#define mozilla_dom_BrowsingContext_h
+
+#include <tuple>
+#include "GVAutoplayRequestUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/HalScreenConfiguration.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Span.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/LocationBase.h"
+#include "mozilla/dom/MaybeDiscarded.h"
+#include "mozilla/dom/PopupBlocker.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/BrowsingContextBinding.h"
+#include "mozilla/dom/ScreenOrientationBinding.h"
+#include "mozilla/dom/SyncedContext.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDocShell.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+#include "nsILoadInfo.h"
+#include "nsILoadContext.h"
+#include "nsThreadUtils.h"
+
+class nsDocShellLoadState;
+class nsGlobalWindowInner;
+class nsGlobalWindowOuter;
+class nsIPrincipal;
+class nsOuterWindowProxy;
+struct nsPoint;
+class PickleIterator;
+
+namespace IPC {
+class Message;
+class MessageReader;
+class MessageWriter;
+} // namespace IPC
+
+namespace mozilla {
+
+class ErrorResult;
+class LogModule;
+
+namespace ipc {
+class IProtocol;
+class IPCResult;
+
+template <typename T>
+struct IPDLParamTraits;
+} // namespace ipc
+
+namespace dom {
+class BrowsingContent;
+class BrowsingContextGroup;
+class CanonicalBrowsingContext;
+class ChildSHistory;
+class ContentParent;
+class Element;
+struct LoadingSessionHistoryInfo;
+class Location;
+template <typename>
+struct Nullable;
+template <typename T>
+class Sequence;
+class SessionHistoryInfo;
+class SessionStorageManager;
+class StructuredCloneHolder;
+class WindowContext;
+class WindowGlobalChild;
+struct WindowPostMessageOptions;
+class WindowProxyHolder;
+
+enum class ExplicitActiveStatus : uint8_t {
+ None,
+ Active,
+ Inactive,
+ EndGuard_,
+};
+
+struct EmbedderColorSchemes {
+ PrefersColorSchemeOverride mUsed{};
+ PrefersColorSchemeOverride mPreferred{};
+
+ bool operator==(const EmbedderColorSchemes& aOther) const {
+ return mUsed == aOther.mUsed && mPreferred == aOther.mPreferred;
+ }
+
+ bool operator!=(const EmbedderColorSchemes& aOther) const {
+ return !(*this == aOther);
+ }
+};
+
+// Fields are, by default, settable by any process and readable by any process.
+// Racy sets will be resolved as-if they occurred in the order the parent
+// process finds out about them.
+//
+// The `DidSet` and `CanSet` methods may be overloaded to provide different
+// behavior for a specific field.
+// * `DidSet` is called to run code in every process whenever the value is
+// updated (This currently occurs even if the value didn't change, though
+// this may change in the future).
+// * `CanSet` is called before attempting to set the value, in both the process
+// which calls `Set`, and the parent process, and will kill the misbehaving
+// process if it fails.
+#define MOZ_EACH_BC_FIELD(FIELD) \
+ FIELD(Name, nsString) \
+ FIELD(Closed, bool) \
+ FIELD(ExplicitActive, ExplicitActiveStatus) \
+ /* Top()-only. If true, new-playing media will be suspended when in an \
+ * inactive browsing context. */ \
+ FIELD(SuspendMediaWhenInactive, bool) \
+ /* If true, we're within the nested event loop in window.open, and this \
+ * context may not be used as the target of a load */ \
+ FIELD(PendingInitialization, bool) \
+ /* Indicates if the browser window is active for the purpose of the \
+ * :-moz-window-inactive pseudoclass. Only read from or set on the \
+ * top BrowsingContext. */ \
+ FIELD(IsActiveBrowserWindowInternal, bool) \
+ FIELD(OpenerPolicy, nsILoadInfo::CrossOriginOpenerPolicy) \
+ /* Current opener for the BrowsingContext. Weak reference */ \
+ FIELD(OpenerId, uint64_t) \
+ FIELD(OnePermittedSandboxedNavigatorId, uint64_t) \
+ /* WindowID of the inner window which embeds this BC */ \
+ FIELD(EmbedderInnerWindowId, uint64_t) \
+ FIELD(CurrentInnerWindowId, uint64_t) \
+ FIELD(HadOriginalOpener, bool) \
+ FIELD(IsPopupSpam, bool) \
+ /* Hold the audio muted state and should be used on top level browsing \
+ * contexts only */ \
+ FIELD(Muted, bool) \
+ /* Hold the pinned/app-tab state and should be used on top level browsing \
+ * contexts only */ \
+ FIELD(IsAppTab, bool) \
+ /* Whether there's more than 1 tab / toplevel browsing context in this \
+ * parent window. Used to determine if a given BC is allowed to resize \
+ * and/or move the window or not. */ \
+ FIELD(HasSiblings, bool) \
+ /* Indicate that whether we should delay media playback, which would only \
+ be done on an unvisited tab. And this should only be used on the top \
+ level browsing contexts */ \
+ FIELD(ShouldDelayMediaFromStart, bool) \
+ /* See nsSandboxFlags.h for the possible flags. */ \
+ FIELD(SandboxFlags, uint32_t) \
+ /* The value of SandboxFlags when the BrowsingContext is first created. \
+ * Used for sandboxing the initial about:blank document. */ \
+ FIELD(InitialSandboxFlags, uint32_t) \
+ /* A non-zero unique identifier for the browser element that is hosting \
+ * this \
+ * BrowsingContext tree. Every BrowsingContext in the element's tree will \
+ * return the same ID in all processes and it will remain stable \
+ * regardless of process changes. When a browser element's frameloader is \
+ * switched to another browser element this ID will remain the same but \
+ * hosted under the under the new browser element. */ \
+ FIELD(BrowserId, uint64_t) \
+ FIELD(HistoryID, nsID) \
+ FIELD(InRDMPane, bool) \
+ FIELD(Loading, bool) \
+ /* A field only set on top browsing contexts, which indicates that either: \
+ * \
+ * * This is a browsing context created explicitly for printing or print \
+ * preview (thus hosting static documents). \
+ * \
+ * * This is a browsing context where something in this tree is calling \
+ * window.print() (and thus showing a modal dialog). \
+ * \
+ * We use it exclusively to block navigation for both of these cases. */ \
+ FIELD(IsPrinting, bool) \
+ FIELD(AncestorLoading, bool) \
+ FIELD(AllowContentRetargeting, bool) \
+ FIELD(AllowContentRetargetingOnChildren, bool) \
+ FIELD(ForceEnableTrackingProtection, bool) \
+ FIELD(UseGlobalHistory, bool) \
+ FIELD(TargetTopLevelLinkClicksToBlankInternal, bool) \
+ FIELD(FullscreenAllowedByOwner, bool) \
+ FIELD(ForceDesktopViewport, bool) \
+ /* \
+ * "is popup" in the spec. \
+ * Set only on top browsing contexts. \
+ * This doesn't indicate whether this is actually a popup or not, \
+ * but whether this browsing context is created by requesting popup or not. \
+ * See also: nsWindowWatcher::ShouldOpenPopup. \
+ */ \
+ FIELD(IsPopupRequested, bool) \
+ /* These field are used to store the states of autoplay media request on \
+ * GeckoView only, and it would only be modified on the top level browsing \
+ * context. */ \
+ FIELD(GVAudibleAutoplayRequestStatus, GVAutoplayRequestStatus) \
+ FIELD(GVInaudibleAutoplayRequestStatus, GVAutoplayRequestStatus) \
+ /* ScreenOrientation-related APIs */ \
+ FIELD(CurrentOrientationAngle, float) \
+ FIELD(CurrentOrientationType, mozilla::dom::OrientationType) \
+ FIELD(OrientationLock, mozilla::hal::ScreenOrientation) \
+ FIELD(UserAgentOverride, nsString) \
+ FIELD(TouchEventsOverrideInternal, mozilla::dom::TouchEventsOverride) \
+ FIELD(EmbedderElementType, Maybe<nsString>) \
+ FIELD(MessageManagerGroup, nsString) \
+ FIELD(MaxTouchPointsOverride, uint8_t) \
+ FIELD(FullZoom, float) \
+ FIELD(WatchedByDevToolsInternal, bool) \
+ FIELD(TextZoom, float) \
+ FIELD(OverrideDPPX, float) \
+ /* The current in-progress load. */ \
+ FIELD(CurrentLoadIdentifier, Maybe<uint64_t>) \
+ /* See nsIRequest for possible flags. */ \
+ FIELD(DefaultLoadFlags, uint32_t) \
+ /* Signals that session history is enabled for this browsing context tree. \
+ * This is only ever set to true on the top BC, so consumers need to get \
+ * the value from the top BC! */ \
+ FIELD(HasSessionHistory, bool) \
+ /* Tracks if this context is the only top-level document in the session \
+ * history of the context. */ \
+ FIELD(IsSingleToplevelInHistory, bool) \
+ FIELD(UseErrorPages, bool) \
+ FIELD(PlatformOverride, nsString) \
+ /* Specifies if this BC has loaded documents besides the initial \
+ * about:blank document. about:privatebrowsing, about:home, about:newtab \
+ * and non-initial about:blank are not considered to be initial \
+ * documents. */ \
+ FIELD(HasLoadedNonInitialDocument, bool) \
+ /* Default value for nsIDocumentViewer::authorStyleDisabled in any new \
+ * browsing contexts created as a descendant of this one. Valid only for \
+ * top BCs. */ \
+ FIELD(AuthorStyleDisabledDefault, bool) \
+ FIELD(ServiceWorkersTestingEnabled, bool) \
+ FIELD(MediumOverride, nsString) \
+ /* DevTools override for prefers-color-scheme */ \
+ FIELD(PrefersColorSchemeOverride, dom::PrefersColorSchemeOverride) \
+ /* prefers-color-scheme override based on the color-scheme style of our \
+ * <browser> embedder element. */ \
+ FIELD(EmbedderColorSchemes, EmbedderColorSchemes) \
+ FIELD(DisplayMode, dom::DisplayMode) \
+ /* The number of entries added to the session history because of this \
+ * browsing context. */ \
+ FIELD(HistoryEntryCount, uint32_t) \
+ /* Don't use the getter of the field, but IsInBFCache() method */ \
+ FIELD(IsInBFCache, bool) \
+ FIELD(HasRestoreData, bool) \
+ FIELD(SessionStoreEpoch, uint32_t) \
+ /* Whether we can execute scripts in this BrowsingContext. Has no effect \
+ * unless scripts are also allowed in the parent WindowContext. */ \
+ FIELD(AllowJavascript, bool) \
+ /* The count of request that are used to prevent the browsing context tree \
+ * from being suspended, which would ONLY be modified on the top level \
+ * context in the chrome process because that's a non-atomic counter */ \
+ FIELD(PageAwakeRequestCount, uint32_t) \
+ /* This field only gets incrememented when we start navigations in the \
+ * parent process. This is used for keeping track of the racing navigations \
+ * between the parent and content processes. */ \
+ FIELD(ParentInitiatedNavigationEpoch, uint64_t) \
+ /* This browsing context is for a synthetic image document wrapping an \
+ * image embedded in <object> or <embed>. */ \
+ FIELD(SyntheticDocumentContainer, bool) \
+ /* If true, this document is embedded within a content document, either \
+ * loaded in the parent (e.g. about:addons or the devtools toolbox), or in \
+ * a content process. */ \
+ FIELD(EmbeddedInContentDocument, bool) \
+ /* If true, this browsing context is within a hidden embedded document. */ \
+ FIELD(IsUnderHiddenEmbedderElement, bool) \
+ /* If true, this browsing context is offline */ \
+ FIELD(ForceOffline, bool)
+
+// BrowsingContext, in this context, is the cross process replicated
+// environment in which information about documents is stored. In
+// particular the tree structure of nested browsing contexts is
+// represented by the tree of BrowsingContexts.
+//
+// The tree of BrowsingContexts is created in step with its
+// corresponding nsDocShell, and when nsDocShells are connected
+// through a parent/child relationship, so are BrowsingContexts. The
+// major difference is that BrowsingContexts are replicated (synced)
+// to the parent process, making it possible to traverse the
+// BrowsingContext tree for a tab, in both the parent and the child
+// process.
+//
+// Trees of BrowsingContexts should only ever contain nodes of the
+// same BrowsingContext::Type. This is enforced by asserts in the
+// BrowsingContext::Create* methods.
+class BrowsingContext : public nsILoadContext, public nsWrapperCache {
+ MOZ_DECL_SYNCED_CONTEXT(BrowsingContext, MOZ_EACH_BC_FIELD)
+
+ public:
+ enum class Type { Chrome, Content };
+
+ static void Init();
+ static LogModule* GetLog();
+ static LogModule* GetSyncLog();
+
+ // Look up a BrowsingContext in the current process by ID.
+ static already_AddRefed<BrowsingContext> Get(uint64_t aId);
+ static already_AddRefed<BrowsingContext> Get(GlobalObject&, uint64_t aId) {
+ return Get(aId);
+ }
+ // Look up the top-level BrowsingContext by BrowserID.
+ static already_AddRefed<BrowsingContext> GetCurrentTopByBrowserId(
+ uint64_t aBrowserId);
+ static already_AddRefed<BrowsingContext> GetCurrentTopByBrowserId(
+ GlobalObject&, uint64_t aId) {
+ return GetCurrentTopByBrowserId(aId);
+ }
+
+ static void UpdateCurrentTopByBrowserId(BrowsingContext* aNewBrowsingContext);
+
+ static already_AddRefed<BrowsingContext> GetFromWindow(
+ WindowProxyHolder& aProxy);
+ static already_AddRefed<BrowsingContext> GetFromWindow(
+ GlobalObject&, WindowProxyHolder& aProxy) {
+ return GetFromWindow(aProxy);
+ }
+
+ static void DiscardFromContentParent(ContentParent* aCP);
+
+ // Create a brand-new toplevel BrowsingContext with no relationships to other
+ // BrowsingContexts, and which is not embedded within any <browser> or frame
+ // element.
+ //
+ // This BrowsingContext is immediately attached, and cannot have LoadContext
+ // flags customized unless it is of `Type::Chrome`.
+ //
+ // The process which created this BrowsingContext is responsible for detaching
+ // it.
+ static already_AddRefed<BrowsingContext> CreateIndependent(Type aType);
+
+ // Create a brand-new BrowsingContext object, but does not immediately attach
+ // it. State such as OriginAttributes and PrivateBrowsingId may be customized
+ // to configure the BrowsingContext before it is attached.
+ //
+ // `EnsureAttached()` must be called before the BrowsingContext is used for a
+ // DocShell, BrowserParent, or BrowserBridgeChild.
+ static already_AddRefed<BrowsingContext> CreateDetached(
+ nsGlobalWindowInner* aParent, BrowsingContext* aOpener,
+ BrowsingContextGroup* aSpecificGroup, const nsAString& aName, Type aType,
+ bool aIsPopupRequested, bool aCreatedDynamically = false);
+
+ void EnsureAttached();
+
+ bool EverAttached() const { return mEverAttached; }
+
+ // Cast this object to a canonical browsing context, and return it.
+ CanonicalBrowsingContext* Canonical();
+
+ // Is the most recent Document in this BrowsingContext loaded within this
+ // process? This may be true with a null mDocShell after the Window has been
+ // closed.
+ bool IsInProcess() const { return mIsInProcess; }
+
+ bool IsOwnedByProcess() const;
+
+ bool CanHaveRemoteOuterProxies() const {
+ return !mIsInProcess || mDanglingRemoteOuterProxies;
+ }
+
+ // Has this BrowsingContext been discarded. A discarded browsing context has
+ // been destroyed, and may not be available on the other side of an IPC
+ // message.
+ bool IsDiscarded() const { return mIsDiscarded; }
+
+ // Returns true if none of the BrowsingContext's ancestor BrowsingContexts or
+ // WindowContexts are discarded or cached.
+ bool AncestorsAreCurrent() const;
+
+ bool Windowless() const { return mWindowless; }
+
+ // Get the DocShell for this BrowsingContext if it is in-process, or
+ // null if it's not.
+ nsIDocShell* GetDocShell() const { return mDocShell; }
+ void SetDocShell(nsIDocShell* aDocShell);
+ void ClearDocShell() { mDocShell = nullptr; }
+
+ // Get the Document for this BrowsingContext if it is in-process, or
+ // null if it's not.
+ Document* GetDocument() const {
+ return mDocShell ? mDocShell->GetDocument() : nullptr;
+ }
+ Document* GetExtantDocument() const {
+ return mDocShell ? mDocShell->GetExtantDocument() : nullptr;
+ }
+
+ // This cleans up remote outer window proxies that might have been left behind
+ // when the browsing context went from being remote to local. It does this by
+ // turning them into cross-compartment wrappers to aOuter. If there is already
+ // a remote proxy in the compartment of aOuter, then aOuter will get swapped
+ // to it and the value of aOuter will be set to the object that used to be the
+ // remote proxy and is now an OuterWindowProxy.
+ void CleanUpDanglingRemoteOuterWindowProxies(
+ JSContext* aCx, JS::MutableHandle<JSObject*> aOuter);
+
+ // Get the embedder element for this BrowsingContext if the embedder is
+ // in-process, or null if it's not.
+ Element* GetEmbedderElement() const { return mEmbedderElement; }
+ void SetEmbedderElement(Element* aEmbedder);
+
+ // Return true if the type of the embedder element is either object
+ // or embed, false otherwise.
+ bool IsEmbedderTypeObjectOrEmbed();
+
+ // Called after the BrowingContext has been embedded in a FrameLoader. This
+ // happens after `SetEmbedderElement` is called on the BrowsingContext and
+ // after the BrowsingContext has been set on the FrameLoader.
+ void Embed();
+
+ // Get the outer window object for this BrowsingContext if it is in-process
+ // and still has a docshell, or null otherwise.
+ nsPIDOMWindowOuter* GetDOMWindow() const {
+ return mDocShell ? mDocShell->GetWindow() : nullptr;
+ }
+
+ uint64_t GetRequestContextId() const { return mRequestContextId; }
+
+ // Detach the current BrowsingContext from its parent, in both the
+ // child and the parent process.
+ void Detach(bool aFromIPC = false);
+
+ // Prepare this BrowsingContext to leave the current process.
+ void PrepareForProcessChange();
+
+ // Triggers a load in the process which currently owns this BrowsingContext.
+ nsresult LoadURI(nsDocShellLoadState* aLoadState,
+ bool aSetNavigating = false);
+
+ nsresult InternalLoad(nsDocShellLoadState* aLoadState);
+
+ // Removes the root document for this BrowsingContext tree from the BFCache,
+ // if it is cached, and returns true if it was.
+ bool RemoveRootFromBFCacheSync();
+
+ // If the load state includes a source BrowsingContext has been passed, check
+ // to see if we are sandboxed from it as the result of an iframe or CSP
+ // sandbox.
+ nsresult CheckSandboxFlags(nsDocShellLoadState* aLoadState);
+
+ void DisplayLoadError(const nsAString& aURI);
+
+ // Check that this browsing context is targetable for navigations (i.e. that
+ // it is neither closed, cached, nor discarded).
+ bool IsTargetable() const;
+
+ // True if this browsing context is inactive and is able to be suspended.
+ bool InactiveForSuspend() const;
+
+ const nsString& Name() const { return GetName(); }
+ void GetName(nsAString& aName) { aName = GetName(); }
+ bool NameEquals(const nsAString& aName) { return GetName().Equals(aName); }
+
+ Type GetType() const { return mType; }
+ bool IsContent() const { return mType == Type::Content; }
+ bool IsChrome() const { return !IsContent(); }
+
+ bool IsTop() const { return !GetParent(); }
+ bool IsSubframe() const { return !IsTop(); }
+
+ bool IsTopContent() const { return IsContent() && IsTop(); }
+
+ bool IsInSubtreeOf(BrowsingContext* aContext);
+
+ bool IsContentSubframe() const { return IsContent() && IsSubframe(); }
+
+ // non-zero
+ uint64_t Id() const { return mBrowsingContextId; }
+
+ BrowsingContext* GetParent() const;
+ BrowsingContext* Top();
+ const BrowsingContext* Top() const;
+
+ int32_t IndexOf(BrowsingContext* aChild);
+
+ // NOTE: Unlike `GetEmbedderWindowGlobal`, `GetParentWindowContext` does not
+ // cross toplevel content browser boundaries.
+ WindowContext* GetParentWindowContext() const { return mParentWindow; }
+ WindowContext* GetTopWindowContext() const;
+
+ already_AddRefed<BrowsingContext> GetOpener() const {
+ RefPtr<BrowsingContext> opener(Get(GetOpenerId()));
+ if (!mIsDiscarded && opener && !opener->mIsDiscarded) {
+ MOZ_DIAGNOSTIC_ASSERT(opener->mType == mType);
+ return opener.forget();
+ }
+ return nullptr;
+ }
+ void SetOpener(BrowsingContext* aOpener);
+ bool HasOpener() const;
+
+ bool HadOriginalOpener() const { return GetHadOriginalOpener(); }
+
+ // Returns true if the browsing context and top context are same origin
+ bool SameOriginWithTop();
+
+ /**
+ * When a new browsing context is opened by a sandboxed document, it needs to
+ * keep track of the browsing context that opened it, so that it can be
+ * navigated by it. This is the "one permitted sandboxed navigator".
+ */
+ already_AddRefed<BrowsingContext> GetOnePermittedSandboxedNavigator() const {
+ return Get(GetOnePermittedSandboxedNavigatorId());
+ }
+ [[nodiscard]] nsresult SetOnePermittedSandboxedNavigator(
+ BrowsingContext* aNavigator) {
+ if (GetOnePermittedSandboxedNavigatorId()) {
+ MOZ_ASSERT(false,
+ "One Permitted Sandboxed Navigator should only be set once.");
+ return NS_ERROR_FAILURE;
+ } else {
+ return SetOnePermittedSandboxedNavigatorId(aNavigator ? aNavigator->Id()
+ : 0);
+ }
+ }
+
+ uint32_t SandboxFlags() const { return GetSandboxFlags(); }
+
+ Span<RefPtr<BrowsingContext>> Children() const;
+ void GetChildren(nsTArray<RefPtr<BrowsingContext>>& aChildren);
+
+ Span<RefPtr<BrowsingContext>> NonSyntheticChildren() const;
+
+ const nsTArray<RefPtr<WindowContext>>& GetWindowContexts() {
+ return mWindowContexts;
+ }
+ void GetWindowContexts(nsTArray<RefPtr<WindowContext>>& aWindows);
+
+ void RegisterWindowContext(WindowContext* aWindow);
+ void UnregisterWindowContext(WindowContext* aWindow);
+ WindowContext* GetCurrentWindowContext() const {
+ return mCurrentWindowContext;
+ }
+
+ // Helpers to traverse this BrowsingContext subtree. Note that these will only
+ // traverse active contexts, and will ignore ones in the BFCache.
+ enum class WalkFlag {
+ Next,
+ Skip,
+ Stop,
+ };
+
+ /**
+ * Walk the browsing context tree in pre-order and call `aCallback`
+ * for every node in the tree. PreOrderWalk accepts two types of
+ * callbacks, either of the type `void(BrowsingContext*)` or
+ * `WalkFlag(BrowsingContext*)`. The former traverses the entire
+ * tree, but the latter let's you control if a sub-tree should be
+ * skipped by returning `WalkFlag::Skip`, completely abort traversal
+ * by returning `WalkFlag::Stop` or continue as normal with
+ * `WalkFlag::Next`.
+ */
+ template <typename F>
+ void PreOrderWalk(F&& aCallback) {
+ if constexpr (std::is_void_v<
+ typename std::invoke_result_t<F, BrowsingContext*>>) {
+ PreOrderWalkVoid(std::forward<F>(aCallback));
+ } else {
+ PreOrderWalkFlag(std::forward<F>(aCallback));
+ }
+ }
+
+ void PreOrderWalkVoid(const std::function<void(BrowsingContext*)>& aCallback);
+ WalkFlag PreOrderWalkFlag(
+ const std::function<WalkFlag(BrowsingContext*)>& aCallback);
+
+ void PostOrderWalk(const std::function<void(BrowsingContext*)>& aCallback);
+
+ void GetAllBrowsingContextsInSubtree(
+ nsTArray<RefPtr<BrowsingContext>>& aBrowsingContexts);
+
+ BrowsingContextGroup* Group() { return mGroup; }
+
+ // WebIDL bindings for nsILoadContext
+ Nullable<WindowProxyHolder> GetAssociatedWindow();
+ Nullable<WindowProxyHolder> GetTopWindow();
+ Element* GetTopFrameElement();
+ bool GetIsContent() { return IsContent(); }
+ void SetUsePrivateBrowsing(bool aUsePrivateBrowsing, ErrorResult& aError);
+ // Needs a different name to disambiguate from the xpidl method with
+ // the same signature but different return value.
+ void SetUseTrackingProtectionWebIDL(bool aUseTrackingProtection,
+ ErrorResult& aRv);
+ bool UseTrackingProtectionWebIDL() { return UseTrackingProtection(); }
+ void GetOriginAttributes(JSContext* aCx, JS::MutableHandle<JS::Value> aVal,
+ ErrorResult& aError);
+
+ bool InRDMPane() const { return GetInRDMPane(); }
+
+ bool WatchedByDevTools();
+ void SetWatchedByDevTools(bool aWatchedByDevTools, ErrorResult& aRv);
+
+ dom::TouchEventsOverride TouchEventsOverride() const;
+ bool TargetTopLevelLinkClicksToBlank() const;
+
+ bool FullscreenAllowed() const;
+
+ float FullZoom() const { return GetFullZoom(); }
+ float TextZoom() const { return GetTextZoom(); }
+
+ float OverrideDPPX() const { return Top()->GetOverrideDPPX(); }
+
+ bool SuspendMediaWhenInactive() const {
+ return GetSuspendMediaWhenInactive();
+ }
+
+ bool IsActive() const;
+ bool ForceOffline() const { return GetForceOffline(); }
+
+ bool ForceDesktopViewport() const { return GetForceDesktopViewport(); }
+
+ bool AuthorStyleDisabledDefault() const {
+ return GetAuthorStyleDisabledDefault();
+ }
+
+ bool UseGlobalHistory() const { return GetUseGlobalHistory(); }
+
+ bool GetIsActiveBrowserWindow();
+
+ void SetIsActiveBrowserWindow(bool aActive);
+
+ uint64_t BrowserId() const { return GetBrowserId(); }
+
+ bool IsLoading();
+
+ void GetEmbedderElementType(nsString& aElementType) {
+ if (GetEmbedderElementType().isSome()) {
+ aElementType = GetEmbedderElementType().value();
+ }
+ }
+
+ bool IsLoadingIdentifier(uint64_t aLoadIdentifer) {
+ if (GetCurrentLoadIdentifier() &&
+ *GetCurrentLoadIdentifier() == aLoadIdentifer) {
+ return true;
+ }
+ return false;
+ }
+
+ // ScreenOrientation related APIs
+ [[nodiscard]] nsresult SetCurrentOrientation(OrientationType aType,
+ float aAngle) {
+ Transaction txn;
+ txn.SetCurrentOrientationType(aType);
+ txn.SetCurrentOrientationAngle(aAngle);
+ return txn.Commit(this);
+ }
+
+ void SetRDMPaneOrientation(OrientationType aType, float aAngle,
+ ErrorResult& aRv) {
+ if (InRDMPane()) {
+ if (NS_FAILED(SetCurrentOrientation(aType, aAngle))) {
+ aRv.ThrowInvalidStateError("Browsing context is discarded");
+ }
+ }
+ }
+
+ void SetRDMPaneMaxTouchPoints(uint8_t aMaxTouchPoints, ErrorResult& aRv) {
+ if (InRDMPane()) {
+ SetMaxTouchPointsOverride(aMaxTouchPoints, aRv);
+ }
+ }
+
+ // Find a browsing context in this context's list of
+ // children. Doesn't consider the special names, '_self', '_parent',
+ // '_top', or '_blank'. Performs access control checks with regard to
+ // 'this'.
+ BrowsingContext* FindChildWithName(const nsAString& aName,
+ WindowGlobalChild& aRequestingWindow);
+
+ // Find a browsing context in the subtree rooted at 'this' Doesn't
+ // consider the special names, '_self', '_parent', '_top', or
+ // '_blank'.
+ //
+ // If passed, performs access control checks with regard to
+ // 'aRequestingContext', otherwise performs no access checks.
+ BrowsingContext* FindWithNameInSubtree(const nsAString& aName,
+ WindowGlobalChild* aRequestingWindow);
+
+ // Find the special browsing context if aName is '_self', '_parent',
+ // '_top', but not '_blank'. The latter is handled in FindWithName
+ BrowsingContext* FindWithSpecialName(const nsAString& aName,
+ WindowGlobalChild& aRequestingWindow);
+
+ nsISupports* GetParentObject() const;
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Return the window proxy object that corresponds to this browsing context.
+ inline JSObject* GetWindowProxy() const { return mWindowProxy; }
+ inline JSObject* GetUnbarrieredWindowProxy() const {
+ return mWindowProxy.unbarrieredGet();
+ }
+
+ // Set the window proxy object that corresponds to this browsing context.
+ void SetWindowProxy(JS::Handle<JSObject*> aWindowProxy) {
+ mWindowProxy = aWindowProxy;
+ }
+
+ Nullable<WindowProxyHolder> GetWindow();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS(BrowsingContext)
+ NS_DECL_NSILOADCONTEXT
+
+ // Window APIs that are cross-origin-accessible (from the HTML spec).
+ WindowProxyHolder Window();
+ BrowsingContext* GetBrowsingContext() { return this; };
+ BrowsingContext* Self() { return this; }
+ void Location(JSContext* aCx, JS::MutableHandle<JSObject*> aLocation,
+ ErrorResult& aError);
+ void Close(CallerType aCallerType, ErrorResult& aError);
+ bool GetClosed(ErrorResult&) { return GetClosed(); }
+ void Focus(CallerType aCallerType, ErrorResult& aError);
+ void Blur(CallerType aCallerType, ErrorResult& aError);
+ WindowProxyHolder GetFrames(ErrorResult& aError);
+ int32_t Length() const { return Children().Length(); }
+ Nullable<WindowProxyHolder> GetTop(ErrorResult& aError);
+ void GetOpener(JSContext* aCx, JS::MutableHandle<JS::Value> aOpener,
+ ErrorResult& aError) const;
+ Nullable<WindowProxyHolder> GetParent(ErrorResult& aError);
+ void PostMessageMoz(JSContext* aCx, JS::Handle<JS::Value> aMessage,
+ const nsAString& aTargetOrigin,
+ const Sequence<JSObject*>& aTransfer,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError);
+ void PostMessageMoz(JSContext* aCx, JS::Handle<JS::Value> aMessage,
+ const WindowPostMessageOptions& aOptions,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError);
+
+ void GetCustomUserAgent(nsAString& aUserAgent) {
+ aUserAgent = Top()->GetUserAgentOverride();
+ }
+ nsresult SetCustomUserAgent(const nsAString& aUserAgent);
+ void SetCustomUserAgent(const nsAString& aUserAgent, ErrorResult& aRv);
+
+ void GetCustomPlatform(nsAString& aPlatform) {
+ aPlatform = Top()->GetPlatformOverride();
+ }
+ void SetCustomPlatform(const nsAString& aPlatform, ErrorResult& aRv);
+
+ JSObject* WrapObject(JSContext* aCx);
+
+ static JSObject* ReadStructuredClone(JSContext* aCx,
+ JSStructuredCloneReader* aReader,
+ StructuredCloneHolder* aHolder);
+ bool WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter,
+ StructuredCloneHolder* aHolder);
+
+ void StartDelayedAutoplayMediaComponents();
+
+ [[nodiscard]] nsresult ResetGVAutoplayRequestStatus();
+
+ /**
+ * Information required to initialize a BrowsingContext in another process.
+ * This object may be serialized over IPC.
+ */
+ struct IPCInitializer {
+ uint64_t mId = 0;
+
+ // IDs are used for Parent and Opener to allow for this object to be
+ // deserialized before other BrowsingContext in the BrowsingContextGroup
+ // have been initialized.
+ uint64_t mParentId = 0;
+ already_AddRefed<WindowContext> GetParent();
+ already_AddRefed<BrowsingContext> GetOpener();
+
+ uint64_t GetOpenerId() const { return mFields.Get<IDX_OpenerId>(); }
+
+ bool mWindowless = false;
+ bool mUseRemoteTabs = false;
+ bool mUseRemoteSubframes = false;
+ bool mCreatedDynamically = false;
+ int32_t mChildOffset = 0;
+ int32_t mSessionHistoryIndex = -1;
+ int32_t mSessionHistoryCount = 0;
+ OriginAttributes mOriginAttributes;
+ uint64_t mRequestContextId = 0;
+
+ FieldValues mFields;
+ };
+
+ // Create an IPCInitializer object for this BrowsingContext.
+ IPCInitializer GetIPCInitializer();
+
+ // Create a BrowsingContext object from over IPC.
+ static mozilla::ipc::IPCResult CreateFromIPC(IPCInitializer&& aInitializer,
+ BrowsingContextGroup* aGroup,
+ ContentParent* aOriginProcess);
+
+ bool IsSandboxedFrom(BrowsingContext* aTarget);
+
+ // The runnable will be called once there is idle time, or the top level
+ // page has been loaded or if a timeout has fired.
+ // Must be called only on the top level BrowsingContext.
+ void AddDeprioritizedLoadRunner(nsIRunnable* aRunner);
+
+ RefPtr<SessionStorageManager> GetSessionStorageManager();
+
+ // Set PendingInitialization on this BrowsingContext before the context has
+ // been attached.
+ void InitPendingInitialization(bool aPendingInitialization) {
+ MOZ_ASSERT(!EverAttached());
+ mFields.SetWithoutSyncing<IDX_PendingInitialization>(
+ aPendingInitialization);
+ }
+
+ bool CreatedDynamically() const { return mCreatedDynamically; }
+
+ // Returns true if this browsing context, or any ancestor to this browsing
+ // context was created dynamically. See also `CreatedDynamically`.
+ bool IsDynamic() const;
+
+ int32_t ChildOffset() const { return mChildOffset; }
+
+ bool GetOffsetPath(nsTArray<uint32_t>& aPath) const;
+
+ const OriginAttributes& OriginAttributesRef() { return mOriginAttributes; }
+ nsresult SetOriginAttributes(const OriginAttributes& aAttrs);
+
+ void GetHistoryID(JSContext* aCx, JS::MutableHandle<JS::Value> aVal,
+ ErrorResult& aError);
+
+ // This should only be called on the top browsing context.
+ void InitSessionHistory();
+
+ // This will only ever return a non-null value if called on the top browsing
+ // context.
+ ChildSHistory* GetChildSessionHistory();
+
+ bool CrossOriginIsolated();
+
+ // Check if it is allowed to open a popup from the current browsing
+ // context or any of its ancestors.
+ bool IsPopupAllowed();
+
+ // aCurrentURI is only required to be non-null if the load type contains the
+ // nsIWebNavigation::LOAD_FLAGS_IS_REFRESH flag and aInfo is for a refresh to
+ // the current URI.
+ void SessionHistoryCommit(const LoadingSessionHistoryInfo& aInfo,
+ uint32_t aLoadType, nsIURI* aCurrentURI,
+ SessionHistoryInfo* aPreviousActiveEntry,
+ bool aPersist, bool aCloneEntryChildren,
+ bool aChannelExpired, uint32_t aCacheKey);
+
+ // Set a new active entry on this browsing context. This is used for
+ // implementing history.pushState/replaceState and same document navigations.
+ // The new active entry will be linked to the current active entry through
+ // its shared state.
+ // aPreviousScrollPos is the scroll position that needs to be saved on the
+ // previous active entry.
+ // aUpdatedCacheKey is the cache key to set on the new active entry. If
+ // aUpdatedCacheKey is 0 then it will be ignored.
+ void SetActiveSessionHistoryEntry(const Maybe<nsPoint>& aPreviousScrollPos,
+ SessionHistoryInfo* aInfo,
+ uint32_t aLoadType,
+ uint32_t aUpdatedCacheKey,
+ bool aUpdateLength = true);
+
+ // Replace the active entry for this browsing context. This is used for
+ // implementing history.replaceState and same document navigations.
+ void ReplaceActiveSessionHistoryEntry(SessionHistoryInfo* aInfo);
+
+ // Removes dynamic child entries of the active entry.
+ void RemoveDynEntriesFromActiveSessionHistoryEntry();
+
+ // Removes entries corresponding to this BrowsingContext from session history.
+ void RemoveFromSessionHistory(const nsID& aChangeID);
+
+ void SetTriggeringAndInheritPrincipals(nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ uint64_t aLoadIdentifier);
+
+ // Return mTriggeringPrincipal and mPrincipalToInherit if the load id
+ // saved with the principal matches the current load identifier of this BC.
+ std::tuple<nsCOMPtr<nsIPrincipal>, nsCOMPtr<nsIPrincipal>>
+ GetTriggeringAndInheritPrincipalsForCurrentLoad();
+
+ void HistoryGo(int32_t aOffset, uint64_t aHistoryEpoch,
+ bool aRequireUserInteraction, bool aUserActivation,
+ std::function<void(Maybe<int32_t>&&)>&& aResolver);
+
+ bool ShouldUpdateSessionHistory(uint32_t aLoadType);
+
+ // Checks if we reached the rate limit for calls to Location and History API.
+ // The rate limit is controlled by the
+ // "dom.navigation.locationChangeRateLimit" prefs.
+ // Rate limit applies per BrowsingContext.
+ // Returns NS_OK if we are below the rate limit and increments the counter.
+ // Returns NS_ERROR_DOM_SECURITY_ERR if limit is reached.
+ nsresult CheckLocationChangeRateLimit(CallerType aCallerType);
+
+ void ResetLocationChangeRateLimit();
+
+ mozilla::dom::DisplayMode DisplayMode() { return Top()->GetDisplayMode(); }
+
+ // Returns canFocus, isActive
+ std::tuple<bool, bool> CanFocusCheck(CallerType aCallerType);
+
+ bool CanBlurCheck(CallerType aCallerType);
+
+ // Examine the current document state to see if we're in a way that is
+ // typically abused by web designers. The window.open code uses this
+ // routine to determine whether to allow the new window.
+ // Returns a value from the PopupControlState enum.
+ PopupBlocker::PopupControlState RevisePopupAbuseLevel(
+ PopupBlocker::PopupControlState aControl);
+
+ // Get the modifiers associated with the user activation for relevant
+ // documents. The window.open code uses this routine to determine where the
+ // new window should be located.
+ void GetUserActivationModifiersForPopup(
+ UserActivation::Modifiers* aModifiers);
+
+ void IncrementHistoryEntryCountForBrowsingContext();
+
+ bool ServiceWorkersTestingEnabled() const {
+ return GetServiceWorkersTestingEnabled();
+ }
+
+ void GetMediumOverride(nsAString& aOverride) const {
+ aOverride = GetMediumOverride();
+ }
+
+ dom::PrefersColorSchemeOverride PrefersColorSchemeOverride() const {
+ return GetPrefersColorSchemeOverride();
+ }
+
+ bool IsInBFCache() const;
+
+ bool AllowJavascript() const { return GetAllowJavascript(); }
+ bool CanExecuteScripts() const { return mCanExecuteScripts; }
+
+ uint32_t DefaultLoadFlags() const { return GetDefaultLoadFlags(); }
+
+ // When request for page awake, it would increase a count that is used to
+ // prevent whole browsing context tree from being suspended. The request can
+ // be called multiple times. When calling the revoke, it would decrease the
+ // count and once the count reaches to zero, the browsing context tree could
+ // be suspended when the tree is inactive.
+ void RequestForPageAwake();
+ void RevokeForPageAwake();
+
+ void AddDiscardListener(std::function<void(uint64_t)>&& aListener);
+
+ bool IsAppTab() { return GetIsAppTab(); }
+ bool HasSiblings() { return GetHasSiblings(); }
+
+ bool IsUnderHiddenEmbedderElement() const {
+ return GetIsUnderHiddenEmbedderElement();
+ }
+
+ void LocationCreated(dom::Location* aLocation);
+ void ClearCachedValuesOfLocations();
+
+ protected:
+ virtual ~BrowsingContext();
+ BrowsingContext(WindowContext* aParentWindow, BrowsingContextGroup* aGroup,
+ uint64_t aBrowsingContextId, Type aType, FieldValues&& aInit);
+
+ void SetChildSHistory(ChildSHistory* aChildSHistory);
+ already_AddRefed<ChildSHistory> ForgetChildSHistory() {
+ // FIXME Do we need to unset mHasSessionHistory?
+ return mChildSessionHistory.forget();
+ }
+
+ static bool ShouldAddEntryForRefresh(nsIURI* aCurrentURI,
+ const SessionHistoryInfo& aInfo);
+ static bool ShouldAddEntryForRefresh(nsIURI* aCurrentURI, nsIURI* aNewURI,
+ bool aHasPostData);
+
+ private:
+ mozilla::ipc::IPCResult Attach(bool aFromIPC, ContentParent* aOriginProcess);
+
+ // Recomputes whether we can execute scripts in this BrowsingContext based on
+ // the value of AllowJavascript() and whether scripts are allowed in the
+ // parent WindowContext. Called whenever the AllowJavascript() flag or the
+ // parent WC changes.
+ void RecomputeCanExecuteScripts();
+
+ // Is it early enough in the BrowsingContext's lifecycle that it is still
+ // OK to set OriginAttributes?
+ bool CanSetOriginAttributes();
+
+ void AssertOriginAttributesMatchPrivateBrowsing();
+
+ // Assert that the BrowsingContext's LoadContext flags appear coherent
+ // relative to related BrowsingContexts.
+ void AssertCoherentLoadContext();
+
+ friend class ::nsOuterWindowProxy;
+ friend class ::nsGlobalWindowOuter;
+ friend class WindowContext;
+
+ // Update the window proxy object that corresponds to this browsing context.
+ // This should be called from the window proxy object's objectMoved hook, if
+ // the object mWindowProxy points to was moved by the JS GC.
+ void UpdateWindowProxy(JSObject* obj, JSObject* old) {
+ if (mWindowProxy) {
+ MOZ_ASSERT(mWindowProxy == old);
+ mWindowProxy = obj;
+ }
+ }
+ // Clear the window proxy object that corresponds to this browsing context.
+ // This should be called if the window proxy object is finalized, or it can't
+ // reach its browsing context anymore.
+ void ClearWindowProxy() { mWindowProxy = nullptr; }
+
+ friend class Location;
+ friend class RemoteLocationProxy;
+ /**
+ * LocationProxy is the class for the native object stored as a private in a
+ * RemoteLocationProxy proxy representing a Location object in a different
+ * process. It forwards all operations to its BrowsingContext and aggregates
+ * its refcount to that BrowsingContext.
+ */
+ class LocationProxy final : public LocationBase {
+ public:
+ MozExternalRefCountType AddRef() { return GetBrowsingContext()->AddRef(); }
+ MozExternalRefCountType Release() {
+ return GetBrowsingContext()->Release();
+ }
+
+ protected:
+ friend class RemoteLocationProxy;
+ BrowsingContext* GetBrowsingContext() override {
+ return reinterpret_cast<BrowsingContext*>(
+ uintptr_t(this) - offsetof(BrowsingContext, mLocation));
+ }
+
+ nsIDocShell* GetDocShell() override { return nullptr; }
+ };
+
+ // Send a given `BaseTransaction` object to the correct remote.
+ void SendCommitTransaction(ContentParent* aParent,
+ const BaseTransaction& aTxn, uint64_t aEpoch);
+ void SendCommitTransaction(ContentChild* aChild, const BaseTransaction& aTxn,
+ uint64_t aEpoch);
+
+ bool CanSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aEpoch,
+ ContentParent* aSource) {
+ return IsTop() && !aSource;
+ }
+
+ void DidSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aOldValue);
+
+ using CanSetResult = syncedcontext::CanSetResult;
+
+ // Ensure that opener is in the same BrowsingContextGroup.
+ bool CanSet(FieldIndex<IDX_OpenerId>, const uint64_t& aValue,
+ ContentParent* aSource) {
+ if (aValue != 0) {
+ RefPtr<BrowsingContext> opener = Get(aValue);
+ return opener && opener->Group() == Group();
+ }
+ return true;
+ }
+
+ bool CanSet(FieldIndex<IDX_OpenerPolicy>,
+ nsILoadInfo::CrossOriginOpenerPolicy, ContentParent*);
+
+ bool CanSet(FieldIndex<IDX_ServiceWorkersTestingEnabled>, bool,
+ ContentParent*) {
+ return IsTop();
+ }
+
+ bool CanSet(FieldIndex<IDX_MediumOverride>, const nsString&, ContentParent*) {
+ return IsTop();
+ }
+
+ bool CanSet(FieldIndex<IDX_EmbedderColorSchemes>, const EmbedderColorSchemes&,
+ ContentParent* aSource) {
+ return CheckOnlyEmbedderCanSet(aSource);
+ }
+
+ bool CanSet(FieldIndex<IDX_PrefersColorSchemeOverride>,
+ dom::PrefersColorSchemeOverride, ContentParent*) {
+ return IsTop();
+ }
+
+ void DidSet(FieldIndex<IDX_InRDMPane>, bool aOldValue);
+
+ void DidSet(FieldIndex<IDX_EmbedderColorSchemes>,
+ EmbedderColorSchemes&& aOldValue);
+
+ void DidSet(FieldIndex<IDX_PrefersColorSchemeOverride>,
+ dom::PrefersColorSchemeOverride aOldValue);
+
+ template <typename Callback>
+ void WalkPresContexts(Callback&&);
+ void PresContextAffectingFieldChanged();
+
+ void DidSet(FieldIndex<IDX_MediumOverride>, nsString&& aOldValue);
+
+ bool CanSet(FieldIndex<IDX_SuspendMediaWhenInactive>, bool, ContentParent*) {
+ return IsTop();
+ }
+
+ bool CanSet(FieldIndex<IDX_TouchEventsOverrideInternal>,
+ dom::TouchEventsOverride aTouchEventsOverride,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_TouchEventsOverrideInternal>,
+ dom::TouchEventsOverride&& aOldValue);
+
+ bool CanSet(FieldIndex<IDX_DisplayMode>, const enum DisplayMode& aDisplayMode,
+ ContentParent* aSource) {
+ return IsTop();
+ }
+
+ void DidSet(FieldIndex<IDX_DisplayMode>, enum DisplayMode aOldValue);
+
+ bool CanSet(FieldIndex<IDX_ExplicitActive>, const ExplicitActiveStatus&,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_ExplicitActive>, ExplicitActiveStatus aOldValue);
+
+ bool CanSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>, const bool& aValue,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>, bool aOldValue);
+
+ // Ensure that we only set the flag on the top level browsingContext.
+ // And then, we do a pre-order walk in the tree to refresh the
+ // volume of all media elements.
+ void DidSet(FieldIndex<IDX_Muted>);
+
+ bool CanSet(FieldIndex<IDX_IsAppTab>, const bool& aValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_HasSiblings>, const bool& aValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_ShouldDelayMediaFromStart>, const bool& aValue,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_ShouldDelayMediaFromStart>, bool aOldValue);
+
+ bool CanSet(FieldIndex<IDX_OverrideDPPX>, const float& aValue,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_OverrideDPPX>, float aOldValue);
+
+ bool CanSet(FieldIndex<IDX_EmbedderInnerWindowId>, const uint64_t& aValue,
+ ContentParent* aSource);
+
+ CanSetResult CanSet(FieldIndex<IDX_CurrentInnerWindowId>,
+ const uint64_t& aValue, ContentParent* aSource);
+
+ void DidSet(FieldIndex<IDX_CurrentInnerWindowId>);
+
+ bool CanSet(FieldIndex<IDX_ParentInitiatedNavigationEpoch>,
+ const uint64_t& aValue, ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_IsPopupSpam>, const bool& aValue,
+ ContentParent* aSource);
+
+ void DidSet(FieldIndex<IDX_IsPopupSpam>);
+
+ void DidSet(FieldIndex<IDX_GVAudibleAutoplayRequestStatus>);
+ void DidSet(FieldIndex<IDX_GVInaudibleAutoplayRequestStatus>);
+
+ void DidSet(FieldIndex<IDX_Loading>);
+
+ void DidSet(FieldIndex<IDX_AncestorLoading>);
+
+ void DidSet(FieldIndex<IDX_PlatformOverride>);
+ CanSetResult CanSet(FieldIndex<IDX_PlatformOverride>,
+ const nsString& aPlatformOverride,
+ ContentParent* aSource);
+
+ void DidSet(FieldIndex<IDX_UserAgentOverride>);
+ CanSetResult CanSet(FieldIndex<IDX_UserAgentOverride>,
+ const nsString& aUserAgent, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_OrientationLock>,
+ const mozilla::hal::ScreenOrientation& aOrientationLock,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_EmbedderElementType>,
+ const Maybe<nsString>& aInitiatorType, ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_MessageManagerGroup>,
+ const nsString& aMessageManagerGroup, ContentParent* aSource);
+
+ CanSetResult CanSet(FieldIndex<IDX_AllowContentRetargeting>,
+ const bool& aAllowContentRetargeting,
+ ContentParent* aSource);
+ CanSetResult CanSet(FieldIndex<IDX_AllowContentRetargetingOnChildren>,
+ const bool& aAllowContentRetargetingOnChildren,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_FullscreenAllowedByOwner>, const bool&,
+ ContentParent*);
+ bool CanSet(FieldIndex<IDX_WatchedByDevToolsInternal>,
+ const bool& aWatchedByDevToolsInternal, ContentParent* aSource);
+
+ CanSetResult CanSet(FieldIndex<IDX_DefaultLoadFlags>,
+ const uint32_t& aDefaultLoadFlags,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_DefaultLoadFlags>);
+
+ bool CanSet(FieldIndex<IDX_UseGlobalHistory>, const bool& aUseGlobalHistory,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>,
+ const bool& aTargetTopLevelLinkClicksToBlankInternal,
+ ContentParent* aSource);
+
+ void DidSet(FieldIndex<IDX_HasSessionHistory>, bool aOldValue);
+
+ bool CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_UseErrorPages>, const bool& aUseErrorPages,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_PendingInitialization>, bool aNewValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_PageAwakeRequestCount>, uint32_t aNewValue,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_PageAwakeRequestCount>, uint32_t aOldValue);
+
+ CanSetResult CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue);
+
+ bool CanSet(FieldIndex<IDX_ForceDesktopViewport>, bool aValue,
+ ContentParent* aSource) {
+ return IsTop() && XRE_IsParentProcess();
+ }
+
+ // TODO(emilio): Maybe handle the flag being set dynamically without
+ // navigating? The previous code didn't do it tho, and a reload is probably
+ // worth it regardless.
+ // void DidSet(FieldIndex<IDX_ForceDesktopViewport>, bool aOldValue);
+
+ bool CanSet(FieldIndex<IDX_HasRestoreData>, bool aNewValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>,
+ const bool& aIsUnderHiddenEmbedderElement,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_ForceOffline>, bool aNewValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_EmbeddedInContentDocument>, bool,
+ ContentParent* aSource) {
+ return CheckOnlyEmbedderCanSet(aSource);
+ }
+
+ template <size_t I, typename T>
+ bool CanSet(FieldIndex<I>, const T&, ContentParent*) {
+ return true;
+ }
+
+ // Overload `DidSet` to get notifications for a particular field being set.
+ //
+ // You can also overload the variant that gets the old value if you need it.
+ template <size_t I>
+ void DidSet(FieldIndex<I>) {}
+ template <size_t I, typename T>
+ void DidSet(FieldIndex<I>, T&& aOldValue) {}
+
+ void DidSet(FieldIndex<IDX_FullZoom>, float aOldValue);
+ void DidSet(FieldIndex<IDX_TextZoom>, float aOldValue);
+ void DidSet(FieldIndex<IDX_AuthorStyleDisabledDefault>);
+
+ bool CanSet(FieldIndex<IDX_IsInBFCache>, bool, ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_IsInBFCache>);
+
+ void DidSet(FieldIndex<IDX_SyntheticDocumentContainer>);
+
+ void DidSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>, bool aOldValue);
+
+ // Allow if the process attemping to set field is the same as the owning
+ // process. Deprecated. New code that might use this should generally be moved
+ // to WindowContext or be settable only by the parent process.
+ CanSetResult LegacyRevertIfNotOwningOrParentProcess(ContentParent* aSource);
+
+ // True if the process attempting to set field is the same as the embedder's
+ // process.
+ bool CheckOnlyEmbedderCanSet(ContentParent* aSource);
+
+ void CreateChildSHistory();
+
+ using PrincipalWithLoadIdentifierTuple =
+ std::tuple<nsCOMPtr<nsIPrincipal>, uint64_t>;
+
+ nsIPrincipal* GetSavedPrincipal(
+ Maybe<PrincipalWithLoadIdentifierTuple> aPrincipalTuple);
+
+ // Type of BrowsingContent
+ const Type mType;
+
+ // Unique id identifying BrowsingContext
+ const uint64_t mBrowsingContextId;
+
+ RefPtr<BrowsingContextGroup> mGroup;
+ RefPtr<WindowContext> mParentWindow;
+ nsCOMPtr<nsIDocShell> mDocShell;
+
+ RefPtr<Element> mEmbedderElement;
+
+ nsTArray<RefPtr<WindowContext>> mWindowContexts;
+ RefPtr<WindowContext> mCurrentWindowContext;
+
+ // This is not a strong reference, but using a JS::Heap for that should be
+ // fine. The JSObject stored in here should be a proxy with a
+ // nsOuterWindowProxy handler, which will update the pointer from its
+ // objectMoved hook and clear it from its finalize hook.
+ JS::Heap<JSObject*> mWindowProxy;
+ LocationProxy mLocation;
+
+ // OriginAttributes for this BrowsingContext. May not be changed after this
+ // BrowsingContext is attached.
+ OriginAttributes mOriginAttributes;
+
+ // The network request context id, representing the nsIRequestContext
+ // associated with this BrowsingContext, and LoadGroups created for it.
+ uint64_t mRequestContextId = 0;
+
+ // Determines if private browsing should be used. May not be changed after
+ // this BrowsingContext is attached. This field matches mOriginAttributes in
+ // content Browsing Contexts. Currently treated as a binary value: 1 - in
+ // private mode, 0 - not private mode.
+ uint32_t mPrivateBrowsingId;
+
+ // True if Attach() has been called on this BrowsingContext already.
+ bool mEverAttached : 1;
+
+ // Is the most recent Document in this BrowsingContext loaded within this
+ // process? This may be true with a null mDocShell after the Window has been
+ // closed.
+ bool mIsInProcess : 1;
+
+ // Has this browsing context been discarded? BrowsingContexts should
+ // only be discarded once.
+ bool mIsDiscarded : 1;
+
+ // True if this BrowsingContext has no associated visible window, and is owned
+ // by whichever process created it, even if top-level.
+ bool mWindowless : 1;
+
+ // This is true if the BrowsingContext was out of process, but is now in
+ // process, and might have remote window proxies that need to be cleaned up.
+ bool mDanglingRemoteOuterProxies : 1;
+
+ // True if this BrowsingContext has been embedded in a element in this
+ // process.
+ bool mEmbeddedByThisProcess : 1;
+
+ // Determines if remote (out-of-process) tabs should be used. May not be
+ // changed after this BrowsingContext is attached.
+ bool mUseRemoteTabs : 1;
+
+ // Determines if out-of-process iframes should be used. May not be changed
+ // after this BrowsingContext is attached.
+ bool mUseRemoteSubframes : 1;
+
+ // True if this BrowsingContext is for a frame that was added dynamically.
+ bool mCreatedDynamically : 1;
+
+ // Set to true if the browsing context is in the bfcache and pagehide has been
+ // dispatched. When coming out from the bfcache, the value is set to false
+ // before dispatching pageshow.
+ bool mIsInBFCache : 1;
+
+ // Determines if we can execute scripts in this BrowsingContext. True if
+ // AllowJavascript() is true and script execution is allowed in the parent
+ // WindowContext.
+ bool mCanExecuteScripts : 1;
+
+ // The original offset of this context in its container. This property is -1
+ // if this BrowsingContext is for a frame that was added dynamically.
+ int32_t mChildOffset;
+
+ // The start time of user gesture, this is only available if the browsing
+ // context is in process.
+ TimeStamp mUserGestureStart;
+
+ // Triggering principal and principal to inherit need to point to original
+ // principal instances if the document is loaded in the same process as the
+ // process that initiated the load. When the load starts we save the
+ // principals along with the current load id.
+ // These principals correspond to the most recent load that took place within
+ // the process of this browsing context.
+ Maybe<PrincipalWithLoadIdentifierTuple> mTriggeringPrincipal;
+ Maybe<PrincipalWithLoadIdentifierTuple> mPrincipalToInherit;
+
+ class DeprioritizedLoadRunner
+ : public mozilla::Runnable,
+ public mozilla::LinkedListElement<DeprioritizedLoadRunner> {
+ public:
+ explicit DeprioritizedLoadRunner(nsIRunnable* aInner)
+ : Runnable("DeprioritizedLoadRunner"), mInner(aInner) {}
+
+ NS_IMETHOD Run() override {
+ if (mInner) {
+ RefPtr<nsIRunnable> inner = std::move(mInner);
+ inner->Run();
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<nsIRunnable> mInner;
+ };
+
+ mozilla::LinkedList<DeprioritizedLoadRunner> mDeprioritizedLoadRunner;
+
+ RefPtr<SessionStorageManager> mSessionStorageManager;
+ RefPtr<ChildSHistory> mChildSessionHistory;
+
+ nsTArray<std::function<void(uint64_t)>> mDiscardListeners;
+
+ // Counter and time span for rate limiting Location and History API calls.
+ // Used by CheckLocationChangeRateLimit. Do not apply cross-process.
+ uint32_t mLocationChangeRateLimitCount;
+ mozilla::TimeStamp mLocationChangeRateLimitSpanStart;
+
+ mozilla::LinkedList<dom::Location> mLocations;
+};
+
+/**
+ * Gets a WindowProxy object for a BrowsingContext that lives in a different
+ * process (creating the object if it doesn't already exist). The WindowProxy
+ * object will be in the compartment that aCx is currently in. This should only
+ * be called if aContext doesn't hold a docshell, otherwise the BrowsingContext
+ * lives in this process, and a same-process WindowProxy should be used (see
+ * nsGlobalWindowOuter). This should only be called by bindings code, ToJSValue
+ * is the right API to get a WindowProxy for a BrowsingContext.
+ *
+ * If aTransplantTo is non-null, then the WindowProxy object will eventually be
+ * transplanted onto it. Therefore it should be used as the value in the remote
+ * proxy map. We assume that in this case the failure is unrecoverable, so we
+ * crash immediately rather than return false.
+ */
+extern bool GetRemoteOuterWindowProxy(JSContext* aCx, BrowsingContext* aContext,
+ JS::Handle<JSObject*> aTransplantTo,
+ JS::MutableHandle<JSObject*> aRetVal);
+
+using BrowsingContextTransaction = BrowsingContext::BaseTransaction;
+using BrowsingContextInitializer = BrowsingContext::IPCInitializer;
+using MaybeDiscardedBrowsingContext = MaybeDiscarded<BrowsingContext>;
+
+// Specialize the transaction object for every translation unit it's used in.
+extern template class syncedcontext::Transaction<BrowsingContext>;
+
+} // namespace dom
+
+// Allow sending BrowsingContext objects over IPC.
+namespace ipc {
+template <>
+struct IPDLParamTraits<dom::MaybeDiscarded<dom::BrowsingContext>> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::MaybeDiscarded<dom::BrowsingContext>& aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::MaybeDiscarded<dom::BrowsingContext>* aResult);
+};
+
+template <>
+struct IPDLParamTraits<dom::BrowsingContext::IPCInitializer> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::BrowsingContext::IPCInitializer& aInitializer);
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::BrowsingContext::IPCInitializer* aInitializer);
+};
+} // namespace ipc
+} // namespace mozilla
+
+#endif // !defined(mozilla_dom_BrowsingContext_h)
diff --git a/docshell/base/BrowsingContextGroup.cpp b/docshell/base/BrowsingContextGroup.cpp
new file mode 100644
index 0000000000..622ed2a6c8
--- /dev/null
+++ b/docshell/base/BrowsingContextGroup.cpp
@@ -0,0 +1,570 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BrowsingContextGroup.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/InputTaskManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/BrowsingContextBinding.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/ThrottledEventQueue.h"
+#include "nsFocusManager.h"
+#include "nsTHashMap.h"
+
+namespace mozilla::dom {
+
+// Maximum number of successive dialogs before we prompt users to disable
+// dialogs for this window.
+#define MAX_SUCCESSIVE_DIALOG_COUNT 5
+
+static StaticRefPtr<BrowsingContextGroup> sChromeGroup;
+
+static StaticAutoPtr<nsTHashMap<uint64_t, RefPtr<BrowsingContextGroup>>>
+ sBrowsingContextGroups;
+
+already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::GetOrCreate(
+ uint64_t aId) {
+ if (!sBrowsingContextGroups) {
+ sBrowsingContextGroups =
+ new nsTHashMap<nsUint64HashKey, RefPtr<BrowsingContextGroup>>();
+ ClearOnShutdown(&sBrowsingContextGroups);
+ }
+
+ return do_AddRef(sBrowsingContextGroups->LookupOrInsertWith(
+ aId, [&aId] { return do_AddRef(new BrowsingContextGroup(aId)); }));
+}
+
+already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::GetExisting(
+ uint64_t aId) {
+ if (sBrowsingContextGroups) {
+ return do_AddRef(sBrowsingContextGroups->Get(aId));
+ }
+ return nullptr;
+}
+
+// Only use 53 bits for the BrowsingContextGroup ID.
+static constexpr uint64_t kBrowsingContextGroupIdTotalBits = 53;
+static constexpr uint64_t kBrowsingContextGroupIdProcessBits = 22;
+static constexpr uint64_t kBrowsingContextGroupIdFlagBits = 1;
+static constexpr uint64_t kBrowsingContextGroupIdBits =
+ kBrowsingContextGroupIdTotalBits - kBrowsingContextGroupIdProcessBits -
+ kBrowsingContextGroupIdFlagBits;
+
+// IDs for the relevant flags
+static constexpr uint64_t kPotentiallyCrossOriginIsolatedFlag = 0x1;
+
+// The next ID value which will be used.
+static uint64_t sNextBrowsingContextGroupId = 1;
+
+// Generate the next ID with the given flags.
+static uint64_t GenerateBrowsingContextGroupId(uint64_t aFlags) {
+ MOZ_RELEASE_ASSERT(aFlags < (uint64_t(1) << kBrowsingContextGroupIdFlagBits));
+ uint64_t childId = XRE_IsContentProcess()
+ ? ContentChild::GetSingleton()->GetID()
+ : uint64_t(0);
+ MOZ_RELEASE_ASSERT(childId <
+ (uint64_t(1) << kBrowsingContextGroupIdProcessBits));
+ uint64_t id = sNextBrowsingContextGroupId++;
+ MOZ_RELEASE_ASSERT(id < (uint64_t(1) << kBrowsingContextGroupIdBits));
+
+ return (childId << (kBrowsingContextGroupIdBits +
+ kBrowsingContextGroupIdFlagBits)) |
+ (id << kBrowsingContextGroupIdFlagBits) | aFlags;
+}
+
+// Extract flags from the given ID.
+static uint64_t GetBrowsingContextGroupIdFlags(uint64_t aId) {
+ return aId & ((uint64_t(1) << kBrowsingContextGroupIdFlagBits) - 1);
+}
+
+uint64_t BrowsingContextGroup::CreateId(bool aPotentiallyCrossOriginIsolated) {
+ // We encode the potentially cross-origin isolated bit within the ID so that
+ // the information can be recovered whenever the group needs to be re-created
+ // due to e.g. being garbage-collected.
+ //
+ // In the future if we end up needing more complex information stored within
+ // the ID, we can consider converting it to a more complex type, like a
+ // string.
+ uint64_t flags =
+ aPotentiallyCrossOriginIsolated ? kPotentiallyCrossOriginIsolatedFlag : 0;
+ uint64_t id = GenerateBrowsingContextGroupId(flags);
+ MOZ_ASSERT(GetBrowsingContextGroupIdFlags(id) == flags);
+ return id;
+}
+
+already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::Create(
+ bool aPotentiallyCrossOriginIsolated) {
+ return GetOrCreate(CreateId(aPotentiallyCrossOriginIsolated));
+}
+
+BrowsingContextGroup::BrowsingContextGroup(uint64_t aId) : mId(aId) {
+ mTimerEventQueue = ThrottledEventQueue::Create(
+ GetMainThreadSerialEventTarget(), "BrowsingContextGroup timer queue");
+
+ mWorkerEventQueue = ThrottledEventQueue::Create(
+ GetMainThreadSerialEventTarget(), "BrowsingContextGroup worker queue");
+}
+
+void BrowsingContextGroup::Register(nsISupports* aContext) {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(aContext);
+ mContexts.Insert(aContext);
+}
+
+void BrowsingContextGroup::Unregister(nsISupports* aContext) {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(aContext);
+ mContexts.Remove(aContext);
+
+ MaybeDestroy();
+}
+
+void BrowsingContextGroup::EnsureHostProcess(ContentParent* aProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(this != sChromeGroup,
+ "cannot have content host for chrome group");
+ MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE,
+ "cannot use preallocated process as host");
+ MOZ_DIAGNOSTIC_ASSERT(!aProcess->GetRemoteType().IsEmpty(),
+ "host process must have remote type");
+
+ // XXX: The diagnostic crashes in bug 1816025 seemed to come through caller
+ // ContentParent::GetNewOrUsedLaunchingBrowserProcess where we already
+ // did AssertAlive, so IsDead should be irrelevant here. Still it reads
+ // wrong that we ever might do AddBrowsingContextGroup if aProcess->IsDead().
+ if (aProcess->IsDead() ||
+ mHosts.WithEntryHandle(aProcess->GetRemoteType(), [&](auto&& entry) {
+ if (entry) {
+ // We know from bug 1816025 that this happens quite often and we have
+ // bug 1815480 on file that should harden the entire flow. But in the
+ // meantime we can just live with NOT replacing the found host
+ // process with a new one here if it is still alive.
+ MOZ_ASSERT(
+ entry.Data() == aProcess,
+ "There's already another host process for this remote type");
+ if (!entry.Data()->IsShuttingDown()) {
+ return false;
+ }
+ }
+
+ // This process wasn't already marked as our host, so insert it (or
+ // update if the old process is shutting down), and begin subscribing,
+ // unless the process is still launching.
+ entry.InsertOrUpdate(do_AddRef(aProcess));
+
+ return true;
+ })) {
+ aProcess->AddBrowsingContextGroup(this);
+ }
+}
+
+void BrowsingContextGroup::RemoveHostProcess(ContentParent* aProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(aProcess);
+ MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE);
+ auto entry = mHosts.Lookup(aProcess->GetRemoteType());
+ if (entry && entry.Data() == aProcess) {
+ entry.Remove();
+ }
+}
+
+static void CollectContextInitializers(
+ Span<RefPtr<BrowsingContext>> aContexts,
+ nsTArray<SyncedContextInitializer>& aInits) {
+ // The order that we record these initializers is important, as it will keep
+ // the order that children are attached to their parent in the newly connected
+ // content process consistent.
+ for (auto& context : aContexts) {
+ aInits.AppendElement(context->GetIPCInitializer());
+ for (const auto& window : context->GetWindowContexts()) {
+ aInits.AppendElement(window->GetIPCInitializer());
+ CollectContextInitializers(window->Children(), aInits);
+ }
+ }
+}
+
+void BrowsingContextGroup::Subscribe(ContentParent* aProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(aProcess && !aProcess->IsLaunching());
+ MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE);
+
+ // Check if we're already subscribed to this process.
+ if (!mSubscribers.EnsureInserted(aProcess)) {
+ return;
+ }
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ // If the process is already marked as dead, we won't be the host, but may
+ // still need to subscribe to the process due to creating a popup while
+ // shutting down.
+ if (!aProcess->IsDead()) {
+ auto hostEntry = mHosts.Lookup(aProcess->GetRemoteType());
+ MOZ_DIAGNOSTIC_ASSERT(hostEntry && hostEntry.Data() == aProcess,
+ "Cannot subscribe a non-host process");
+ }
+#endif
+
+ // FIXME: This won't send non-discarded children of discarded BCs, but those
+ // BCs will be in the process of being destroyed anyway.
+ // FIXME: Prevent that situation from occuring.
+ nsTArray<SyncedContextInitializer> inits(mContexts.Count());
+ CollectContextInitializers(mToplevels, inits);
+
+ // Send all of our contexts to the target content process.
+ Unused << aProcess->SendRegisterBrowsingContextGroup(Id(), inits);
+
+ // If the focused or active BrowsingContexts belong in this group, tell the
+ // newly subscribed process.
+ if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) {
+ BrowsingContext* focused = fm->GetFocusedBrowsingContextInChrome();
+ if (focused && focused->Group() != this) {
+ focused = nullptr;
+ }
+ BrowsingContext* active = fm->GetActiveBrowsingContextInChrome();
+ if (active && active->Group() != this) {
+ active = nullptr;
+ }
+
+ if (focused || active) {
+ Unused << aProcess->SendSetupFocusedAndActive(
+ focused, fm->GetActionIdForFocusedBrowsingContextInChrome(), active,
+ fm->GetActionIdForActiveBrowsingContextInChrome());
+ }
+ }
+}
+
+void BrowsingContextGroup::Unsubscribe(ContentParent* aProcess) {
+ MOZ_DIAGNOSTIC_ASSERT(aProcess);
+ MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE);
+ mSubscribers.Remove(aProcess);
+ aProcess->RemoveBrowsingContextGroup(this);
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ auto hostEntry = mHosts.Lookup(aProcess->GetRemoteType());
+ MOZ_DIAGNOSTIC_ASSERT(!hostEntry || hostEntry.Data() != aProcess,
+ "Unsubscribing existing host entry");
+#endif
+}
+
+ContentParent* BrowsingContextGroup::GetHostProcess(
+ const nsACString& aRemoteType) {
+ return mHosts.GetWeak(aRemoteType);
+}
+
+void BrowsingContextGroup::UpdateToplevelsSuspendedIfNeeded() {
+ if (!StaticPrefs::dom_suspend_inactive_enabled()) {
+ return;
+ }
+
+ mToplevelsSuspended = ShouldSuspendAllTopLevelContexts();
+ for (const auto& context : mToplevels) {
+ nsPIDOMWindowOuter* outer = context->GetDOMWindow();
+ if (!outer) {
+ continue;
+ }
+ nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow();
+ if (!inner) {
+ continue;
+ }
+ if (mToplevelsSuspended && !inner->GetWasSuspendedByGroup()) {
+ inner->Suspend();
+ inner->SetWasSuspendedByGroup(true);
+ } else if (!mToplevelsSuspended && inner->GetWasSuspendedByGroup()) {
+ inner->Resume();
+ inner->SetWasSuspendedByGroup(false);
+ }
+ }
+}
+
+bool BrowsingContextGroup::ShouldSuspendAllTopLevelContexts() const {
+ for (const auto& context : mToplevels) {
+ if (!context->InactiveForSuspend()) {
+ return false;
+ }
+ }
+ return true;
+}
+
+BrowsingContextGroup::~BrowsingContextGroup() { Destroy(); }
+
+void BrowsingContextGroup::Destroy() {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (mDestroyed) {
+ MOZ_DIAGNOSTIC_ASSERT(mHosts.Count() == 0);
+ MOZ_DIAGNOSTIC_ASSERT(mSubscribers.Count() == 0);
+ MOZ_DIAGNOSTIC_ASSERT_IF(sBrowsingContextGroups,
+ !sBrowsingContextGroups->Contains(Id()) ||
+ *sBrowsingContextGroups->Lookup(Id()) != this);
+ }
+ mDestroyed = true;
+#endif
+
+ // Make sure to call `RemoveBrowsingContextGroup` for every entry in both
+ // `mHosts` and `mSubscribers`. This will visit most entries twice, but
+ // `RemoveBrowsingContextGroup` is safe to call multiple times.
+ for (const auto& entry : mHosts.Values()) {
+ entry->RemoveBrowsingContextGroup(this);
+ }
+ for (const auto& key : mSubscribers) {
+ key->RemoveBrowsingContextGroup(this);
+ }
+ mHosts.Clear();
+ mSubscribers.Clear();
+
+ if (sBrowsingContextGroups) {
+ sBrowsingContextGroups->Remove(Id());
+ }
+}
+
+void BrowsingContextGroup::AddKeepAlive() {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ mKeepAliveCount++;
+}
+
+void BrowsingContextGroup::RemoveKeepAlive() {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(mKeepAliveCount > 0);
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ mKeepAliveCount--;
+
+ MaybeDestroy();
+}
+
+auto BrowsingContextGroup::MakeKeepAlivePtr() -> KeepAlivePtr {
+ AddKeepAlive();
+ return KeepAlivePtr{do_AddRef(this).take()};
+}
+
+void BrowsingContextGroup::MaybeDestroy() {
+ // Once there are no synced contexts referencing a `BrowsingContextGroup`, we
+ // can clear subscribers and destroy this group. We only do this in the parent
+ // process, as it will orchestrate destruction of BCGs in content processes.
+ if (XRE_IsParentProcess() && mContexts.IsEmpty() && mKeepAliveCount == 0 &&
+ this != sChromeGroup) {
+ Destroy();
+
+ // We may have been deleted here, as `Destroy()` will clear references. Do
+ // not access any members at this point.
+ }
+}
+
+void BrowsingContextGroup::ChildDestroy() {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess());
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyed);
+ MOZ_DIAGNOSTIC_ASSERT(mContexts.IsEmpty());
+ Destroy();
+}
+
+nsISupports* BrowsingContextGroup::GetParentObject() const {
+ return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+}
+
+JSObject* BrowsingContextGroup::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return BrowsingContextGroup_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsresult BrowsingContextGroup::QueuePostMessageEvent(nsIRunnable* aRunnable) {
+ MOZ_ASSERT(StaticPrefs::dom_separate_event_queue_for_post_message_enabled());
+
+ if (!mPostMessageEventQueue) {
+ nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget();
+ mPostMessageEventQueue = ThrottledEventQueue::Create(
+ target, "PostMessage Queue",
+ nsIRunnablePriority::PRIORITY_DEFERRED_TIMERS);
+ nsresult rv = mPostMessageEventQueue->SetIsPaused(false);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ }
+
+ // Ensure the queue is enabled
+ if (mPostMessageEventQueue->IsPaused()) {
+ nsresult rv = mPostMessageEventQueue->SetIsPaused(false);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ }
+
+ return mPostMessageEventQueue->Dispatch(aRunnable, NS_DISPATCH_NORMAL);
+}
+
+void BrowsingContextGroup::FlushPostMessageEvents() {
+ if (!mPostMessageEventQueue) {
+ return;
+ }
+ nsresult rv = mPostMessageEventQueue->SetIsPaused(true);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ nsCOMPtr<nsIRunnable> event;
+ while ((event = mPostMessageEventQueue->GetEvent())) {
+ NS_DispatchToMainThread(event.forget());
+ }
+}
+
+bool BrowsingContextGroup::HasActiveBC() {
+ for (auto& topLevelBC : Toplevels()) {
+ if (topLevelBC->IsActive()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void BrowsingContextGroup::IncInputEventSuspensionLevel() {
+ MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled());
+ if (!mHasIncreasedInputTaskManagerSuspensionLevel && HasActiveBC()) {
+ IncInputTaskManagerSuspensionLevel();
+ }
+ ++mInputEventSuspensionLevel;
+}
+
+void BrowsingContextGroup::DecInputEventSuspensionLevel() {
+ MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled());
+ --mInputEventSuspensionLevel;
+ if (!mInputEventSuspensionLevel &&
+ mHasIncreasedInputTaskManagerSuspensionLevel) {
+ DecInputTaskManagerSuspensionLevel();
+ }
+}
+
+void BrowsingContextGroup::DecInputTaskManagerSuspensionLevel() {
+ MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled());
+ MOZ_ASSERT(mHasIncreasedInputTaskManagerSuspensionLevel);
+
+ InputTaskManager::Get()->DecSuspensionLevel();
+ mHasIncreasedInputTaskManagerSuspensionLevel = false;
+}
+
+void BrowsingContextGroup::IncInputTaskManagerSuspensionLevel() {
+ MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled());
+ MOZ_ASSERT(!mHasIncreasedInputTaskManagerSuspensionLevel);
+ MOZ_ASSERT(HasActiveBC());
+
+ InputTaskManager::Get()->IncSuspensionLevel();
+ mHasIncreasedInputTaskManagerSuspensionLevel = true;
+}
+
+void BrowsingContextGroup::UpdateInputTaskManagerIfNeeded(bool aIsActive) {
+ MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled());
+ if (!aIsActive) {
+ if (mHasIncreasedInputTaskManagerSuspensionLevel) {
+ MOZ_ASSERT(mInputEventSuspensionLevel > 0);
+ if (!HasActiveBC()) {
+ DecInputTaskManagerSuspensionLevel();
+ }
+ }
+ } else {
+ if (mInputEventSuspensionLevel &&
+ !mHasIncreasedInputTaskManagerSuspensionLevel) {
+ IncInputTaskManagerSuspensionLevel();
+ }
+ }
+}
+
+/* static */
+BrowsingContextGroup* BrowsingContextGroup::GetChromeGroup() {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ if (!sChromeGroup && XRE_IsParentProcess()) {
+ sChromeGroup = BrowsingContextGroup::Create();
+ ClearOnShutdown(&sChromeGroup);
+ }
+
+ return sChromeGroup;
+}
+
+void BrowsingContextGroup::GetDocGroups(nsTArray<DocGroup*>& aDocGroups) {
+ MOZ_ASSERT(NS_IsMainThread());
+ AppendToArray(aDocGroups, mDocGroups.Values());
+}
+
+already_AddRefed<DocGroup> BrowsingContextGroup::AddDocument(
+ const nsACString& aKey, Document* aDocument) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<DocGroup>& docGroup = mDocGroups.LookupOrInsertWith(
+ aKey, [&] { return DocGroup::Create(this, aKey); });
+
+ docGroup->AddDocument(aDocument);
+ return do_AddRef(docGroup);
+}
+
+void BrowsingContextGroup::RemoveDocument(Document* aDocument,
+ DocGroup* aDocGroup) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<DocGroup> docGroup = aDocGroup;
+ // Removing the last document in DocGroup might decrement the
+ // DocGroup BrowsingContextGroup's refcount to 0.
+ RefPtr<BrowsingContextGroup> kungFuDeathGrip(this);
+ docGroup->RemoveDocument(aDocument);
+
+ if (docGroup->IsEmpty()) {
+ mDocGroups.Remove(docGroup->GetKey());
+ }
+}
+
+already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::Select(
+ WindowContext* aParent, BrowsingContext* aOpener) {
+ if (aParent) {
+ return do_AddRef(aParent->Group());
+ }
+ if (aOpener) {
+ return do_AddRef(aOpener->Group());
+ }
+ return Create();
+}
+
+void BrowsingContextGroup::GetAllGroups(
+ nsTArray<RefPtr<BrowsingContextGroup>>& aGroups) {
+ aGroups.Clear();
+ if (!sBrowsingContextGroups) {
+ return;
+ }
+
+ aGroups = ToArray(sBrowsingContextGroups->Values());
+}
+
+// For tests only.
+void BrowsingContextGroup::ResetDialogAbuseState() {
+ mDialogAbuseCount = 0;
+ // Reset the timer.
+ mLastDialogQuitTime =
+ TimeStamp::Now() -
+ TimeDuration::FromSeconds(DEFAULT_SUCCESSIVE_DIALOG_TIME_LIMIT);
+}
+
+bool BrowsingContextGroup::DialogsAreBeingAbused() {
+ if (mLastDialogQuitTime.IsNull() || nsContentUtils::IsCallerChrome()) {
+ return false;
+ }
+
+ TimeDuration dialogInterval(TimeStamp::Now() - mLastDialogQuitTime);
+ if (dialogInterval.ToSeconds() <
+ Preferences::GetInt("dom.successive_dialog_time_limit",
+ DEFAULT_SUCCESSIVE_DIALOG_TIME_LIMIT)) {
+ mDialogAbuseCount++;
+
+ return PopupBlocker::GetPopupControlState() > PopupBlocker::openAllowed ||
+ mDialogAbuseCount > MAX_SUCCESSIVE_DIALOG_COUNT;
+ }
+
+ // Reset the abuse counter
+ mDialogAbuseCount = 0;
+
+ return false;
+}
+
+bool BrowsingContextGroup::IsPotentiallyCrossOriginIsolated() {
+ return GetBrowsingContextGroupIdFlags(mId) &
+ kPotentiallyCrossOriginIsolatedFlag;
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(BrowsingContextGroup, mContexts,
+ mToplevels, mHosts, mSubscribers,
+ mTimerEventQueue, mWorkerEventQueue,
+ mDocGroups)
+
+} // namespace mozilla::dom
diff --git a/docshell/base/BrowsingContextGroup.h b/docshell/base/BrowsingContextGroup.h
new file mode 100644
index 0000000000..2f8649da2d
--- /dev/null
+++ b/docshell/base/BrowsingContextGroup.h
@@ -0,0 +1,318 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BrowsingContextGroup_h
+#define mozilla_dom_BrowsingContextGroup_h
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/FunctionRef.h"
+#include "nsRefPtrHashtable.h"
+#include "nsHashKeys.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+#include "nsWrapperCache.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+class ThrottledEventQueue;
+
+namespace dom {
+
+// Amount of time allowed between alert/prompt/confirm before enabling
+// the stop dialog checkbox.
+#define DEFAULT_SUCCESSIVE_DIALOG_TIME_LIMIT 3 // 3 sec
+
+class BrowsingContext;
+class WindowContext;
+class ContentParent;
+class DocGroup;
+
+// A BrowsingContextGroup represents the Unit of Related Browsing Contexts in
+// the standard.
+//
+// A BrowsingContext may not hold references to other BrowsingContext objects
+// which are not in the same BrowsingContextGroup.
+class BrowsingContextGroup final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BrowsingContextGroup)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(BrowsingContextGroup)
+
+ // Interact with the list of synced contexts. This controls the lifecycle of
+ // the BrowsingContextGroup and contexts loaded within them.
+ void Register(nsISupports* aContext);
+ void Unregister(nsISupports* aContext);
+
+ // Control which processes will be used to host documents loaded in this
+ // BrowsingContextGroup. There should only ever be one host process per remote
+ // type.
+ //
+ // A new host process will be subscribed to the BrowsingContextGroup unless it
+ // is still launching, in which case it will subscribe itself when it is done
+ // launching.
+ void EnsureHostProcess(ContentParent* aProcess);
+
+ // A removed host process will no longer be used to host documents loaded in
+ // this BrowsingContextGroup.
+ void RemoveHostProcess(ContentParent* aProcess);
+
+ // Synchronize the current BrowsingContextGroup state down to the given
+ // content process, and continue updating it.
+ //
+ // You rarely need to call this directly, as it's automatically called by
+ // |EnsureHostProcess| as needed.
+ void Subscribe(ContentParent* aProcess);
+
+ // Stop synchronizing the current BrowsingContextGroup state down to a given
+ // content process. The content process must no longer be a host process.
+ void Unsubscribe(ContentParent* aProcess);
+
+ // Look up the process which should be used to host documents with this
+ // RemoteType. This will be a non-dead process associated with this
+ // BrowsingContextGroup, if possible.
+ ContentParent* GetHostProcess(const nsACString& aRemoteType);
+
+ // When a BrowsingContext is being discarded, we may want to keep the
+ // corresponding BrowsingContextGroup alive until the other process
+ // acknowledges that the BrowsingContext has been discarded. A `KeepAlive`
+ // will be added to the `BrowsingContextGroup`, delaying destruction.
+ void AddKeepAlive();
+ void RemoveKeepAlive();
+
+ // A `KeepAlivePtr` will hold both a strong reference to the
+ // `BrowsingContextGroup` and holds a `KeepAlive`. When the pointer is
+ // dropped, it will release both the strong reference and the keepalive.
+ struct KeepAliveDeleter {
+ void operator()(BrowsingContextGroup* aPtr) {
+ if (RefPtr<BrowsingContextGroup> ptr = already_AddRefed(aPtr)) {
+ ptr->RemoveKeepAlive();
+ }
+ }
+ };
+ using KeepAlivePtr = UniquePtr<BrowsingContextGroup, KeepAliveDeleter>;
+ KeepAlivePtr MakeKeepAlivePtr();
+
+ // Call when we want to check if we should suspend or resume all top level
+ // contexts.
+ void UpdateToplevelsSuspendedIfNeeded();
+
+ // Get a reference to the list of toplevel contexts in this
+ // BrowsingContextGroup.
+ nsTArray<RefPtr<BrowsingContext>>& Toplevels() { return mToplevels; }
+ void GetToplevels(nsTArray<RefPtr<BrowsingContext>>& aToplevels) {
+ aToplevels.AppendElements(mToplevels);
+ }
+
+ uint64_t Id() { return mId; }
+
+ nsISupports* GetParentObject() const;
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Get or create a BrowsingContextGroup with the given ID.
+ static already_AddRefed<BrowsingContextGroup> GetOrCreate(uint64_t aId);
+ static already_AddRefed<BrowsingContextGroup> GetExisting(uint64_t aId);
+ static already_AddRefed<BrowsingContextGroup> Create(
+ bool aPotentiallyCrossOriginIsolated = false);
+ static already_AddRefed<BrowsingContextGroup> Select(
+ WindowContext* aParent, BrowsingContext* aOpener);
+
+ // Like `Create` but only generates and reserves a new ID without actually
+ // creating the BrowsingContextGroup object.
+ static uint64_t CreateId(bool aPotentiallyCrossOriginIsolated = false);
+
+ // For each 'ContentParent', except for 'aExcludedParent',
+ // associated with this group call 'aCallback'.
+ template <typename Func>
+ void EachOtherParent(ContentParent* aExcludedParent, Func&& aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ for (const auto& key : mSubscribers) {
+ if (key != aExcludedParent) {
+ aCallback(key);
+ }
+ }
+ }
+
+ // For each 'ContentParent' associated with
+ // this group call 'aCallback'.
+ template <typename Func>
+ void EachParent(Func&& aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ for (const auto& key : mSubscribers) {
+ aCallback(key);
+ }
+ }
+
+ nsresult QueuePostMessageEvent(nsIRunnable* aRunnable);
+
+ void FlushPostMessageEvents();
+
+ // Increase or decrease the suspension level in InputTaskManager
+ void UpdateInputTaskManagerIfNeeded(bool aIsActive);
+
+ static BrowsingContextGroup* GetChromeGroup();
+
+ void GetDocGroups(nsTArray<DocGroup*>& aDocGroups);
+
+ // Called by Document when a Document needs to be added to a DocGroup.
+ already_AddRefed<DocGroup> AddDocument(const nsACString& aKey,
+ Document* aDocument);
+
+ // Called by Document when a Document needs to be removed from a DocGroup.
+ // aDocGroup should be from aDocument. This is done to avoid the assert
+ // in GetDocGroup() which can crash when called during unlinking.
+ void RemoveDocument(Document* aDocument, DocGroup* aDocGroup);
+
+ mozilla::ThrottledEventQueue* GetTimerEventQueue() const {
+ return mTimerEventQueue;
+ }
+
+ mozilla::ThrottledEventQueue* GetWorkerEventQueue() const {
+ return mWorkerEventQueue;
+ }
+
+ void SetAreDialogsEnabled(bool aAreDialogsEnabled) {
+ mAreDialogsEnabled = aAreDialogsEnabled;
+ }
+
+ bool GetAreDialogsEnabled() { return mAreDialogsEnabled; }
+
+ bool GetDialogAbuseCount() { return mDialogAbuseCount; }
+
+ // For tests only.
+ void ResetDialogAbuseState();
+
+ bool DialogsAreBeingAbused();
+
+ TimeStamp GetLastDialogQuitTime() { return mLastDialogQuitTime; }
+
+ void SetLastDialogQuitTime(TimeStamp aLastDialogQuitTime) {
+ mLastDialogQuitTime = aLastDialogQuitTime;
+ }
+
+ // Whether all toplevel documents loaded in this group are allowed to be
+ // Cross-Origin Isolated.
+ //
+ // This does not reflect the actual value of `crossOriginIsolated`, as that
+ // also requires that the document is loaded within a `webCOOP+COEP` content
+ // process.
+ bool IsPotentiallyCrossOriginIsolated();
+
+ static void GetAllGroups(nsTArray<RefPtr<BrowsingContextGroup>>& aGroups);
+
+ void IncInputEventSuspensionLevel();
+ void DecInputEventSuspensionLevel();
+
+ void ChildDestroy();
+
+ private:
+ friend class CanonicalBrowsingContext;
+
+ explicit BrowsingContextGroup(uint64_t aId);
+ ~BrowsingContextGroup();
+
+ void MaybeDestroy();
+ void Destroy();
+
+ bool ShouldSuspendAllTopLevelContexts() const;
+
+ bool HasActiveBC();
+ void DecInputTaskManagerSuspensionLevel();
+ void IncInputTaskManagerSuspensionLevel();
+
+ uint64_t mId;
+
+ uint32_t mKeepAliveCount = 0;
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ bool mDestroyed = false;
+#endif
+
+ // A BrowsingContextGroup contains a series of {Browsing,Window}Context
+ // objects. They are addressed using a hashtable to avoid linear lookup when
+ // adding or removing elements from the set.
+ //
+ // FIXME: This list is only required over a counter to keep nested
+ // non-discarded contexts within discarded contexts alive. It should be
+ // removed in the future.
+ // FIXME: Consider introducing a better common base than `nsISupports`?
+ nsTHashSet<nsRefPtrHashKey<nsISupports>> mContexts;
+
+ // The set of toplevel browsing contexts in the current BrowsingContextGroup.
+ nsTArray<RefPtr<BrowsingContext>> mToplevels;
+
+ // Whether or not all toplevels in this group should be suspended
+ bool mToplevelsSuspended = false;
+
+ // DocGroups are thread-safe, and not able to be cycle collected,
+ // but we still keep strong pointers. When all Documents are removed
+ // from DocGroup (by the BrowsingContextGroup), the DocGroup is
+ // removed from here.
+ nsRefPtrHashtable<nsCStringHashKey, DocGroup> mDocGroups;
+
+ // The content process which will host documents in this BrowsingContextGroup
+ // which need to be loaded with a given remote type.
+ //
+ // A non-launching host process must also be a subscriber, though a launching
+ // host process may not yet be subscribed, and a subscriber need not be a host
+ // process.
+ nsRefPtrHashtable<nsCStringHashKey, ContentParent> mHosts;
+
+ nsTHashSet<nsRefPtrHashKey<ContentParent>> mSubscribers;
+
+ // A queue to store postMessage events during page load, the queue will be
+ // flushed once the page is loaded
+ RefPtr<mozilla::ThrottledEventQueue> mPostMessageEventQueue;
+
+ RefPtr<mozilla::ThrottledEventQueue> mTimerEventQueue;
+ RefPtr<mozilla::ThrottledEventQueue> mWorkerEventQueue;
+
+ // A counter to keep track of the input event suspension level of this BCG
+ //
+ // We use BrowsingContextGroup to emulate process isolation in Fission, so
+ // documents within the same the same BCG will behave like they share
+ // the same input task queue.
+ uint32_t mInputEventSuspensionLevel = 0;
+ // Whether this BCG has increased the suspension level in InputTaskManager
+ bool mHasIncreasedInputTaskManagerSuspensionLevel = false;
+
+ // This flag keeps track of whether dialogs are
+ // currently enabled for windows of this group.
+ // It's OK to have these local to each process only because even if
+ // frames from two/three different sites (and thus, processes) coordinate a
+ // dialog abuse attack, they would only the double/triple number of dialogs,
+ // as it is still limited per-site.
+ bool mAreDialogsEnabled = true;
+
+ // This counts the number of windows that have been opened in rapid succession
+ // (i.e. within dom.successive_dialog_time_limit of each other). It is reset
+ // to 0 once a dialog is opened after dom.successive_dialog_time_limit seconds
+ // have elapsed without any other dialogs.
+ // See comment for mAreDialogsEnabled as to why it's ok to have this local to
+ // each process.
+ uint32_t mDialogAbuseCount = 0;
+
+ // This holds the time when the last modal dialog was shown. If more than
+ // MAX_DIALOG_LIMIT dialogs are shown within the time span defined by
+ // dom.successive_dialog_time_limit, we show a checkbox or confirmation prompt
+ // to allow disabling of further dialogs from windows in this BC group.
+ TimeStamp mLastDialogQuitTime;
+};
+} // namespace dom
+} // namespace mozilla
+
+inline void ImplCycleCollectionUnlink(
+ mozilla::dom::BrowsingContextGroup::KeepAlivePtr& aField) {
+ aField = nullptr;
+}
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback,
+ mozilla::dom::BrowsingContextGroup::KeepAlivePtr& aField, const char* aName,
+ uint32_t aFlags = 0) {
+ CycleCollectionNoteChild(aCallback, aField.get(), aName, aFlags);
+}
+
+#endif // !defined(mozilla_dom_BrowsingContextGroup_h)
diff --git a/docshell/base/BrowsingContextWebProgress.cpp b/docshell/base/BrowsingContextWebProgress.cpp
new file mode 100644
index 0000000000..7c915bd666
--- /dev/null
+++ b/docshell/base/BrowsingContextWebProgress.cpp
@@ -0,0 +1,443 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BrowsingContextWebProgress.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/BounceTrackingState.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Logging.h"
+#include "nsCOMPtr.h"
+#include "nsIWebProgressListener.h"
+#include "nsString.h"
+#include "nsPrintfCString.h"
+#include "nsIChannel.h"
+#include "xptinfo.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+static mozilla::LazyLogModule gBCWebProgressLog("BCWebProgress");
+
+static nsCString DescribeBrowsingContext(CanonicalBrowsingContext* aContext);
+static nsCString DescribeWebProgress(nsIWebProgress* aWebProgress);
+static nsCString DescribeRequest(nsIRequest* aRequest);
+static nsCString DescribeWebProgressFlags(uint32_t aFlags,
+ const nsACString& aPrefix);
+static nsCString DescribeError(nsresult aError);
+
+NS_IMPL_CYCLE_COLLECTION(BrowsingContextWebProgress, mCurrentBrowsingContext)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(BrowsingContextWebProgress)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(BrowsingContextWebProgress)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BrowsingContextWebProgress)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebProgress)
+ NS_INTERFACE_MAP_ENTRY(nsIWebProgress)
+ NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener)
+NS_INTERFACE_MAP_END
+
+BrowsingContextWebProgress::BrowsingContextWebProgress(
+ CanonicalBrowsingContext* aBrowsingContext)
+ : mCurrentBrowsingContext(aBrowsingContext) {}
+
+BrowsingContextWebProgress::~BrowsingContextWebProgress() = default;
+
+NS_IMETHODIMP BrowsingContextWebProgress::AddProgressListener(
+ nsIWebProgressListener* aListener, uint32_t aNotifyMask) {
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ if (!listener) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mListenerInfoList.Contains(listener)) {
+ // The listener is already registered!
+ return NS_ERROR_FAILURE;
+ }
+
+ mListenerInfoList.AppendElement(ListenerInfo(listener, aNotifyMask));
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::RemoveProgressListener(
+ nsIWebProgressListener* aListener) {
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ if (!listener) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return mListenerInfoList.RemoveElement(listener) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::GetBrowsingContextXPCOM(
+ BrowsingContext** aBrowsingContext) {
+ NS_IF_ADDREF(*aBrowsingContext = mCurrentBrowsingContext);
+ return NS_OK;
+}
+
+BrowsingContext* BrowsingContextWebProgress::GetBrowsingContext() {
+ return mCurrentBrowsingContext;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::GetDOMWindow(
+ mozIDOMWindowProxy** aDOMWindow) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::GetIsTopLevel(bool* aIsTopLevel) {
+ *aIsTopLevel = mCurrentBrowsingContext->IsTop();
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::GetIsLoadingDocument(
+ bool* aIsLoadingDocument) {
+ *aIsLoadingDocument = mIsLoadingDocument;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::GetLoadType(uint32_t* aLoadType) {
+ *aLoadType = mLoadType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::GetTarget(nsIEventTarget** aTarget) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP BrowsingContextWebProgress::SetTarget(nsIEventTarget* aTarget) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void BrowsingContextWebProgress::UpdateAndNotifyListeners(
+ uint32_t aFlag,
+ const std::function<void(nsIWebProgressListener*)>& aCallback) {
+ RefPtr<BrowsingContextWebProgress> kungFuDeathGrip = this;
+
+ ListenerArray::ForwardIterator iter(mListenerInfoList);
+ while (iter.HasMore()) {
+ ListenerInfo& info = iter.GetNext();
+ if (!(info.mNotifyMask & aFlag)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIWebProgressListener> listener =
+ do_QueryReferent(info.mWeakListener);
+ if (!listener) {
+ mListenerInfoList.RemoveElement(info);
+ continue;
+ }
+
+ aCallback(listener);
+ }
+
+ mListenerInfoList.Compact();
+
+ // Notify the parent BrowsingContextWebProgress of the event to continue
+ // propagating.
+ auto* parent = mCurrentBrowsingContext->GetParent();
+ if (parent && parent->GetWebProgress()) {
+ aCallback(parent->GetWebProgress());
+ }
+}
+
+void BrowsingContextWebProgress::ContextDiscarded() {
+ if (!mIsLoadingDocument) {
+ return;
+ }
+
+ // If our BrowsingContext is being discarded while still loading a document,
+ // fire a synthetic `STATE_STOP` to end the ongoing load.
+ MOZ_LOG(gBCWebProgressLog, LogLevel::Info,
+ ("Discarded while loading %s",
+ DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+
+ // This matches what nsDocLoader::doStopDocumentLoad does, except we don't
+ // bother notifying for `STATE_STOP | STATE_IS_DOCUMENT`,
+ // nsBrowserStatusFilter would filter it out before it gets to the parent
+ // process.
+ nsCOMPtr<nsIRequest> request = mLoadingDocumentRequest;
+ OnStateChange(this, request, STATE_STOP | STATE_IS_WINDOW | STATE_IS_NETWORK,
+ NS_ERROR_ABORT);
+}
+
+void BrowsingContextWebProgress::ContextReplaced(
+ CanonicalBrowsingContext* aNewContext) {
+ mCurrentBrowsingContext = aNewContext;
+}
+
+already_AddRefed<BounceTrackingState>
+BrowsingContextWebProgress::GetBounceTrackingState() {
+ if (!mBounceTrackingState) {
+ mBounceTrackingState = BounceTrackingState::GetOrCreate(this);
+ }
+ return do_AddRef(mBounceTrackingState);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIWebProgressListener
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus) {
+ MOZ_LOG(
+ gBCWebProgressLog, LogLevel::Info,
+ ("OnStateChange(%s, %s, %s, %s) on %s",
+ DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(),
+ DescribeWebProgressFlags(aStateFlags, "STATE_"_ns).get(),
+ DescribeError(aStatus).get(),
+ DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+
+ bool targetIsThis = aWebProgress == this;
+
+ // We may receive a request from an in-process nsDocShell which doesn't have
+ // `aWebProgress == this` which we should still consider as targeting
+ // ourselves.
+ if (nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aWebProgress);
+ docShell && docShell->GetBrowsingContext() == mCurrentBrowsingContext) {
+ targetIsThis = true;
+ aWebProgress->GetLoadType(&mLoadType);
+ }
+
+ // Track `mIsLoadingDocument` based on the notifications we've received so far
+ // if the nsIWebProgress being targeted is this one.
+ if (targetIsThis) {
+ constexpr uint32_t startFlags = nsIWebProgressListener::STATE_START |
+ nsIWebProgressListener::STATE_IS_DOCUMENT |
+ nsIWebProgressListener::STATE_IS_REQUEST |
+ nsIWebProgressListener::STATE_IS_WINDOW |
+ nsIWebProgressListener::STATE_IS_NETWORK;
+ constexpr uint32_t stopFlags = nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_WINDOW;
+ constexpr uint32_t redirectFlags =
+ nsIWebProgressListener::STATE_IS_REDIRECTED_DOCUMENT;
+ if ((aStateFlags & startFlags) == startFlags) {
+ if (mIsLoadingDocument) {
+ // We received a duplicate `STATE_START` notification, silence this
+ // notification until we receive the matching `STATE_STOP` to not fire
+ // duplicate `STATE_START` notifications into frontend on process
+ // switches.
+ return NS_OK;
+ }
+ mIsLoadingDocument = true;
+
+ // Record the request we started the load with, so we can emit a synthetic
+ // `STATE_STOP` notification if the BrowsingContext is discarded before
+ // the notification arrives.
+ mLoadingDocumentRequest = aRequest;
+ } else if ((aStateFlags & stopFlags) == stopFlags) {
+ // We've received a `STATE_STOP` notification targeting this web progress,
+ // clear our loading document flag.
+ mIsLoadingDocument = false;
+ mLoadingDocumentRequest = nullptr;
+ } else if (mIsLoadingDocument &&
+ (aStateFlags & redirectFlags) == redirectFlags) {
+ // If we see a redirected document load, update the loading request which
+ // we'll emit the synthetic STATE_STOP notification with.
+ mLoadingDocumentRequest = aRequest;
+ }
+ }
+
+ UpdateAndNotifyListeners(
+ ((aStateFlags >> 16) & nsIWebProgress::NOTIFY_STATE_ALL),
+ [&](nsIWebProgressListener* listener) {
+ listener->OnStateChange(aWebProgress, aRequest, aStateFlags, aStatus);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ MOZ_LOG(
+ gBCWebProgressLog, LogLevel::Info,
+ ("OnProgressChange(%s, %s, %d, %d, %d, %d) on %s",
+ DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(),
+ aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress,
+ DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+ UpdateAndNotifyListeners(
+ nsIWebProgress::NOTIFY_PROGRESS, [&](nsIWebProgressListener* listener) {
+ listener->OnProgressChange(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI* aLocation,
+ uint32_t aFlags) {
+ MOZ_LOG(
+ gBCWebProgressLog, LogLevel::Info,
+ ("OnLocationChange(%s, %s, %s, %s) on %s",
+ DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(),
+ aLocation ? aLocation->GetSpecOrDefault().get() : "<null>",
+ DescribeWebProgressFlags(aFlags, "LOCATION_CHANGE_"_ns).get(),
+ DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+ UpdateAndNotifyListeners(
+ nsIWebProgress::NOTIFY_LOCATION, [&](nsIWebProgressListener* listener) {
+ listener->OnLocationChange(aWebProgress, aRequest, aLocation, aFlags);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsresult aStatus,
+ const char16_t* aMessage) {
+ MOZ_LOG(
+ gBCWebProgressLog, LogLevel::Info,
+ ("OnStatusChange(%s, %s, %s, \"%s\") on %s",
+ DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(),
+ DescribeError(aStatus).get(), NS_ConvertUTF16toUTF8(aMessage).get(),
+ DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+ UpdateAndNotifyListeners(
+ nsIWebProgress::NOTIFY_STATUS, [&](nsIWebProgressListener* listener) {
+ listener->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aState) {
+ MOZ_LOG(
+ gBCWebProgressLog, LogLevel::Info,
+ ("OnSecurityChange(%s, %s, %x) on %s",
+ DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(),
+ aState, DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+ UpdateAndNotifyListeners(
+ nsIWebProgress::NOTIFY_SECURITY, [&](nsIWebProgressListener* listener) {
+ listener->OnSecurityChange(aWebProgress, aRequest, aState);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ MOZ_LOG(
+ gBCWebProgressLog, LogLevel::Info,
+ ("OnContentBlockingEvent(%s, %s, %x) on %s",
+ DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(),
+ aEvent, DescribeBrowsingContext(mCurrentBrowsingContext).get()));
+ UpdateAndNotifyListeners(nsIWebProgress::NOTIFY_CONTENT_BLOCKING,
+ [&](nsIWebProgressListener* listener) {
+ listener->OnContentBlockingEvent(aWebProgress,
+ aRequest, aEvent);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BrowsingContextWebProgress::GetDocumentRequest(nsIRequest** aRequest) {
+ NS_IF_ADDREF(*aRequest = mLoadingDocumentRequest);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Helper methods for notification logging
+
+static nsCString DescribeBrowsingContext(CanonicalBrowsingContext* aContext) {
+ if (!aContext) {
+ return "<null>"_ns;
+ }
+
+ nsCOMPtr<nsIURI> currentURI = aContext->GetCurrentURI();
+ return nsPrintfCString(
+ "{top:%d, id:%" PRIx64 ", url:%s}", aContext->IsTop(), aContext->Id(),
+ currentURI ? currentURI->GetSpecOrDefault().get() : "<null>");
+}
+
+static nsCString DescribeWebProgress(nsIWebProgress* aWebProgress) {
+ if (!aWebProgress) {
+ return "<null>"_ns;
+ }
+
+ bool isTopLevel = false;
+ aWebProgress->GetIsTopLevel(&isTopLevel);
+ bool isLoadingDocument = false;
+ aWebProgress->GetIsLoadingDocument(&isLoadingDocument);
+ return nsPrintfCString("{isTopLevel:%d, isLoadingDocument:%d}", isTopLevel,
+ isLoadingDocument);
+}
+
+static nsCString DescribeRequest(nsIRequest* aRequest) {
+ if (!aRequest) {
+ return "<null>"_ns;
+ }
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (!channel) {
+ return "<non-channel>"_ns;
+ }
+
+ nsCOMPtr<nsIURI> originalURI;
+ channel->GetOriginalURI(getter_AddRefs(originalURI));
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+
+ return nsPrintfCString(
+ "{URI:%s, originalURI:%s}",
+ uri ? uri->GetSpecOrDefault().get() : "<null>",
+ originalURI ? originalURI->GetSpecOrDefault().get() : "<null>");
+}
+
+static nsCString DescribeWebProgressFlags(uint32_t aFlags,
+ const nsACString& aPrefix) {
+ nsCString flags;
+ uint32_t remaining = aFlags;
+
+ // Hackily fetch the names of each constant from the XPT information used for
+ // reflecting it into JS. This doesn't need to be reliable and just exists as
+ // a logging aid.
+ //
+ // If a change to xpt in the future breaks this code, just delete it and
+ // replace it with a normal hex log.
+ if (const auto* ifaceInfo =
+ nsXPTInterfaceInfo::ByIID(NS_GET_IID(nsIWebProgressListener))) {
+ for (uint16_t i = 0; i < ifaceInfo->ConstantCount(); ++i) {
+ const auto& constInfo = ifaceInfo->Constant(i);
+ nsDependentCString name(constInfo.Name());
+ if (!StringBeginsWith(name, aPrefix)) {
+ continue;
+ }
+
+ if (remaining & constInfo.mValue) {
+ remaining &= ~constInfo.mValue;
+ if (!flags.IsEmpty()) {
+ flags.AppendLiteral("|");
+ }
+ flags.Append(name);
+ }
+ }
+ }
+ if (remaining != 0 || flags.IsEmpty()) {
+ if (!flags.IsEmpty()) {
+ flags.AppendLiteral("|");
+ }
+ flags.AppendInt(remaining, 16);
+ }
+ return flags;
+}
+
+static nsCString DescribeError(nsresult aError) {
+ nsCString name;
+ GetErrorName(aError, name);
+ return name;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/docshell/base/BrowsingContextWebProgress.h b/docshell/base/BrowsingContextWebProgress.h
new file mode 100644
index 0000000000..81610886b9
--- /dev/null
+++ b/docshell/base/BrowsingContextWebProgress.h
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BrowsingContextWebProgress_h
+#define mozilla_dom_BrowsingContextWebProgress_h
+
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.h"
+#include "nsTObserverArray.h"
+#include "nsWeakReference.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/BounceTrackingState.h"
+
+namespace mozilla::dom {
+
+class CanonicalBrowsingContext;
+
+/// Object acting as the nsIWebProgress instance for a BrowsingContext over its
+/// lifetime.
+///
+/// An active toplevel CanonicalBrowsingContext will always have a
+/// BrowsingContextWebProgress, which will be moved between contexts as
+/// BrowsingContextGroup-changing loads are performed.
+///
+/// Subframes will only have a `BrowsingContextWebProgress` if they are loaded
+/// in a content process, and will use the nsDocShell instead if they are loaded
+/// in the parent process, as parent process documents cannot have or be
+/// out-of-process iframes.
+class BrowsingContextWebProgress final : public nsIWebProgress,
+ public nsIWebProgressListener {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(BrowsingContextWebProgress,
+ nsIWebProgress)
+ NS_DECL_NSIWEBPROGRESS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ explicit BrowsingContextWebProgress(
+ CanonicalBrowsingContext* aBrowsingContext);
+
+ struct ListenerInfo {
+ ListenerInfo(nsIWeakReference* aListener, unsigned long aNotifyMask)
+ : mWeakListener(aListener), mNotifyMask(aNotifyMask) {}
+
+ bool operator==(const ListenerInfo& aOther) const {
+ return mWeakListener == aOther.mWeakListener;
+ }
+ bool operator==(const nsWeakPtr& aOther) const {
+ return mWeakListener == aOther;
+ }
+
+ // Weak pointer for the nsIWebProgressListener...
+ nsWeakPtr mWeakListener;
+
+ // Mask indicating which notifications the listener wants to receive.
+ unsigned long mNotifyMask;
+ };
+
+ void ContextDiscarded();
+ void ContextReplaced(CanonicalBrowsingContext* aNewContext);
+
+ void SetLoadType(uint32_t aLoadType) { mLoadType = aLoadType; }
+
+ already_AddRefed<BounceTrackingState> GetBounceTrackingState();
+
+ private:
+ virtual ~BrowsingContextWebProgress();
+
+ void UpdateAndNotifyListeners(
+ uint32_t aFlag,
+ const std::function<void(nsIWebProgressListener*)>& aCallback);
+
+ using ListenerArray = nsAutoTObserverArray<ListenerInfo, 4>;
+ ListenerArray mListenerInfoList;
+
+ // The current BrowsingContext which owns this BrowsingContextWebProgress.
+ // This context may change during navigations and may not be fully attached at
+ // all times.
+ RefPtr<CanonicalBrowsingContext> mCurrentBrowsingContext;
+
+ // The current request being actively loaded by the BrowsingContext. Only set
+ // while mIsLoadingDocument is true, and is used to fire STATE_STOP
+ // notifications if the BrowsingContext is discarded while the load is
+ // ongoing.
+ nsCOMPtr<nsIRequest> mLoadingDocumentRequest;
+
+ // The most recent load type observed for this BrowsingContextWebProgress.
+ uint32_t mLoadType = 0;
+
+ // Are we currently in the process of loading a document? This is true between
+ // the `STATE_START` notification from content and the `STATE_STOP`
+ // notification being received. Duplicate `STATE_START` events may be
+ // discarded while loading a document to avoid noise caused by process
+ // switches.
+ bool mIsLoadingDocument = false;
+
+ RefPtr<mozilla::BounceTrackingState> mBounceTrackingState;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_BrowsingContextWebProgress_h
diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp
new file mode 100644
index 0000000000..33ed5aab28
--- /dev/null
+++ b/docshell/base/CanonicalBrowsingContext.cpp
@@ -0,0 +1,3104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+
+#include "ErrorList.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/BrowsingContextBinding.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/PBrowserParent.h"
+#include "mozilla/dom/PBackgroundSessionStorageCache.h"
+#include "mozilla/dom/PWindowGlobalParent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Promise-inl.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/dom/MediaController.h"
+#include "mozilla/dom/MediaControlService.h"
+#include "mozilla/dom/ContentPlaybackController.h"
+#include "mozilla/dom/SessionStorageManager.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#ifdef NS_PRINTING
+# include "mozilla/layout/RemotePrintJobParent.h"
+#endif
+#include "mozilla/net/DocumentLoadListener.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_docshell.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/Telemetry.h"
+#include "nsILayoutHistoryState.h"
+#include "nsIPrintSettings.h"
+#include "nsIPrintSettingsService.h"
+#include "nsISupports.h"
+#include "nsIWebNavigation.h"
+#include "nsDocShell.h"
+#include "nsFrameLoader.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsIXULRuntime.h"
+#include "nsNetUtil.h"
+#include "nsSHistory.h"
+#include "nsSecureBrowserUI.h"
+#include "nsQueryObject.h"
+#include "nsBrowserStatusFilter.h"
+#include "nsIBrowser.h"
+#include "nsTHashSet.h"
+#include "SessionStoreFunctions.h"
+#include "nsIXPConnect.h"
+#include "nsImportModule.h"
+#include "UnitTransforms.h"
+
+using namespace mozilla::ipc;
+
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
+extern mozilla::LazyLogModule gSHLog;
+extern mozilla::LazyLogModule gSHIPBFCacheLog;
+
+#define AUTOPLAY_LOG(msg, ...) \
+ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+static mozilla::LazyLogModule sPBContext("PBContext");
+
+// Global count of canonical browsing contexts with the private attribute set
+static uint32_t gNumberOfPrivateContexts = 0;
+
+// Current parent process epoch for parent initiated navigations
+static uint64_t gParentInitiatedNavigationEpoch = 0;
+
+static void IncreasePrivateCount() {
+ gNumberOfPrivateContexts++;
+ MOZ_LOG(sPBContext, mozilla::LogLevel::Debug,
+ ("%s: Private browsing context count %d -> %d", __func__,
+ gNumberOfPrivateContexts - 1, gNumberOfPrivateContexts));
+ if (gNumberOfPrivateContexts > 1) {
+ return;
+ }
+
+ static bool sHasSeenPrivateContext = false;
+ if (!sHasSeenPrivateContext) {
+ sHasSeenPrivateContext = true;
+ mozilla::Telemetry::ScalarSet(
+ mozilla::Telemetry::ScalarID::DOM_PARENTPROCESS_PRIVATE_WINDOW_USED,
+ true);
+ }
+}
+
+static void DecreasePrivateCount() {
+ MOZ_ASSERT(gNumberOfPrivateContexts > 0);
+ gNumberOfPrivateContexts--;
+
+ MOZ_LOG(sPBContext, mozilla::LogLevel::Debug,
+ ("%s: Private browsing context count %d -> %d", __func__,
+ gNumberOfPrivateContexts + 1, gNumberOfPrivateContexts));
+ if (!gNumberOfPrivateContexts &&
+ !mozilla::StaticPrefs::browser_privatebrowsing_autostart()) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ MOZ_LOG(sPBContext, mozilla::LogLevel::Debug,
+ ("%s: last-pb-context-exited fired", __func__));
+ observerService->NotifyObservers(nullptr, "last-pb-context-exited",
+ nullptr);
+ }
+ }
+}
+
+namespace mozilla::dom {
+
+extern mozilla::LazyLogModule gUserInteractionPRLog;
+
+#define USER_ACTIVATION_LOG(msg, ...) \
+ MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+CanonicalBrowsingContext::CanonicalBrowsingContext(WindowContext* aParentWindow,
+ BrowsingContextGroup* aGroup,
+ uint64_t aBrowsingContextId,
+ uint64_t aOwnerProcessId,
+ uint64_t aEmbedderProcessId,
+ BrowsingContext::Type aType,
+ FieldValues&& aInit)
+ : BrowsingContext(aParentWindow, aGroup, aBrowsingContextId, aType,
+ std::move(aInit)),
+ mProcessId(aOwnerProcessId),
+ mEmbedderProcessId(aEmbedderProcessId),
+ mPermanentKey(JS::NullValue()) {
+ // You are only ever allowed to create CanonicalBrowsingContexts in the
+ // parent process.
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+
+ // The initial URI in a BrowsingContext is always "about:blank".
+ MOZ_ALWAYS_SUCCEEDS(
+ NS_NewURI(getter_AddRefs(mCurrentRemoteURI), "about:blank"));
+
+ mozilla::HoldJSObjects(this);
+}
+
+CanonicalBrowsingContext::~CanonicalBrowsingContext() {
+ mPermanentKey.setNull();
+
+ mozilla::DropJSObjects(this);
+
+ if (mSessionHistory) {
+ mSessionHistory->SetBrowsingContext(nullptr);
+ }
+}
+
+/* static */
+already_AddRefed<CanonicalBrowsingContext> CanonicalBrowsingContext::Get(
+ uint64_t aId) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ return BrowsingContext::Get(aId).downcast<CanonicalBrowsingContext>();
+}
+
+/* static */
+CanonicalBrowsingContext* CanonicalBrowsingContext::Cast(
+ BrowsingContext* aContext) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ return static_cast<CanonicalBrowsingContext*>(aContext);
+}
+
+/* static */
+const CanonicalBrowsingContext* CanonicalBrowsingContext::Cast(
+ const BrowsingContext* aContext) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ return static_cast<const CanonicalBrowsingContext*>(aContext);
+}
+
+already_AddRefed<CanonicalBrowsingContext> CanonicalBrowsingContext::Cast(
+ already_AddRefed<BrowsingContext>&& aContext) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ return aContext.downcast<CanonicalBrowsingContext>();
+}
+
+ContentParent* CanonicalBrowsingContext::GetContentParent() const {
+ if (mProcessId == 0) {
+ return nullptr;
+ }
+
+ ContentProcessManager* cpm = ContentProcessManager::GetSingleton();
+ if (!cpm) {
+ return nullptr;
+ }
+ return cpm->GetContentProcessById(ContentParentId(mProcessId));
+}
+
+void CanonicalBrowsingContext::GetCurrentRemoteType(nsACString& aRemoteType,
+ ErrorResult& aRv) const {
+ // If we're in the parent process, dump out the void string.
+ if (mProcessId == 0) {
+ aRemoteType = NOT_REMOTE_TYPE;
+ return;
+ }
+
+ ContentParent* cp = GetContentParent();
+ if (!cp) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ aRemoteType = cp->GetRemoteType();
+}
+
+void CanonicalBrowsingContext::SetOwnerProcessId(uint64_t aProcessId) {
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("SetOwnerProcessId for 0x%08" PRIx64 " (0x%08" PRIx64
+ " -> 0x%08" PRIx64 ")",
+ Id(), mProcessId, aProcessId));
+
+ mProcessId = aProcessId;
+}
+
+nsISecureBrowserUI* CanonicalBrowsingContext::GetSecureBrowserUI() {
+ if (!IsTop()) {
+ return nullptr;
+ }
+ if (!mSecureBrowserUI) {
+ mSecureBrowserUI = new nsSecureBrowserUI(this);
+ }
+ return mSecureBrowserUI;
+}
+
+namespace {
+// The DocShellProgressBridge is attached to a root content docshell loaded in
+// the parent process. Notifications are paired up with the docshell which they
+// came from, so that they can be fired to the correct
+// BrowsingContextWebProgress and bubble through this tree separately.
+//
+// Notifications are filtered by a nsBrowserStatusFilter before being received
+// by the DocShellProgressBridge.
+class DocShellProgressBridge : public nsIWebProgressListener {
+ public:
+ NS_DECL_ISUPPORTS
+ // NOTE: This relies in the expansion of `NS_FORWARD_SAFE` and all listener
+ // methods accepting an `aWebProgress` argument. If this changes in the
+ // future, this may need to be written manually.
+ NS_FORWARD_SAFE_NSIWEBPROGRESSLISTENER(GetTargetContext(aWebProgress))
+
+ explicit DocShellProgressBridge(uint64_t aTopContextId)
+ : mTopContextId(aTopContextId) {}
+
+ private:
+ virtual ~DocShellProgressBridge() = default;
+
+ nsIWebProgressListener* GetTargetContext(nsIWebProgress* aWebProgress) {
+ RefPtr<CanonicalBrowsingContext> context;
+ if (nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aWebProgress)) {
+ context = docShell->GetBrowsingContext()->Canonical();
+ } else {
+ context = CanonicalBrowsingContext::Get(mTopContextId);
+ }
+ return context && !context->IsDiscarded() ? context->GetWebProgress()
+ : nullptr;
+ }
+
+ uint64_t mTopContextId = 0;
+};
+
+NS_IMPL_ISUPPORTS(DocShellProgressBridge, nsIWebProgressListener)
+} // namespace
+
+void CanonicalBrowsingContext::MaybeAddAsProgressListener(
+ nsIWebProgress* aWebProgress) {
+ // Only add as a listener if the created docshell is a toplevel content
+ // docshell. We'll get notifications for all of our subframes through a single
+ // listener.
+ if (!IsTopContent()) {
+ return;
+ }
+
+ if (!mDocShellProgressBridge) {
+ mDocShellProgressBridge = new DocShellProgressBridge(Id());
+ mStatusFilter = new nsBrowserStatusFilter();
+ mStatusFilter->AddProgressListener(mDocShellProgressBridge,
+ nsIWebProgress::NOTIFY_ALL);
+ }
+
+ aWebProgress->AddProgressListener(mStatusFilter, nsIWebProgress::NOTIFY_ALL);
+}
+
+void CanonicalBrowsingContext::ReplacedBy(
+ CanonicalBrowsingContext* aNewContext,
+ const NavigationIsolationOptions& aRemotenessOptions) {
+ MOZ_ASSERT(!aNewContext->mWebProgress);
+ MOZ_ASSERT(!aNewContext->mSessionHistory);
+ MOZ_ASSERT(IsTop() && aNewContext->IsTop());
+
+ mIsReplaced = true;
+ aNewContext->mIsReplaced = false;
+
+ if (mStatusFilter) {
+ mStatusFilter->RemoveProgressListener(mDocShellProgressBridge);
+ mStatusFilter = nullptr;
+ }
+
+ mWebProgress->ContextReplaced(aNewContext);
+ aNewContext->mWebProgress = std::move(mWebProgress);
+
+ // Use the Transaction for the fields which need to be updated whether or not
+ // the new context has been attached before.
+ // SetWithoutSyncing can be used if context hasn't been attached.
+ Transaction txn;
+ txn.SetBrowserId(GetBrowserId());
+ txn.SetIsAppTab(GetIsAppTab());
+ txn.SetHasSiblings(GetHasSiblings());
+ txn.SetHistoryID(GetHistoryID());
+ txn.SetExplicitActive(GetExplicitActive());
+ txn.SetEmbedderColorSchemes(GetEmbedderColorSchemes());
+ txn.SetHasRestoreData(GetHasRestoreData());
+ txn.SetShouldDelayMediaFromStart(GetShouldDelayMediaFromStart());
+ txn.SetForceOffline(GetForceOffline());
+
+ // Propagate some settings on BrowsingContext replacement so they're not lost
+ // on bfcached navigations. These are important for GeckoView (see bug
+ // 1781936).
+ txn.SetAllowJavascript(GetAllowJavascript());
+ txn.SetForceEnableTrackingProtection(GetForceEnableTrackingProtection());
+ txn.SetUserAgentOverride(GetUserAgentOverride());
+ txn.SetSuspendMediaWhenInactive(GetSuspendMediaWhenInactive());
+ txn.SetDisplayMode(GetDisplayMode());
+ txn.SetForceDesktopViewport(GetForceDesktopViewport());
+ txn.SetIsUnderHiddenEmbedderElement(GetIsUnderHiddenEmbedderElement());
+
+ // When using site-specific zoom, we let the front-end manage it, otherwise it
+ // can cause weirdness like bug 1846141.
+ if (!StaticPrefs::browser_zoom_siteSpecific()) {
+ txn.SetFullZoom(GetFullZoom());
+ txn.SetTextZoom(GetTextZoom());
+ }
+
+ // Propagate the default load flags so that the TRR mode flags are forwarded
+ // to the new browsing context. See bug 1828643.
+ txn.SetDefaultLoadFlags(GetDefaultLoadFlags());
+
+ // As this is a different BrowsingContext, set InitialSandboxFlags to the
+ // current flags in the new context so that they also apply to any initial
+ // about:blank documents created in it.
+ txn.SetSandboxFlags(GetSandboxFlags());
+ txn.SetInitialSandboxFlags(GetSandboxFlags());
+ txn.SetTargetTopLevelLinkClicksToBlankInternal(
+ TargetTopLevelLinkClicksToBlank());
+ if (aNewContext->EverAttached()) {
+ MOZ_ALWAYS_SUCCEEDS(txn.Commit(aNewContext));
+ } else {
+ txn.CommitWithoutSyncing(aNewContext);
+ }
+
+ aNewContext->mRestoreState = mRestoreState.forget();
+ MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false));
+
+ // XXXBFCache name handling is still a bit broken in Fission in general,
+ // at least in case name should be cleared.
+ if (aRemotenessOptions.mTryUseBFCache) {
+ MOZ_ASSERT(!aNewContext->EverAttached());
+ aNewContext->mFields.SetWithoutSyncing<IDX_Name>(GetName());
+ // We don't copy over HasLoadedNonInitialDocument here, we'll actually end
+ // up loading a new initial document at this point, before the real load.
+ // The real load will then end up setting HasLoadedNonInitialDocument to
+ // true.
+ }
+
+ if (mSessionHistory) {
+ mSessionHistory->SetBrowsingContext(aNewContext);
+ // At this point we will be creating a new ChildSHistory in the child.
+ // That means that the child's epoch will be reset, so it makes sense to
+ // reset the epoch in the parent too.
+ mSessionHistory->SetEpoch(0, Nothing());
+ mSessionHistory.swap(aNewContext->mSessionHistory);
+ RefPtr<ChildSHistory> childSHistory = ForgetChildSHistory();
+ aNewContext->SetChildSHistory(childSHistory);
+ }
+
+ BackgroundSessionStorageManager::PropagateManager(Id(), aNewContext->Id());
+
+ // Transfer the ownership of the priority active status from the old context
+ // to the new context.
+ aNewContext->mPriorityActive = mPriorityActive;
+ mPriorityActive = false;
+
+ MOZ_ASSERT(aNewContext->mLoadingEntries.IsEmpty());
+ mLoadingEntries.SwapElements(aNewContext->mLoadingEntries);
+ MOZ_ASSERT(!aNewContext->mActiveEntry);
+ mActiveEntry.swap(aNewContext->mActiveEntry);
+
+ aNewContext->mPermanentKey = mPermanentKey;
+ mPermanentKey.setNull();
+}
+
+void CanonicalBrowsingContext::UpdateSecurityState() {
+ if (mSecureBrowserUI) {
+ mSecureBrowserUI->RecomputeSecurityFlags();
+ }
+}
+
+void CanonicalBrowsingContext::GetWindowGlobals(
+ nsTArray<RefPtr<WindowGlobalParent>>& aWindows) {
+ aWindows.SetCapacity(GetWindowContexts().Length());
+ for (auto& window : GetWindowContexts()) {
+ aWindows.AppendElement(static_cast<WindowGlobalParent*>(window.get()));
+ }
+}
+
+WindowGlobalParent* CanonicalBrowsingContext::GetCurrentWindowGlobal() const {
+ return static_cast<WindowGlobalParent*>(GetCurrentWindowContext());
+}
+
+WindowGlobalParent* CanonicalBrowsingContext::GetParentWindowContext() {
+ return static_cast<WindowGlobalParent*>(
+ BrowsingContext::GetParentWindowContext());
+}
+
+WindowGlobalParent* CanonicalBrowsingContext::GetTopWindowContext() {
+ return static_cast<WindowGlobalParent*>(
+ BrowsingContext::GetTopWindowContext());
+}
+
+already_AddRefed<nsIWidget>
+CanonicalBrowsingContext::GetParentProcessWidgetContaining() {
+ // If our document is loaded in-process, such as chrome documents, get the
+ // widget directly from our outer window. Otherwise, try to get the widget
+ // from the toplevel content's browser's element.
+ nsCOMPtr<nsIWidget> widget;
+ if (nsGlobalWindowOuter* window = nsGlobalWindowOuter::Cast(GetDOMWindow())) {
+ widget = window->GetNearestWidget();
+ } else if (Element* topEmbedder = Top()->GetEmbedderElement()) {
+ widget = nsContentUtils::WidgetForContent(topEmbedder);
+ if (!widget) {
+ widget = nsContentUtils::WidgetForDocument(topEmbedder->OwnerDoc());
+ }
+ }
+
+ if (widget) {
+ widget = widget->GetTopLevelWidget();
+ }
+
+ return widget.forget();
+}
+
+already_AddRefed<nsIBrowserDOMWindow>
+CanonicalBrowsingContext::GetBrowserDOMWindow() {
+ RefPtr<CanonicalBrowsingContext> chromeTop = TopCrossChromeBoundary();
+ nsGlobalWindowOuter* topWin;
+ if ((topWin = nsGlobalWindowOuter::Cast(chromeTop->GetDOMWindow())) &&
+ topWin->IsChromeWindow()) {
+ return do_AddRef(topWin->GetBrowserDOMWindow());
+ }
+ return nullptr;
+}
+
+already_AddRefed<WindowGlobalParent>
+CanonicalBrowsingContext::GetEmbedderWindowGlobal() const {
+ uint64_t windowId = GetEmbedderInnerWindowId();
+ if (windowId == 0) {
+ return nullptr;
+ }
+
+ return WindowGlobalParent::GetByInnerWindowId(windowId);
+}
+
+CanonicalBrowsingContext*
+CanonicalBrowsingContext::GetParentCrossChromeBoundary() {
+ if (GetParent()) {
+ return Cast(GetParent());
+ }
+ if (auto* embedder = GetEmbedderElement()) {
+ return Cast(embedder->OwnerDoc()->GetBrowsingContext());
+ }
+ return nullptr;
+}
+
+CanonicalBrowsingContext* CanonicalBrowsingContext::TopCrossChromeBoundary() {
+ CanonicalBrowsingContext* bc = this;
+ while (auto* parent = bc->GetParentCrossChromeBoundary()) {
+ bc = parent;
+ }
+ return bc;
+}
+
+Nullable<WindowProxyHolder> CanonicalBrowsingContext::GetTopChromeWindow() {
+ RefPtr<CanonicalBrowsingContext> bc = TopCrossChromeBoundary();
+ if (bc->IsChrome()) {
+ return WindowProxyHolder(bc.forget());
+ }
+ return nullptr;
+}
+
+nsISHistory* CanonicalBrowsingContext::GetSessionHistory() {
+ if (!IsTop()) {
+ return Cast(Top())->GetSessionHistory();
+ }
+
+ // Check GetChildSessionHistory() to make sure that this BrowsingContext has
+ // session history enabled.
+ if (!mSessionHistory && GetChildSessionHistory()) {
+ mSessionHistory = new nsSHistory(this);
+ }
+
+ return mSessionHistory;
+}
+
+SessionHistoryEntry* CanonicalBrowsingContext::GetActiveSessionHistoryEntry() {
+ return mActiveEntry;
+}
+
+void CanonicalBrowsingContext::SetActiveSessionHistoryEntry(
+ SessionHistoryEntry* aEntry) {
+ mActiveEntry = aEntry;
+}
+
+bool CanonicalBrowsingContext::HasHistoryEntry(nsISHEntry* aEntry) {
+ // XXX Should we check also loading entries?
+ return aEntry && mActiveEntry == aEntry;
+}
+
+void CanonicalBrowsingContext::SwapHistoryEntries(nsISHEntry* aOldEntry,
+ nsISHEntry* aNewEntry) {
+ // XXX Should we check also loading entries?
+ if (mActiveEntry == aOldEntry) {
+ nsCOMPtr<SessionHistoryEntry> newEntry = do_QueryInterface(aNewEntry);
+ mActiveEntry = newEntry.forget();
+ }
+}
+
+void CanonicalBrowsingContext::AddLoadingSessionHistoryEntry(
+ uint64_t aLoadId, SessionHistoryEntry* aEntry) {
+ Unused << SetHistoryID(aEntry->DocshellID());
+ mLoadingEntries.AppendElement(LoadingSessionHistoryEntry{aLoadId, aEntry});
+}
+
+void CanonicalBrowsingContext::GetLoadingSessionHistoryInfoFromParent(
+ Maybe<LoadingSessionHistoryInfo>& aLoadingInfo) {
+ nsISHistory* shistory = GetSessionHistory();
+ if (!shistory || !GetParent()) {
+ return;
+ }
+
+ SessionHistoryEntry* parentSHE =
+ GetParent()->Canonical()->GetActiveSessionHistoryEntry();
+ if (parentSHE) {
+ int32_t index = -1;
+ for (BrowsingContext* sibling : GetParent()->Children()) {
+ ++index;
+ if (sibling == this) {
+ nsCOMPtr<nsISHEntry> shEntry;
+ parentSHE->GetChildSHEntryIfHasNoDynamicallyAddedChild(
+ index, getter_AddRefs(shEntry));
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(shEntry);
+ if (she) {
+ aLoadingInfo.emplace(she);
+ mLoadingEntries.AppendElement(LoadingSessionHistoryEntry{
+ aLoadingInfo.value().mLoadId, she.get()});
+ Unused << SetHistoryID(she->DocshellID());
+ }
+ break;
+ }
+ }
+ }
+}
+
+UniquePtr<LoadingSessionHistoryInfo>
+CanonicalBrowsingContext::CreateLoadingSessionHistoryEntryForLoad(
+ nsDocShellLoadState* aLoadState, SessionHistoryEntry* existingEntry,
+ nsIChannel* aChannel) {
+ RefPtr<SessionHistoryEntry> entry;
+ const LoadingSessionHistoryInfo* existingLoadingInfo =
+ aLoadState->GetLoadingSessionHistoryInfo();
+ MOZ_ASSERT_IF(!existingLoadingInfo, !existingEntry);
+ if (existingLoadingInfo) {
+ if (existingEntry) {
+ entry = existingEntry;
+ } else {
+ MOZ_ASSERT(!existingLoadingInfo->mLoadIsFromSessionHistory);
+
+ SessionHistoryEntry::LoadingEntry* loadingEntry =
+ SessionHistoryEntry::GetByLoadId(existingLoadingInfo->mLoadId);
+ MOZ_LOG(gSHLog, LogLevel::Verbose,
+ ("SHEntry::GetByLoadId(%" PRIu64 ") -> %p",
+ existingLoadingInfo->mLoadId, entry.get()));
+ if (!loadingEntry) {
+ return nullptr;
+ }
+ entry = loadingEntry->mEntry;
+ }
+
+ // If the entry was updated, update also the LoadingSessionHistoryInfo.
+ UniquePtr<LoadingSessionHistoryInfo> lshi =
+ MakeUnique<LoadingSessionHistoryInfo>(entry, existingLoadingInfo);
+ aLoadState->SetLoadingSessionHistoryInfo(std::move(lshi));
+ existingLoadingInfo = aLoadState->GetLoadingSessionHistoryInfo();
+ Unused << SetHistoryEntryCount(entry->BCHistoryLength());
+ } else if (aLoadState->LoadType() == LOAD_REFRESH &&
+ !ShouldAddEntryForRefresh(aLoadState->URI(),
+ aLoadState->PostDataStream()) &&
+ mActiveEntry) {
+ entry = mActiveEntry;
+ } else {
+ entry = new SessionHistoryEntry(aLoadState, aChannel);
+ if (IsTop()) {
+ // Only top level pages care about Get/SetPersist.
+ entry->SetPersist(
+ nsDocShell::ShouldAddToSessionHistory(aLoadState->URI(), aChannel));
+ } else if (mActiveEntry || !mLoadingEntries.IsEmpty()) {
+ entry->SetIsSubFrame(true);
+ }
+ entry->SetDocshellID(GetHistoryID());
+ entry->SetIsDynamicallyAdded(CreatedDynamically());
+ entry->SetForInitialLoad(true);
+ }
+ MOZ_DIAGNOSTIC_ASSERT(entry);
+
+ UniquePtr<LoadingSessionHistoryInfo> loadingInfo;
+ if (existingLoadingInfo) {
+ loadingInfo = MakeUnique<LoadingSessionHistoryInfo>(*existingLoadingInfo);
+ } else {
+ loadingInfo = MakeUnique<LoadingSessionHistoryInfo>(entry);
+ mLoadingEntries.AppendElement(
+ LoadingSessionHistoryEntry{loadingInfo->mLoadId, entry});
+ }
+
+ MOZ_ASSERT(SessionHistoryEntry::GetByLoadId(loadingInfo->mLoadId)->mEntry ==
+ entry);
+
+ return loadingInfo;
+}
+
+UniquePtr<LoadingSessionHistoryInfo>
+CanonicalBrowsingContext::ReplaceLoadingSessionHistoryEntryForLoad(
+ LoadingSessionHistoryInfo* aInfo, nsIChannel* aNewChannel) {
+ MOZ_ASSERT(aInfo);
+ MOZ_ASSERT(aNewChannel);
+
+ SessionHistoryInfo newInfo = SessionHistoryInfo(
+ aNewChannel, aInfo->mInfo.LoadType(),
+ aInfo->mInfo.GetPartitionedPrincipalToInherit(), aInfo->mInfo.GetCsp());
+
+ for (size_t i = 0; i < mLoadingEntries.Length(); ++i) {
+ if (mLoadingEntries[i].mLoadId == aInfo->mLoadId) {
+ RefPtr<SessionHistoryEntry> loadingEntry = mLoadingEntries[i].mEntry;
+ loadingEntry->SetInfo(&newInfo);
+
+ if (IsTop()) {
+ // Only top level pages care about Get/SetPersist.
+ nsCOMPtr<nsIURI> uri;
+ aNewChannel->GetURI(getter_AddRefs(uri));
+ loadingEntry->SetPersist(
+ nsDocShell::ShouldAddToSessionHistory(uri, aNewChannel));
+ } else {
+ loadingEntry->SetIsSubFrame(aInfo->mInfo.IsSubFrame());
+ }
+ loadingEntry->SetDocshellID(GetHistoryID());
+ loadingEntry->SetIsDynamicallyAdded(CreatedDynamically());
+ return MakeUnique<LoadingSessionHistoryInfo>(loadingEntry, aInfo);
+ }
+ }
+ return nullptr;
+}
+
+using PrintPromise = CanonicalBrowsingContext::PrintPromise;
+#ifdef NS_PRINTING
+class PrintListenerAdapter final : public nsIWebProgressListener {
+ public:
+ explicit PrintListenerAdapter(PrintPromise::Private* aPromise)
+ : mPromise(aPromise) {}
+
+ NS_DECL_ISUPPORTS
+
+ // NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ uint32_t aStateFlags, nsresult aStatus) override {
+ if (aStateFlags & nsIWebProgressListener::STATE_STOP &&
+ aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT && mPromise) {
+ mPromise->Resolve(true, __func__);
+ mPromise = nullptr;
+ }
+ return NS_OK;
+ }
+ NS_IMETHOD OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ nsresult aStatus,
+ const char16_t* aMessage) override {
+ if (aStatus != NS_OK && mPromise) {
+ mPromise->Reject(aStatus, __func__);
+ mPromise = nullptr;
+ }
+ return NS_OK;
+ }
+ NS_IMETHOD OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) override {
+ return NS_OK;
+ }
+ NS_IMETHOD OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* aLocation,
+ uint32_t aFlags) override {
+ return NS_OK;
+ }
+ NS_IMETHOD OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) override {
+ return NS_OK;
+ }
+ NS_IMETHOD OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) override {
+ return NS_OK;
+ }
+
+ private:
+ ~PrintListenerAdapter() = default;
+
+ RefPtr<PrintPromise::Private> mPromise;
+};
+
+NS_IMPL_ISUPPORTS(PrintListenerAdapter, nsIWebProgressListener)
+#endif
+
+already_AddRefed<Promise> CanonicalBrowsingContext::PrintJS(
+ nsIPrintSettings* aPrintSettings, ErrorResult& aRv) {
+ RefPtr<Promise> promise = Promise::Create(GetIncumbentGlobal(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return promise.forget();
+ }
+
+ Print(aPrintSettings)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise](bool) { promise->MaybeResolveWithUndefined(); },
+ [promise](nsresult aResult) { promise->MaybeReject(aResult); });
+ return promise.forget();
+}
+
+RefPtr<PrintPromise> CanonicalBrowsingContext::Print(
+ nsIPrintSettings* aPrintSettings) {
+#ifndef NS_PRINTING
+ return PrintPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+#else
+
+ auto promise = MakeRefPtr<PrintPromise::Private>(__func__);
+ auto listener = MakeRefPtr<PrintListenerAdapter>(promise);
+ if (IsInProcess()) {
+ RefPtr<nsGlobalWindowOuter> outerWindow =
+ nsGlobalWindowOuter::Cast(GetDOMWindow());
+ if (NS_WARN_IF(!outerWindow)) {
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ return promise;
+ }
+
+ ErrorResult rv;
+ outerWindow->Print(aPrintSettings,
+ /* aRemotePrintJob = */ nullptr, listener,
+ /* aDocShellToCloneInto = */ nullptr,
+ nsGlobalWindowOuter::IsPreview::No,
+ nsGlobalWindowOuter::IsForWindowDotPrint::No,
+ /* aPrintPreviewCallback = */ nullptr, rv);
+ if (rv.Failed()) {
+ promise->Reject(rv.StealNSResult(), __func__);
+ }
+ return promise;
+ }
+
+ auto* browserParent = GetBrowserParent();
+ if (NS_WARN_IF(!browserParent)) {
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ return promise;
+ }
+
+ nsCOMPtr<nsIPrintSettingsService> printSettingsSvc =
+ do_GetService("@mozilla.org/gfx/printsettings-service;1");
+ if (NS_WARN_IF(!printSettingsSvc)) {
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ return promise;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPrintSettings> printSettings = aPrintSettings;
+ if (!printSettings) {
+ rv =
+ printSettingsSvc->CreateNewPrintSettings(getter_AddRefs(printSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->Reject(rv, __func__);
+ return promise;
+ }
+ }
+
+ embedding::PrintData printData;
+ rv = printSettingsSvc->SerializeToPrintData(printSettings, &printData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->Reject(rv, __func__);
+ return promise;
+ }
+
+ layout::RemotePrintJobParent* remotePrintJob =
+ new layout::RemotePrintJobParent(printSettings);
+ printData.remotePrintJob() =
+ browserParent->Manager()->SendPRemotePrintJobConstructor(remotePrintJob);
+
+ if (listener) {
+ remotePrintJob->RegisterListener(listener);
+ }
+
+ if (NS_WARN_IF(!browserParent->SendPrint(this, printData))) {
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ }
+ return promise.forget();
+#endif
+}
+
+void CanonicalBrowsingContext::CallOnAllTopDescendants(
+ const FunctionRef<CallState(CanonicalBrowsingContext*)>& aCallback,
+ bool aIncludeNestedBrowsers) {
+ MOZ_ASSERT(IsTop(), "Should only call on top BC");
+ MOZ_ASSERT(
+ !aIncludeNestedBrowsers ||
+ (IsChrome() && !GetParentCrossChromeBoundary()),
+ "If aIncludeNestedBrowsers is set, should only call on top chrome BC");
+
+ if (!IsInProcess()) {
+ // We rely on top levels having to be embedded in the parent process, so
+ // we can only have top level descendants if embedded here..
+ return;
+ }
+
+ AutoTArray<RefPtr<BrowsingContextGroup>, 32> groups;
+ BrowsingContextGroup::GetAllGroups(groups);
+ for (auto& browsingContextGroup : groups) {
+ for (auto& bc : browsingContextGroup->Toplevels()) {
+ if (bc == this) {
+ // Cannot be a descendent of myself so skip.
+ continue;
+ }
+
+ if (aIncludeNestedBrowsers) {
+ if (this != bc->Canonical()->TopCrossChromeBoundary()) {
+ continue;
+ }
+ } else {
+ auto* parent = bc->Canonical()->GetParentCrossChromeBoundary();
+ if (!parent || this != parent->Top()) {
+ continue;
+ }
+ }
+
+ if (aCallback(bc->Canonical()) == CallState::Stop) {
+ return;
+ }
+ }
+ }
+}
+
+void CanonicalBrowsingContext::SessionHistoryCommit(
+ uint64_t aLoadId, const nsID& aChangeID, uint32_t aLoadType, bool aPersist,
+ bool aCloneEntryChildren, bool aChannelExpired, uint32_t aCacheKey) {
+ MOZ_LOG(gSHLog, LogLevel::Verbose,
+ ("CanonicalBrowsingContext::SessionHistoryCommit %p %" PRIu64, this,
+ aLoadId));
+ MOZ_ASSERT(aLoadId != UINT64_MAX,
+ "Must not send special about:blank loadinfo to parent.");
+ for (size_t i = 0; i < mLoadingEntries.Length(); ++i) {
+ if (mLoadingEntries[i].mLoadId == aLoadId) {
+ nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory());
+ if (!shistory) {
+ SessionHistoryEntry::RemoveLoadId(aLoadId);
+ mLoadingEntries.RemoveElementAt(i);
+ return;
+ }
+
+ RefPtr<SessionHistoryEntry> newActiveEntry = mLoadingEntries[i].mEntry;
+ if (aCacheKey != 0) {
+ newActiveEntry->SetCacheKey(aCacheKey);
+ }
+
+ if (aChannelExpired) {
+ newActiveEntry->SharedInfo()->mExpired = true;
+ }
+
+ bool loadFromSessionHistory = !newActiveEntry->ForInitialLoad();
+ newActiveEntry->SetForInitialLoad(false);
+ SessionHistoryEntry::RemoveLoadId(aLoadId);
+ mLoadingEntries.RemoveElementAt(i);
+
+ int32_t indexOfHistoryLoad = -1;
+ if (loadFromSessionHistory) {
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(newActiveEntry);
+ indexOfHistoryLoad = shistory->GetIndexOfEntry(root);
+ if (indexOfHistoryLoad < 0) {
+ // Entry has been removed from the session history.
+ return;
+ }
+ }
+
+ CallerWillNotifyHistoryIndexAndLengthChanges caller(shistory);
+
+ // If there is a name in the new entry, clear the name of all contiguous
+ // entries. This is for https://html.spec.whatwg.org/#history-traversal
+ // Step 4.4.2.
+ nsAutoString nameOfNewEntry;
+ newActiveEntry->GetName(nameOfNewEntry);
+ if (!nameOfNewEntry.IsEmpty()) {
+ nsSHistory::WalkContiguousEntries(
+ newActiveEntry,
+ [](nsISHEntry* aEntry) { aEntry->SetName(EmptyString()); });
+ }
+
+ bool addEntry = ShouldUpdateSessionHistory(aLoadType);
+ if (IsTop()) {
+ if (mActiveEntry && !mActiveEntry->GetFrameLoader()) {
+ bool sharesDocument = true;
+ mActiveEntry->SharesDocumentWith(newActiveEntry, &sharesDocument);
+ if (!sharesDocument) {
+ // If the old page won't be in the bfcache,
+ // clear the dynamic entries.
+ RemoveDynEntriesFromActiveSessionHistoryEntry();
+ }
+ }
+
+ if (LOAD_TYPE_HAS_FLAGS(aLoadType,
+ nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY)) {
+ // Replace the current entry with the new entry.
+ int32_t index = shistory->GetIndexForReplace();
+
+ // If we're trying to replace an inexistant shistory entry then we
+ // should append instead.
+ addEntry = index < 0;
+ if (!addEntry) {
+ shistory->ReplaceEntry(index, newActiveEntry);
+ }
+ mActiveEntry = newActiveEntry;
+ } else if (LOAD_TYPE_HAS_FLAGS(
+ aLoadType, nsIWebNavigation::LOAD_FLAGS_IS_REFRESH) &&
+ !ShouldAddEntryForRefresh(newActiveEntry) && mActiveEntry) {
+ addEntry = false;
+ mActiveEntry->ReplaceWith(*newActiveEntry);
+ } else {
+ mActiveEntry = newActiveEntry;
+ }
+
+ if (loadFromSessionHistory) {
+ // XXX Synchronize browsing context tree and session history tree?
+ shistory->InternalSetRequestedIndex(indexOfHistoryLoad);
+ shistory->UpdateIndex();
+
+ if (IsTop()) {
+ mActiveEntry->SetWireframe(Nothing());
+ }
+ } else if (addEntry) {
+ shistory->AddEntry(mActiveEntry, aPersist);
+ shistory->InternalSetRequestedIndex(-1);
+ }
+ } else {
+ // FIXME The old implementations adds it to the parent's mLSHE if there
+ // is one, need to figure out if that makes sense here (peterv
+ // doesn't think it would).
+ if (loadFromSessionHistory) {
+ if (mActiveEntry) {
+ // mActiveEntry is null if we're loading iframes from session
+ // history while also parent page is loading from session history.
+ // In that case there isn't anything to sync.
+ mActiveEntry->SyncTreesForSubframeNavigation(newActiveEntry, Top(),
+ this);
+ }
+ mActiveEntry = newActiveEntry;
+
+ shistory->InternalSetRequestedIndex(indexOfHistoryLoad);
+ // FIXME UpdateIndex() here may update index too early (but even the
+ // old implementation seems to have similar issues).
+ shistory->UpdateIndex();
+ } else if (addEntry) {
+ if (mActiveEntry) {
+ if (LOAD_TYPE_HAS_FLAGS(
+ aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY) ||
+ (LOAD_TYPE_HAS_FLAGS(aLoadType,
+ nsIWebNavigation::LOAD_FLAGS_IS_REFRESH) &&
+ !ShouldAddEntryForRefresh(newActiveEntry))) {
+ // FIXME We need to make sure that when we create the info we
+ // make a copy of the shared state.
+ mActiveEntry->ReplaceWith(*newActiveEntry);
+ } else {
+ // AddChildSHEntryHelper does update the index of the session
+ // history!
+ shistory->AddChildSHEntryHelper(mActiveEntry, newActiveEntry,
+ Top(), aCloneEntryChildren);
+ mActiveEntry = newActiveEntry;
+ }
+ } else {
+ SessionHistoryEntry* parentEntry = GetParent()->mActiveEntry;
+ // XXX What should happen if parent doesn't have mActiveEntry?
+ // Or can that even happen ever?
+ if (parentEntry) {
+ mActiveEntry = newActiveEntry;
+ // FIXME Using IsInProcess for aUseRemoteSubframes isn't quite
+ // right, but aUseRemoteSubframes should be going away.
+ parentEntry->AddChild(
+ mActiveEntry,
+ CreatedDynamically() ? -1 : GetParent()->IndexOf(this),
+ IsInProcess());
+ }
+ }
+ shistory->InternalSetRequestedIndex(-1);
+ }
+ }
+
+ ResetSHEntryHasUserInteractionCache();
+
+ HistoryCommitIndexAndLength(aChangeID, caller);
+
+ shistory->LogHistory();
+
+ return;
+ }
+ // XXX Should the loading entries before [i] be removed?
+ }
+ // FIXME Should we throw an error if we don't find an entry for
+ // aSessionHistoryEntryId?
+}
+
+already_AddRefed<nsDocShellLoadState> CanonicalBrowsingContext::CreateLoadInfo(
+ SessionHistoryEntry* aEntry) {
+ const SessionHistoryInfo& info = aEntry->Info();
+ RefPtr<nsDocShellLoadState> loadState(new nsDocShellLoadState(info.GetURI()));
+ info.FillLoadInfo(*loadState);
+ UniquePtr<LoadingSessionHistoryInfo> loadingInfo;
+ loadingInfo = MakeUnique<LoadingSessionHistoryInfo>(aEntry);
+ mLoadingEntries.AppendElement(
+ LoadingSessionHistoryEntry{loadingInfo->mLoadId, aEntry});
+ loadState->SetLoadingSessionHistoryInfo(std::move(loadingInfo));
+
+ return loadState.forget();
+}
+
+void CanonicalBrowsingContext::NotifyOnHistoryReload(
+ bool aForceReload, bool& aCanReload,
+ Maybe<NotNull<RefPtr<nsDocShellLoadState>>>& aLoadState,
+ Maybe<bool>& aReloadActiveEntry) {
+ MOZ_DIAGNOSTIC_ASSERT(!aLoadState);
+
+ aCanReload = true;
+ nsISHistory* shistory = GetSessionHistory();
+ NS_ENSURE_TRUE_VOID(shistory);
+
+ shistory->NotifyOnHistoryReload(&aCanReload);
+ if (!aCanReload) {
+ return;
+ }
+
+ if (mActiveEntry) {
+ aLoadState.emplace(WrapMovingNotNull(RefPtr{CreateLoadInfo(mActiveEntry)}));
+ aReloadActiveEntry.emplace(true);
+ if (aForceReload) {
+ shistory->RemoveFrameEntries(mActiveEntry);
+ }
+ } else if (!mLoadingEntries.IsEmpty()) {
+ const LoadingSessionHistoryEntry& loadingEntry =
+ mLoadingEntries.LastElement();
+ uint64_t loadId = loadingEntry.mLoadId;
+ aLoadState.emplace(
+ WrapMovingNotNull(RefPtr{CreateLoadInfo(loadingEntry.mEntry)}));
+ aReloadActiveEntry.emplace(false);
+ if (aForceReload) {
+ SessionHistoryEntry::LoadingEntry* entry =
+ SessionHistoryEntry::GetByLoadId(loadId);
+ if (entry) {
+ shistory->RemoveFrameEntries(entry->mEntry);
+ }
+ }
+ }
+
+ if (aLoadState) {
+ // Use 0 as the offset, since aLoadState will be be used for reload.
+ aLoadState.ref()->SetLoadIsFromSessionHistory(0,
+ aReloadActiveEntry.value());
+ }
+ // If we don't have an active entry and we don't have a loading entry then
+ // the nsDocShell will create a load state based on its document.
+}
+
+void CanonicalBrowsingContext::SetActiveSessionHistoryEntry(
+ const Maybe<nsPoint>& aPreviousScrollPos, SessionHistoryInfo* aInfo,
+ uint32_t aLoadType, uint32_t aUpdatedCacheKey, const nsID& aChangeID) {
+ nsISHistory* shistory = GetSessionHistory();
+ if (!shistory) {
+ return;
+ }
+ CallerWillNotifyHistoryIndexAndLengthChanges caller(shistory);
+
+ RefPtr<SessionHistoryEntry> oldActiveEntry = mActiveEntry;
+ if (aPreviousScrollPos.isSome() && oldActiveEntry) {
+ oldActiveEntry->SetScrollPosition(aPreviousScrollPos.ref().x,
+ aPreviousScrollPos.ref().y);
+ }
+ mActiveEntry = new SessionHistoryEntry(aInfo);
+ mActiveEntry->SetDocshellID(GetHistoryID());
+ mActiveEntry->AdoptBFCacheEntry(oldActiveEntry);
+ if (aUpdatedCacheKey != 0) {
+ mActiveEntry->SharedInfo()->mCacheKey = aUpdatedCacheKey;
+ }
+
+ if (IsTop()) {
+ Maybe<int32_t> previousEntryIndex, loadedEntryIndex;
+ shistory->AddToRootSessionHistory(
+ true, oldActiveEntry, this, mActiveEntry, aLoadType,
+ nsDocShell::ShouldAddToSessionHistory(aInfo->GetURI(), nullptr),
+ &previousEntryIndex, &loadedEntryIndex);
+ } else {
+ if (oldActiveEntry) {
+ shistory->AddChildSHEntryHelper(oldActiveEntry, mActiveEntry, Top(),
+ true);
+ } else if (GetParent() && GetParent()->mActiveEntry) {
+ GetParent()->mActiveEntry->AddChild(
+ mActiveEntry, CreatedDynamically() ? -1 : GetParent()->IndexOf(this),
+ UseRemoteSubframes());
+ }
+ }
+
+ ResetSHEntryHasUserInteractionCache();
+
+ shistory->InternalSetRequestedIndex(-1);
+
+ // FIXME Need to do the equivalent of EvictDocumentViewersOrReplaceEntry.
+ HistoryCommitIndexAndLength(aChangeID, caller);
+
+ static_cast<nsSHistory*>(shistory)->LogHistory();
+}
+
+void CanonicalBrowsingContext::ReplaceActiveSessionHistoryEntry(
+ SessionHistoryInfo* aInfo) {
+ if (!mActiveEntry) {
+ return;
+ }
+
+ mActiveEntry->SetInfo(aInfo);
+ // Notify children of the update
+ nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory());
+ if (shistory) {
+ shistory->NotifyOnHistoryReplaceEntry();
+ shistory->UpdateRootBrowsingContextState();
+ }
+
+ ResetSHEntryHasUserInteractionCache();
+
+ if (IsTop()) {
+ mActiveEntry->SetWireframe(Nothing());
+ }
+
+ // FIXME Need to do the equivalent of EvictDocumentViewersOrReplaceEntry.
+}
+
+void CanonicalBrowsingContext::RemoveDynEntriesFromActiveSessionHistoryEntry() {
+ nsISHistory* shistory = GetSessionHistory();
+ // In theory shistory can be null here if the method is called right after
+ // CanonicalBrowsingContext::ReplacedBy call.
+ NS_ENSURE_TRUE_VOID(shistory);
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(mActiveEntry);
+ shistory->RemoveDynEntries(shistory->GetIndexOfEntry(root), mActiveEntry);
+}
+
+void CanonicalBrowsingContext::RemoveFromSessionHistory(const nsID& aChangeID) {
+ nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory());
+ if (shistory) {
+ CallerWillNotifyHistoryIndexAndLengthChanges caller(shistory);
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(mActiveEntry);
+ bool didRemove;
+ AutoTArray<nsID, 16> ids({GetHistoryID()});
+ shistory->RemoveEntries(ids, shistory->GetIndexOfEntry(root), &didRemove);
+ if (didRemove) {
+ RefPtr<BrowsingContext> rootBC = shistory->GetBrowsingContext();
+ if (rootBC) {
+ if (!rootBC->IsInProcess()) {
+ if (ContentParent* cp = rootBC->Canonical()->GetContentParent()) {
+ Unused << cp->SendDispatchLocationChangeEvent(rootBC);
+ }
+ } else if (rootBC->GetDocShell()) {
+ rootBC->GetDocShell()->DispatchLocationChangeEvent();
+ }
+ }
+ }
+ HistoryCommitIndexAndLength(aChangeID, caller);
+ }
+}
+
+Maybe<int32_t> CanonicalBrowsingContext::HistoryGo(
+ int32_t aOffset, uint64_t aHistoryEpoch, bool aRequireUserInteraction,
+ bool aUserActivation, Maybe<ContentParentId> aContentId) {
+ if (aRequireUserInteraction && aOffset != -1 && aOffset != 1) {
+ NS_ERROR(
+ "aRequireUserInteraction may only be used with an offset of -1 or 1");
+ return Nothing();
+ }
+
+ nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory());
+ if (!shistory) {
+ return Nothing();
+ }
+
+ CheckedInt<int32_t> index = shistory->GetRequestedIndex() >= 0
+ ? shistory->GetRequestedIndex()
+ : shistory->Index();
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("HistoryGo(%d->%d) epoch %" PRIu64 "/id %" PRIu64, aOffset,
+ (index + aOffset).value(), aHistoryEpoch,
+ (uint64_t)(aContentId.isSome() ? aContentId.value() : 0)));
+
+ while (true) {
+ index += aOffset;
+ if (!index.isValid()) {
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("Invalid index"));
+ return Nothing();
+ }
+
+ // Check for user interaction if desired, except for the first and last
+ // history entries. We compare with >= to account for the case where
+ // aOffset >= length.
+ if (!aRequireUserInteraction || index.value() >= shistory->Length() - 1 ||
+ index.value() <= 0) {
+ break;
+ }
+ if (shistory->HasUserInteractionAtIndex(index.value())) {
+ break;
+ }
+ }
+
+ // Implement aborting additional history navigations from within the same
+ // event spin of the content process.
+
+ uint64_t epoch;
+ bool sameEpoch = false;
+ Maybe<ContentParentId> id;
+ shistory->GetEpoch(epoch, id);
+
+ if (aContentId == id && epoch >= aHistoryEpoch) {
+ sameEpoch = true;
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("Same epoch/id"));
+ }
+ // Don't update the epoch until we know if the target index is valid
+
+ // GoToIndex checks that index is >= 0 and < length.
+ nsTArray<nsSHistory::LoadEntryResult> loadResults;
+ nsresult rv = shistory->GotoIndex(index.value(), loadResults, sameEpoch,
+ aOffset == 0, aUserActivation);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Dropping HistoryGo - bad index or same epoch (not in same doc)"));
+ return Nothing();
+ }
+ if (epoch < aHistoryEpoch || aContentId != id) {
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("Set epoch"));
+ shistory->SetEpoch(aHistoryEpoch, aContentId);
+ }
+ int32_t requestedIndex = shistory->GetRequestedIndex();
+ nsSHistory::LoadURIs(loadResults);
+ return Some(requestedIndex);
+}
+
+JSObject* CanonicalBrowsingContext::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return CanonicalBrowsingContext_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void CanonicalBrowsingContext::DispatchWheelZoomChange(bool aIncrease) {
+ Element* element = Top()->GetEmbedderElement();
+ if (!element) {
+ return;
+ }
+
+ auto event = aIncrease ? u"DoZoomEnlargeBy10"_ns : u"DoZoomReduceBy10"_ns;
+ auto dispatcher = MakeRefPtr<AsyncEventDispatcher>(
+ element, event, CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ dispatcher->PostDOMEvent();
+}
+
+void CanonicalBrowsingContext::CanonicalDiscard() {
+ if (mTabMediaController) {
+ mTabMediaController->Shutdown();
+ mTabMediaController = nullptr;
+ }
+
+ if (mCurrentLoad) {
+ mCurrentLoad->Cancel(NS_BINDING_ABORTED,
+ "CanonicalBrowsingContext::CanonicalDiscard"_ns);
+ }
+
+ if (mWebProgress) {
+ RefPtr<BrowsingContextWebProgress> progress = mWebProgress;
+ progress->ContextDiscarded();
+ }
+
+ if (IsTop()) {
+ BackgroundSessionStorageManager::RemoveManager(Id());
+ }
+
+ CancelSessionStoreUpdate();
+
+ if (UsePrivateBrowsing() && EverAttached() && IsContent()) {
+ DecreasePrivateCount();
+ }
+}
+
+void CanonicalBrowsingContext::CanonicalAttach() {
+ if (UsePrivateBrowsing() && IsContent()) {
+ IncreasePrivateCount();
+ }
+}
+
+void CanonicalBrowsingContext::AddPendingDiscard() {
+ MOZ_ASSERT(!mFullyDiscarded);
+ mPendingDiscards++;
+}
+
+void CanonicalBrowsingContext::RemovePendingDiscard() {
+ mPendingDiscards--;
+ if (!mPendingDiscards) {
+ mFullyDiscarded = true;
+ auto listeners = std::move(mFullyDiscardedListeners);
+ for (const auto& listener : listeners) {
+ listener(Id());
+ }
+ }
+}
+
+void CanonicalBrowsingContext::AddFinalDiscardListener(
+ std::function<void(uint64_t)>&& aListener) {
+ if (mFullyDiscarded) {
+ aListener(Id());
+ return;
+ }
+ mFullyDiscardedListeners.AppendElement(std::move(aListener));
+}
+
+void CanonicalBrowsingContext::SetForceAppWindowActive(bool aForceActive,
+ ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(IsChrome());
+ MOZ_DIAGNOSTIC_ASSERT(IsTop());
+ if (!IsChrome() || !IsTop()) {
+ return aRv.ThrowNotAllowedError(
+ "You shouldn't need to force this BrowsingContext to be active, use "
+ ".isActive instead");
+ }
+ if (mForceAppWindowActive == aForceActive) {
+ return;
+ }
+ mForceAppWindowActive = aForceActive;
+ RecomputeAppWindowVisibility();
+}
+
+void CanonicalBrowsingContext::RecomputeAppWindowVisibility() {
+ MOZ_RELEASE_ASSERT(IsChrome());
+ MOZ_RELEASE_ASSERT(IsTop());
+
+ const bool wasAlreadyActive = IsActive();
+
+ nsCOMPtr<nsIWidget> widget;
+ if (auto* docShell = GetDocShell()) {
+ nsDocShell::Cast(docShell)->GetMainWidget(getter_AddRefs(widget));
+ }
+
+ Unused << NS_WARN_IF(!widget);
+ const bool isNowActive =
+ ForceAppWindowActive() || (widget && !widget->IsFullyOccluded() &&
+ widget->SizeMode() != nsSizeMode_Minimized);
+
+ if (isNowActive == wasAlreadyActive) {
+ return;
+ }
+
+ SetIsActiveInternal(isNowActive, IgnoreErrors());
+ if (widget) {
+ // Pause if we are not active, resume if we are active.
+ widget->PauseOrResumeCompositor(!isNowActive);
+ }
+}
+
+void CanonicalBrowsingContext::AdjustPrivateBrowsingCount(
+ bool aPrivateBrowsing) {
+ if (IsDiscarded() || !EverAttached() || IsChrome()) {
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aPrivateBrowsing == UsePrivateBrowsing());
+ if (aPrivateBrowsing) {
+ IncreasePrivateCount();
+ } else {
+ DecreasePrivateCount();
+ }
+}
+
+void CanonicalBrowsingContext::NotifyStartDelayedAutoplayMedia() {
+ WindowContext* windowContext = GetCurrentWindowContext();
+ if (!windowContext) {
+ return;
+ }
+
+ // As this function would only be called when user click the play icon on the
+ // tab bar. That's clear user intent to play, so gesture activate the window
+ // context so that the block-autoplay logic allows the media to autoplay.
+ windowContext->NotifyUserGestureActivation();
+ AUTOPLAY_LOG("NotifyStartDelayedAutoplayMedia for chrome bc 0x%08" PRIx64,
+ Id());
+ StartDelayedAutoplayMediaComponents();
+ // Notfiy all content browsing contexts which are related with the canonical
+ // browsing content tree to start delayed autoplay media.
+
+ Group()->EachParent([&](ContentParent* aParent) {
+ Unused << aParent->SendStartDelayedAutoplayMediaComponents(this);
+ });
+}
+
+void CanonicalBrowsingContext::NotifyMediaMutedChanged(bool aMuted,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(!GetParent(),
+ "Notify media mute change on non top-level context!");
+ SetMuted(aMuted, aRv);
+}
+
+uint32_t CanonicalBrowsingContext::CountSiteOrigins(
+ GlobalObject& aGlobal,
+ const Sequence<OwningNonNull<BrowsingContext>>& aRoots) {
+ nsTHashSet<nsCString> uniqueSiteOrigins;
+
+ for (const auto& root : aRoots) {
+ root->PreOrderWalk([&](BrowsingContext* aContext) {
+ WindowGlobalParent* windowGlobalParent =
+ aContext->Canonical()->GetCurrentWindowGlobal();
+ if (windowGlobalParent) {
+ nsIPrincipal* documentPrincipal =
+ windowGlobalParent->DocumentPrincipal();
+
+ bool isContentPrincipal = documentPrincipal->GetIsContentPrincipal();
+ if (isContentPrincipal) {
+ nsCString siteOrigin;
+ documentPrincipal->GetSiteOrigin(siteOrigin);
+ uniqueSiteOrigins.Insert(siteOrigin);
+ }
+ }
+ });
+ }
+
+ return uniqueSiteOrigins.Count();
+}
+
+/* static */
+bool CanonicalBrowsingContext::IsPrivateBrowsingActive() {
+ return gNumberOfPrivateContexts > 0;
+}
+
+void CanonicalBrowsingContext::UpdateMediaControlAction(
+ const MediaControlAction& aAction) {
+ if (IsDiscarded()) {
+ return;
+ }
+ ContentMediaControlKeyHandler::HandleMediaControlAction(this, aAction);
+ Group()->EachParent([&](ContentParent* aParent) {
+ Unused << aParent->SendUpdateMediaControlAction(this, aAction);
+ });
+}
+
+void CanonicalBrowsingContext::LoadURI(nsIURI* aURI,
+ const LoadURIOptions& aOptions,
+ ErrorResult& aError) {
+ RefPtr<nsDocShellLoadState> loadState;
+ nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions(
+ this, aURI, aOptions, getter_AddRefs(loadState));
+ MOZ_ASSERT(rv != NS_ERROR_MALFORMED_URI);
+
+ if (NS_FAILED(rv)) {
+ aError.Throw(rv);
+ return;
+ }
+
+ LoadURI(loadState, true);
+}
+
+void CanonicalBrowsingContext::FixupAndLoadURIString(
+ const nsAString& aURI, const LoadURIOptions& aOptions,
+ ErrorResult& aError) {
+ RefPtr<nsDocShellLoadState> loadState;
+ nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions(
+ this, aURI, aOptions, getter_AddRefs(loadState));
+
+ if (rv == NS_ERROR_MALFORMED_URI) {
+ DisplayLoadError(aURI);
+ return;
+ }
+
+ if (NS_FAILED(rv)) {
+ aError.Throw(rv);
+ return;
+ }
+
+ LoadURI(loadState, true);
+}
+
+void CanonicalBrowsingContext::GoBack(
+ const Optional<int32_t>& aCancelContentJSEpoch,
+ bool aRequireUserInteraction, bool aUserActivation) {
+ if (IsDiscarded()) {
+ return;
+ }
+
+ // Stop any known network loads if necessary.
+ if (mCurrentLoad) {
+ mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns);
+ }
+
+ if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) {
+ if (aCancelContentJSEpoch.WasPassed()) {
+ docShell->SetCancelContentJSEpoch(aCancelContentJSEpoch.Value());
+ }
+ docShell->GoBack(aRequireUserInteraction, aUserActivation);
+ } else if (ContentParent* cp = GetContentParent()) {
+ Maybe<int32_t> cancelContentJSEpoch;
+ if (aCancelContentJSEpoch.WasPassed()) {
+ cancelContentJSEpoch = Some(aCancelContentJSEpoch.Value());
+ }
+ Unused << cp->SendGoBack(this, cancelContentJSEpoch,
+ aRequireUserInteraction, aUserActivation);
+ }
+}
+void CanonicalBrowsingContext::GoForward(
+ const Optional<int32_t>& aCancelContentJSEpoch,
+ bool aRequireUserInteraction, bool aUserActivation) {
+ if (IsDiscarded()) {
+ return;
+ }
+
+ // Stop any known network loads if necessary.
+ if (mCurrentLoad) {
+ mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns);
+ }
+
+ if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) {
+ if (aCancelContentJSEpoch.WasPassed()) {
+ docShell->SetCancelContentJSEpoch(aCancelContentJSEpoch.Value());
+ }
+ docShell->GoForward(aRequireUserInteraction, aUserActivation);
+ } else if (ContentParent* cp = GetContentParent()) {
+ Maybe<int32_t> cancelContentJSEpoch;
+ if (aCancelContentJSEpoch.WasPassed()) {
+ cancelContentJSEpoch.emplace(aCancelContentJSEpoch.Value());
+ }
+ Unused << cp->SendGoForward(this, cancelContentJSEpoch,
+ aRequireUserInteraction, aUserActivation);
+ }
+}
+void CanonicalBrowsingContext::GoToIndex(
+ int32_t aIndex, const Optional<int32_t>& aCancelContentJSEpoch,
+ bool aUserActivation) {
+ if (IsDiscarded()) {
+ return;
+ }
+
+ // Stop any known network loads if necessary.
+ if (mCurrentLoad) {
+ mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns);
+ }
+
+ if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) {
+ if (aCancelContentJSEpoch.WasPassed()) {
+ docShell->SetCancelContentJSEpoch(aCancelContentJSEpoch.Value());
+ }
+ docShell->GotoIndex(aIndex, aUserActivation);
+ } else if (ContentParent* cp = GetContentParent()) {
+ Maybe<int32_t> cancelContentJSEpoch;
+ if (aCancelContentJSEpoch.WasPassed()) {
+ cancelContentJSEpoch.emplace(aCancelContentJSEpoch.Value());
+ }
+ Unused << cp->SendGoToIndex(this, aIndex, cancelContentJSEpoch,
+ aUserActivation);
+ }
+}
+void CanonicalBrowsingContext::Reload(uint32_t aReloadFlags) {
+ if (IsDiscarded()) {
+ return;
+ }
+
+ // Stop any known network loads if necessary.
+ if (mCurrentLoad) {
+ mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns);
+ }
+
+ if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) {
+ docShell->Reload(aReloadFlags);
+ } else if (ContentParent* cp = GetContentParent()) {
+ Unused << cp->SendReload(this, aReloadFlags);
+ }
+}
+
+void CanonicalBrowsingContext::Stop(uint32_t aStopFlags) {
+ if (IsDiscarded()) {
+ return;
+ }
+
+ // Stop any known network loads if necessary.
+ if (mCurrentLoad && (aStopFlags & nsIWebNavigation::STOP_NETWORK)) {
+ mCurrentLoad->Cancel(NS_BINDING_ABORTED,
+ "CanonicalBrowsingContext::Stop"_ns);
+ }
+
+ // Ask the docshell to stop to handle loads that haven't
+ // yet reached here, as well as non-network activity.
+ if (auto* docShell = nsDocShell::Cast(GetDocShell())) {
+ docShell->Stop(aStopFlags);
+ } else if (ContentParent* cp = GetContentParent()) {
+ Unused << cp->SendStopLoad(this, aStopFlags);
+ }
+}
+
+void CanonicalBrowsingContext::PendingRemotenessChange::ProcessLaunched() {
+ if (!mPromise) {
+ return;
+ }
+
+ if (mContentParent) {
+ // If our new content process is still unloading from a previous process
+ // switch, wait for that unload to complete before continuing.
+ auto found = mTarget->FindUnloadingHost(mContentParent->ChildID());
+ if (found != mTarget->mUnloadingHosts.end()) {
+ found->mCallbacks.AppendElement(
+ [self = RefPtr{this}]() { self->ProcessReady(); });
+ return;
+ }
+ }
+
+ ProcessReady();
+}
+
+void CanonicalBrowsingContext::PendingRemotenessChange::ProcessReady() {
+ if (!mPromise) {
+ return;
+ }
+
+ MOZ_ASSERT(!mProcessReady);
+ mProcessReady = true;
+ MaybeFinish();
+}
+
+void CanonicalBrowsingContext::PendingRemotenessChange::MaybeFinish() {
+ if (!mPromise) {
+ return;
+ }
+
+ if (!mProcessReady || mWaitingForPrepareToChange) {
+ return;
+ }
+
+ // If this BrowsingContext is embedded within the parent process, perform the
+ // process switch directly.
+ nsresult rv = mTarget->IsTopContent() ? FinishTopContent() : FinishSubframe();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error finishing PendingRemotenessChange!");
+ Cancel(rv);
+ } else {
+ Clear();
+ }
+}
+
+// Logic for finishing a toplevel process change embedded within the parent
+// process. Due to frontend integration the logic differs substantially from
+// subframe process switches, and is handled separately.
+nsresult CanonicalBrowsingContext::PendingRemotenessChange::FinishTopContent() {
+ MOZ_DIAGNOSTIC_ASSERT(mTarget->IsTop(),
+ "We shouldn't be trying to change the remoteness of "
+ "non-remote iframes");
+
+ // Abort if our ContentParent died while process switching.
+ if (mContentParent && NS_WARN_IF(mContentParent->IsShuttingDown())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // While process switching, we need to check if any of our ancestors are
+ // discarded or no longer current, in which case the process switch needs to
+ // be aborted.
+ RefPtr<CanonicalBrowsingContext> target(mTarget);
+ if (target->IsDiscarded() || !target->AncestorsAreCurrent()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Element* browserElement = target->GetEmbedderElement();
+ if (!browserElement) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIBrowser> browser = browserElement->AsBrowser();
+ if (!browser) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsFrameLoaderOwner> frameLoaderOwner = do_QueryObject(browserElement);
+ MOZ_RELEASE_ASSERT(frameLoaderOwner,
+ "embedder browser must be nsFrameLoaderOwner");
+
+ // If we're process switching a browsing context in private browsing
+ // mode we might decrease the private browsing count to '0', which
+ // would make us fire "last-pb-context-exited" and drop the private
+ // session. To prevent that we artificially increment the number of
+ // private browsing contexts with '1' until the process switch is done.
+ bool usePrivateBrowsing = mTarget->UsePrivateBrowsing();
+ if (usePrivateBrowsing) {
+ IncreasePrivateCount();
+ }
+
+ auto restorePrivateCount = MakeScopeExit([usePrivateBrowsing]() {
+ if (usePrivateBrowsing) {
+ DecreasePrivateCount();
+ }
+ });
+
+ // Tell frontend code that this browser element is about to change process.
+ nsresult rv = browser->BeforeChangeRemoteness();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Some frontend code checks the value of the `remote` attribute on the
+ // browser to determine if it is remote, so update the value.
+ browserElement->SetAttr(kNameSpaceID_None, nsGkAtoms::remote,
+ mContentParent ? u"true"_ns : u"false"_ns,
+ /* notify */ true);
+
+ // The process has been created, hand off to nsFrameLoaderOwner to finish
+ // the process switch.
+ ErrorResult error;
+ frameLoaderOwner->ChangeRemotenessToProcess(mContentParent, mOptions,
+ mSpecificGroup, error);
+ if (error.Failed()) {
+ return error.StealNSResult();
+ }
+
+ // Tell frontend the load is done.
+ bool loadResumed = false;
+ rv = browser->FinishChangeRemoteness(mPendingSwitchId, &loadResumed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // We did it! The process switch is complete.
+ RefPtr<nsFrameLoader> frameLoader = frameLoaderOwner->GetFrameLoader();
+ RefPtr<BrowserParent> newBrowser = frameLoader->GetBrowserParent();
+ if (!newBrowser) {
+ if (mContentParent) {
+ // Failed to create the BrowserParent somehow! Abort the process switch
+ // attempt.
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!loadResumed) {
+ RefPtr<nsDocShell> newDocShell = frameLoader->GetDocShell(error);
+ if (error.Failed()) {
+ return error.StealNSResult();
+ }
+
+ rv = newDocShell->ResumeRedirectedLoad(mPendingSwitchId,
+ /* aHistoryIndex */ -1);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ } else if (!loadResumed) {
+ newBrowser->ResumeLoad(mPendingSwitchId);
+ }
+
+ mPromise->Resolve(newBrowser, __func__);
+ return NS_OK;
+}
+
+nsresult CanonicalBrowsingContext::PendingRemotenessChange::FinishSubframe() {
+ MOZ_DIAGNOSTIC_ASSERT(!mOptions.mReplaceBrowsingContext,
+ "Cannot replace BC for subframe");
+ MOZ_DIAGNOSTIC_ASSERT(!mTarget->IsTop());
+
+ // While process switching, we need to check if any of our ancestors are
+ // discarded or no longer current, in which case the process switch needs to
+ // be aborted.
+ RefPtr<CanonicalBrowsingContext> target(mTarget);
+ if (target->IsDiscarded() || !target->AncestorsAreCurrent()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!mContentParent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<WindowGlobalParent> embedderWindow = target->GetParentWindowContext();
+ if (NS_WARN_IF(!embedderWindow) || NS_WARN_IF(!embedderWindow->CanSend())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<BrowserParent> embedderBrowser = embedderWindow->GetBrowserParent();
+ if (NS_WARN_IF(!embedderBrowser)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we're creating a new remote browser, and the host process is already
+ // dead, abort the process switch.
+ if (mContentParent != embedderBrowser->Manager() &&
+ NS_WARN_IF(mContentParent->IsShuttingDown())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<BrowserParent> oldBrowser = target->GetBrowserParent();
+ target->SetCurrentBrowserParent(nullptr);
+
+ // If we were in a remote frame, trigger unloading of the remote window. The
+ // previous BrowserParent is registered in `mUnloadingHosts` and will only be
+ // cleared when the BrowserParent is fully destroyed.
+ bool wasRemote = oldBrowser && oldBrowser->GetBrowsingContext() == target;
+ if (wasRemote) {
+ MOZ_DIAGNOSTIC_ASSERT(oldBrowser != embedderBrowser);
+ MOZ_DIAGNOSTIC_ASSERT(oldBrowser->IsDestroyed() ||
+ oldBrowser->GetBrowserBridgeParent());
+
+ // `oldBrowser` will clear the `UnloadingHost` status once the actor has
+ // been destroyed.
+ if (oldBrowser->CanSend()) {
+ target->StartUnloadingHost(oldBrowser->Manager()->ChildID());
+ Unused << oldBrowser->SendWillChangeProcess();
+ oldBrowser->Destroy();
+ }
+ }
+
+ // Update which process is considered the current owner
+ target->SetOwnerProcessId(mContentParent->ChildID());
+
+ // If we're switching from remote to local, we don't need to create a
+ // BrowserBridge, and can instead perform the switch directly.
+ if (mContentParent == embedderBrowser->Manager()) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mPendingSwitchId,
+ "We always have a PendingSwitchId, except for print-preview loads, "
+ "which will never perform a process-switch to being in-process with "
+ "their embedder");
+ MOZ_DIAGNOSTIC_ASSERT(wasRemote,
+ "Attempt to process-switch from local to local?");
+
+ target->SetCurrentBrowserParent(embedderBrowser);
+ Unused << embedderWindow->SendMakeFrameLocal(target, mPendingSwitchId);
+ mPromise->Resolve(embedderBrowser, __func__);
+ return NS_OK;
+ }
+
+ // The BrowsingContext will be remote, either as an already-remote frame
+ // changing processes, or as a local frame becoming remote. Construct a new
+ // BrowserBridgeParent to host the remote content.
+ target->SetCurrentBrowserParent(nullptr);
+
+ MOZ_DIAGNOSTIC_ASSERT(target->UseRemoteTabs() && target->UseRemoteSubframes(),
+ "Not supported without fission");
+ uint32_t chromeFlags = nsIWebBrowserChrome::CHROME_REMOTE_WINDOW |
+ nsIWebBrowserChrome::CHROME_FISSION_WINDOW;
+ if (target->UsePrivateBrowsing()) {
+ chromeFlags |= nsIWebBrowserChrome::CHROME_PRIVATE_WINDOW;
+ }
+
+ nsCOMPtr<nsIPrincipal> initialPrincipal =
+ NullPrincipal::Create(target->OriginAttributesRef());
+ WindowGlobalInit windowInit =
+ WindowGlobalActor::AboutBlankInitializer(target, initialPrincipal);
+
+ // Create and initialize our new BrowserBridgeParent.
+ TabId tabId(nsContentUtils::GenerateTabId());
+ RefPtr<BrowserBridgeParent> bridge = new BrowserBridgeParent();
+ nsresult rv = bridge->InitWithProcess(embedderBrowser, mContentParent,
+ windowInit, chromeFlags, tabId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If we've already destroyed our previous document, make a best-effort
+ // attempt to recover from this failure and show the crashed tab UI. We only
+ // do this in the previously-remote case, as previously in-process frames
+ // will have their navigation cancelled, and will remain visible.
+ if (wasRemote) {
+ target->ShowSubframeCrashedUI(oldBrowser->GetBrowserBridgeParent());
+ }
+ return rv;
+ }
+
+ // Tell the embedder process a remoteness change is in-process. When this is
+ // acknowledged, reset the in-flight ID if it used to be an in-process load.
+ RefPtr<BrowserParent> newBrowser = bridge->GetBrowserParent();
+ {
+ // If we weren't remote, mark our embedder window browser as unloading until
+ // our embedder process has acked our MakeFrameRemote message.
+ Maybe<uint64_t> clearChildID;
+ if (!wasRemote) {
+ clearChildID = Some(embedderBrowser->Manager()->ChildID());
+ target->StartUnloadingHost(*clearChildID);
+ }
+ auto callback = [target, clearChildID](auto&&) {
+ if (clearChildID) {
+ target->ClearUnloadingHost(*clearChildID);
+ }
+ };
+
+ ManagedEndpoint<PBrowserBridgeChild> endpoint =
+ embedderBrowser->OpenPBrowserBridgeEndpoint(bridge);
+ MOZ_DIAGNOSTIC_ASSERT(endpoint.IsValid());
+ embedderWindow->SendMakeFrameRemote(target, std::move(endpoint), tabId,
+ newBrowser->GetLayersId(), callback,
+ callback);
+ }
+
+ // Resume the pending load in our new process.
+ if (mPendingSwitchId) {
+ newBrowser->ResumeLoad(mPendingSwitchId);
+ }
+
+ // We did it! The process switch is complete.
+ mPromise->Resolve(newBrowser, __func__);
+ return NS_OK;
+}
+
+void CanonicalBrowsingContext::PendingRemotenessChange::Cancel(nsresult aRv) {
+ if (!mPromise) {
+ return;
+ }
+
+ mPromise->Reject(aRv, __func__);
+ Clear();
+}
+
+void CanonicalBrowsingContext::PendingRemotenessChange::Clear() {
+ // Make sure we don't die while we're doing cleanup.
+ RefPtr<PendingRemotenessChange> kungFuDeathGrip(this);
+ if (mTarget) {
+ MOZ_DIAGNOSTIC_ASSERT(mTarget->mPendingRemotenessChange == this);
+ mTarget->mPendingRemotenessChange = nullptr;
+ }
+
+ // When this PendingRemotenessChange was created, it was given a
+ // `mContentParent`.
+ if (mContentParent) {
+ mContentParent->RemoveKeepAlive();
+ mContentParent = nullptr;
+ }
+
+ // If we were given a specific group, stop keeping that group alive manually.
+ if (mSpecificGroup) {
+ mSpecificGroup->RemoveKeepAlive();
+ mSpecificGroup = nullptr;
+ }
+
+ mPromise = nullptr;
+ mTarget = nullptr;
+}
+
+CanonicalBrowsingContext::PendingRemotenessChange::PendingRemotenessChange(
+ CanonicalBrowsingContext* aTarget, RemotenessPromise::Private* aPromise,
+ uint64_t aPendingSwitchId, const NavigationIsolationOptions& aOptions)
+ : mTarget(aTarget),
+ mPromise(aPromise),
+ mPendingSwitchId(aPendingSwitchId),
+ mOptions(aOptions) {}
+
+CanonicalBrowsingContext::PendingRemotenessChange::~PendingRemotenessChange() {
+ MOZ_ASSERT(!mPromise && !mTarget && !mContentParent && !mSpecificGroup,
+ "should've already been Cancel() or Complete()-ed");
+}
+
+BrowserParent* CanonicalBrowsingContext::GetBrowserParent() const {
+ return mCurrentBrowserParent;
+}
+
+void CanonicalBrowsingContext::SetCurrentBrowserParent(
+ BrowserParent* aBrowserParent) {
+ MOZ_DIAGNOSTIC_ASSERT(!mCurrentBrowserParent || !aBrowserParent,
+ "BrowsingContext already has a current BrowserParent!");
+ MOZ_DIAGNOSTIC_ASSERT_IF(aBrowserParent, aBrowserParent->CanSend());
+ MOZ_DIAGNOSTIC_ASSERT_IF(aBrowserParent,
+ aBrowserParent->Manager()->ChildID() == mProcessId);
+
+ // BrowserParent must either be directly for this BrowsingContext, or the
+ // manager out our embedder WindowGlobal.
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ aBrowserParent && aBrowserParent->GetBrowsingContext() != this,
+ GetParentWindowContext() &&
+ GetParentWindowContext()->Manager() == aBrowserParent);
+
+ if (aBrowserParent && IsTopContent() && !ManuallyManagesActiveness()) {
+ aBrowserParent->SetRenderLayers(IsActive());
+ }
+
+ mCurrentBrowserParent = aBrowserParent;
+}
+
+bool CanonicalBrowsingContext::ManuallyManagesActiveness() const {
+ auto* el = GetEmbedderElement();
+ return el && el->IsXULElement() && el->HasAttr(nsGkAtoms::manualactiveness);
+}
+
+RefPtr<CanonicalBrowsingContext::RemotenessPromise>
+CanonicalBrowsingContext::ChangeRemoteness(
+ const NavigationIsolationOptions& aOptions, uint64_t aPendingSwitchId) {
+ MOZ_DIAGNOSTIC_ASSERT(IsContent(),
+ "cannot change the process of chrome contexts");
+ MOZ_DIAGNOSTIC_ASSERT(
+ IsTop() == IsEmbeddedInProcess(0),
+ "toplevel content must be embedded in the parent process");
+ MOZ_DIAGNOSTIC_ASSERT(!aOptions.mReplaceBrowsingContext || IsTop(),
+ "Cannot replace BrowsingContext for subframes");
+ MOZ_DIAGNOSTIC_ASSERT(
+ aOptions.mSpecificGroupId == 0 || aOptions.mReplaceBrowsingContext,
+ "Cannot specify group ID unless replacing BC");
+ MOZ_DIAGNOSTIC_ASSERT(aPendingSwitchId || !IsTop(),
+ "Should always have aPendingSwitchId for top-level "
+ "frames");
+
+ if (!AncestorsAreCurrent()) {
+ NS_WARNING("An ancestor context is no longer current");
+ return RemotenessPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ // Ensure our embedder hasn't been destroyed or asked to shutdown already.
+ RefPtr<WindowGlobalParent> embedderWindowGlobal = GetEmbedderWindowGlobal();
+ if (!embedderWindowGlobal) {
+ NS_WARNING("Non-embedded BrowsingContext");
+ return RemotenessPromise::CreateAndReject(NS_ERROR_UNEXPECTED, __func__);
+ }
+
+ if (!embedderWindowGlobal->CanSend()) {
+ NS_WARNING("Embedder already been destroyed.");
+ return RemotenessPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ RefPtr<BrowserParent> embedderBrowser =
+ embedderWindowGlobal->GetBrowserParent();
+ if (embedderBrowser && embedderBrowser->Manager()->IsShuttingDown()) {
+ NS_WARNING("Embedder already asked to shutdown.");
+ return RemotenessPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ if (aOptions.mRemoteType.IsEmpty() && (!IsTop() || !GetEmbedderElement())) {
+ NS_WARNING("Cannot load non-remote subframes");
+ return RemotenessPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ // Cancel ongoing remoteness changes.
+ if (mPendingRemotenessChange) {
+ mPendingRemotenessChange->Cancel(NS_ERROR_ABORT);
+ MOZ_DIAGNOSTIC_ASSERT(!mPendingRemotenessChange, "Should have cleared");
+ }
+
+ auto promise = MakeRefPtr<RemotenessPromise::Private>(__func__);
+ promise->UseDirectTaskDispatch(__func__);
+
+ RefPtr<PendingRemotenessChange> change =
+ new PendingRemotenessChange(this, promise, aPendingSwitchId, aOptions);
+ mPendingRemotenessChange = change;
+
+ // If a specific BrowsingContextGroup ID was specified for this load, make
+ // sure to keep it alive until the process switch is completed.
+ if (aOptions.mSpecificGroupId) {
+ change->mSpecificGroup =
+ BrowsingContextGroup::GetOrCreate(aOptions.mSpecificGroupId);
+ change->mSpecificGroup->AddKeepAlive();
+ }
+
+ // Call `prepareToChangeRemoteness` in parallel with starting a new process
+ // for <browser> loads.
+ if (IsTop() && GetEmbedderElement()) {
+ nsCOMPtr<nsIBrowser> browser = GetEmbedderElement()->AsBrowser();
+ if (!browser) {
+ change->Cancel(NS_ERROR_FAILURE);
+ return promise.forget();
+ }
+
+ RefPtr<Promise> blocker;
+ nsresult rv = browser->PrepareToChangeRemoteness(getter_AddRefs(blocker));
+ if (NS_FAILED(rv)) {
+ change->Cancel(rv);
+ return promise.forget();
+ }
+
+ // Mark prepareToChange as unresolved, and wait for it to become resolved.
+ if (blocker && blocker->State() != Promise::PromiseState::Resolved) {
+ change->mWaitingForPrepareToChange = true;
+ blocker->AddCallbacksWithCycleCollectedArgs(
+ [change](JSContext*, JS::Handle<JS::Value>, ErrorResult&) {
+ change->mWaitingForPrepareToChange = false;
+ change->MaybeFinish();
+ },
+ [change](JSContext*, JS::Handle<JS::Value> aValue, ErrorResult&) {
+ change->Cancel(
+ Promise::TryExtractNSResultFromRejectionValue(aValue));
+ });
+ }
+ }
+
+ // Switching a subframe to be local within it's embedding process.
+ if (embedderBrowser &&
+ aOptions.mRemoteType == embedderBrowser->Manager()->GetRemoteType()) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aPendingSwitchId,
+ "We always have a PendingSwitchId, except for print-preview loads, "
+ "which will never perform a process-switch to being in-process with "
+ "their embedder");
+ MOZ_DIAGNOSTIC_ASSERT(!aOptions.mReplaceBrowsingContext);
+ MOZ_DIAGNOSTIC_ASSERT(!aOptions.mRemoteType.IsEmpty());
+ MOZ_DIAGNOSTIC_ASSERT(!change->mWaitingForPrepareToChange);
+ MOZ_DIAGNOSTIC_ASSERT(!change->mSpecificGroup);
+
+ // Switching to local, so we don't need to create a new process, and will
+ // instead use our embedder process.
+ change->mContentParent = embedderBrowser->Manager();
+ change->mContentParent->AddKeepAlive();
+ change->ProcessLaunched();
+ return promise.forget();
+ }
+
+ // Switching to the parent process.
+ if (aOptions.mRemoteType.IsEmpty()) {
+ change->ProcessLaunched();
+ return promise.forget();
+ }
+
+ // If we're aiming to end up in a new process of the same type as our old
+ // process, and then putting our previous document in the BFCache, try to stay
+ // in the same process to avoid creating new processes unnecessarily.
+ RefPtr<ContentParent> existingProcess = GetContentParent();
+ if (existingProcess && !existingProcess->IsShuttingDown() &&
+ aOptions.mReplaceBrowsingContext &&
+ aOptions.mRemoteType == existingProcess->GetRemoteType()) {
+ change->mContentParent = existingProcess;
+ change->mContentParent->AddKeepAlive();
+ change->ProcessLaunched();
+ return promise.forget();
+ }
+
+ // Try to predict which BrowsingContextGroup will be used for the final load
+ // in this BrowsingContext. This has to be accurate if switching into an
+ // existing group, as it will control what pool of processes will be used
+ // for process selection.
+ //
+ // It's _technically_ OK to provide a group here if we're actually going to
+ // switch into a brand new group, though it's sub-optimal, as it can
+ // restrict the set of processes we're using.
+ BrowsingContextGroup* finalGroup =
+ aOptions.mReplaceBrowsingContext ? change->mSpecificGroup.get() : Group();
+
+ bool preferUsed =
+ StaticPrefs::browser_tabs_remote_subframesPreferUsed() && !IsTop();
+
+ change->mContentParent = ContentParent::GetNewOrUsedLaunchingBrowserProcess(
+ /* aRemoteType = */ aOptions.mRemoteType,
+ /* aGroup = */ finalGroup,
+ /* aPriority = */ hal::PROCESS_PRIORITY_FOREGROUND,
+ /* aPreferUsed = */ preferUsed);
+ if (!change->mContentParent) {
+ change->Cancel(NS_ERROR_FAILURE);
+ return promise.forget();
+ }
+
+ // Add a KeepAlive used by this ContentParent, which will be cleared when
+ // the change is complete. This should prevent the process dying before
+ // we're ready to use it.
+ change->mContentParent->AddKeepAlive();
+ if (change->mContentParent->IsLaunching()) {
+ change->mContentParent->WaitForLaunchAsync()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [change](ContentParent*) { change->ProcessLaunched(); },
+ [change]() { change->Cancel(NS_ERROR_FAILURE); });
+ } else {
+ change->ProcessLaunched();
+ }
+ return promise.forget();
+}
+
+void CanonicalBrowsingContext::MaybeSetPermanentKey(Element* aEmbedder) {
+ MOZ_DIAGNOSTIC_ASSERT(IsTop());
+
+ if (aEmbedder) {
+ if (nsCOMPtr<nsIBrowser> browser = aEmbedder->AsBrowser()) {
+ JS::Rooted<JS::Value> key(RootingCx());
+ if (NS_SUCCEEDED(browser->GetPermanentKey(&key)) && key.isObject()) {
+ mPermanentKey = key;
+ }
+ }
+ }
+}
+
+MediaController* CanonicalBrowsingContext::GetMediaController() {
+ // We would only create one media controller per tab, so accessing the
+ // controller via the top-level browsing context.
+ if (GetParent()) {
+ return Cast(Top())->GetMediaController();
+ }
+
+ MOZ_ASSERT(!GetParent(),
+ "Must access the controller from the top-level browsing context!");
+ // Only content browsing context can create media controller, we won't create
+ // controller for chrome document, such as the browser UI.
+ if (!mTabMediaController && !IsDiscarded() && IsContent()) {
+ mTabMediaController = new MediaController(Id());
+ }
+ return mTabMediaController;
+}
+
+bool CanonicalBrowsingContext::HasCreatedMediaController() const {
+ return !!mTabMediaController;
+}
+
+bool CanonicalBrowsingContext::SupportsLoadingInParent(
+ nsDocShellLoadState* aLoadState, uint64_t* aOuterWindowId) {
+ // We currently don't support initiating loads in the parent when they are
+ // watched by devtools. This is because devtools tracks loads using content
+ // process notifications, which happens after the load is initiated in this
+ // case. Devtools clears all prior requests when it detects a new navigation,
+ // so it drops the main document load that happened here.
+ if (WatchedByDevTools()) {
+ return false;
+ }
+
+ // Session-history-in-parent implementation relies currently on getting a
+ // round trip through a child process.
+ if (aLoadState->LoadIsFromSessionHistory()) {
+ return false;
+ }
+
+ // DocumentChannel currently only supports connecting channels into the
+ // content process, so we can only support schemes that will always be loaded
+ // there for now. Restrict to just http(s) for simplicity.
+ if (!net::SchemeIsHTTP(aLoadState->URI()) &&
+ !net::SchemeIsHTTPS(aLoadState->URI())) {
+ return false;
+ }
+
+ if (WindowGlobalParent* global = GetCurrentWindowGlobal()) {
+ nsCOMPtr<nsIURI> currentURI = global->GetDocumentURI();
+ if (currentURI) {
+ bool newURIHasRef = false;
+ aLoadState->URI()->GetHasRef(&newURIHasRef);
+ bool equalsExceptRef = false;
+ aLoadState->URI()->EqualsExceptRef(currentURI, &equalsExceptRef);
+
+ if (equalsExceptRef && newURIHasRef) {
+ // This navigation is same-doc WRT the current one, we should pass it
+ // down to the docshell to be handled.
+ return false;
+ }
+ }
+ // If the current document has a beforeunload listener, then we need to
+ // start the load in that process after we fire the event.
+ if (global->HasBeforeUnload()) {
+ return false;
+ }
+
+ *aOuterWindowId = global->OuterWindowId();
+ }
+ return true;
+}
+
+bool CanonicalBrowsingContext::LoadInParent(nsDocShellLoadState* aLoadState,
+ bool aSetNavigating) {
+ // We currently only support starting loads directly from the
+ // CanonicalBrowsingContext for top-level BCs.
+ // We currently only support starting loads directly from the
+ // CanonicalBrowsingContext for top-level BCs.
+ if (!IsTopContent() || !GetContentParent() ||
+ !StaticPrefs::browser_tabs_documentchannel_parent_controlled()) {
+ return false;
+ }
+
+ uint64_t outerWindowId = 0;
+ if (!SupportsLoadingInParent(aLoadState, &outerWindowId)) {
+ return false;
+ }
+
+ MOZ_ASSERT(!net::SchemeIsJavascript(aLoadState->URI()));
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SetParentInitiatedNavigationEpoch(++gParentInitiatedNavigationEpoch));
+ // Note: If successful, this will recurse into StartDocumentLoad and
+ // set mCurrentLoad to the DocumentLoadListener instance created.
+ // Ideally in the future we will only start loads from here, and we can
+ // just set this directly instead.
+ return net::DocumentLoadListener::LoadInParent(this, aLoadState,
+ aSetNavigating);
+}
+
+bool CanonicalBrowsingContext::AttemptSpeculativeLoadInParent(
+ nsDocShellLoadState* aLoadState) {
+ // We currently only support starting loads directly from the
+ // CanonicalBrowsingContext for top-level BCs.
+ // We currently only support starting loads directly from the
+ // CanonicalBrowsingContext for top-level BCs.
+ if (!IsTopContent() || !GetContentParent() ||
+ (StaticPrefs::browser_tabs_documentchannel_parent_controlled())) {
+ return false;
+ }
+
+ uint64_t outerWindowId = 0;
+ if (!SupportsLoadingInParent(aLoadState, &outerWindowId)) {
+ return false;
+ }
+
+ // If we successfully open the DocumentChannel, then it'll register
+ // itself using aLoadIdentifier and be kept alive until it completes
+ // loading.
+ return net::DocumentLoadListener::SpeculativeLoadInParent(this, aLoadState);
+}
+
+bool CanonicalBrowsingContext::StartDocumentLoad(
+ net::DocumentLoadListener* aLoad) {
+ // If we're controlling loads from the parent, then starting a new load means
+ // that we need to cancel any existing ones.
+ if (StaticPrefs::browser_tabs_documentchannel_parent_controlled() &&
+ mCurrentLoad) {
+ // Make sure we are not loading a javascript URI.
+ MOZ_ASSERT(!aLoad->IsLoadingJSURI());
+
+ // If we want to do a download, don't cancel the current navigation.
+ if (!aLoad->IsDownload()) {
+ mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns);
+ }
+ }
+ mCurrentLoad = aLoad;
+
+ if (NS_FAILED(SetCurrentLoadIdentifier(Some(aLoad->GetLoadIdentifier())))) {
+ mCurrentLoad = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+void CanonicalBrowsingContext::EndDocumentLoad(bool aContinueNavigating) {
+ mCurrentLoad = nullptr;
+
+ if (!aContinueNavigating) {
+ // Resetting the current load identifier on a discarded context
+ // has no effect when a document load has finished.
+ Unused << SetCurrentLoadIdentifier(Nothing());
+ }
+}
+
+already_AddRefed<nsIURI> CanonicalBrowsingContext::GetCurrentURI() const {
+ nsCOMPtr<nsIURI> currentURI;
+ if (nsIDocShell* docShell = GetDocShell()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ nsDocShell::Cast(docShell)->GetCurrentURI(getter_AddRefs(currentURI)));
+ } else {
+ currentURI = mCurrentRemoteURI;
+ }
+ return currentURI.forget();
+}
+
+void CanonicalBrowsingContext::SetCurrentRemoteURI(nsIURI* aCurrentRemoteURI) {
+ MOZ_ASSERT(!GetDocShell());
+ mCurrentRemoteURI = aCurrentRemoteURI;
+}
+
+void CanonicalBrowsingContext::ResetSHEntryHasUserInteractionCache() {
+ WindowContext* topWc = GetTopWindowContext();
+ if (topWc && !topWc->IsDiscarded()) {
+ MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(false));
+ }
+}
+
+void CanonicalBrowsingContext::HistoryCommitIndexAndLength() {
+ nsID changeID = {};
+ CallerWillNotifyHistoryIndexAndLengthChanges caller(nullptr);
+ HistoryCommitIndexAndLength(changeID, caller);
+}
+void CanonicalBrowsingContext::HistoryCommitIndexAndLength(
+ const nsID& aChangeID,
+ const CallerWillNotifyHistoryIndexAndLengthChanges& aProofOfCaller) {
+ if (!IsTop()) {
+ Cast(Top())->HistoryCommitIndexAndLength(aChangeID, aProofOfCaller);
+ return;
+ }
+
+ nsISHistory* shistory = GetSessionHistory();
+ if (!shistory) {
+ return;
+ }
+ int32_t index = 0;
+ shistory->GetIndex(&index);
+ int32_t length = shistory->GetCount();
+
+ GetChildSessionHistory()->SetIndexAndLength(index, length, aChangeID);
+
+ shistory->EvictOutOfRangeDocumentViewers(index);
+
+ Group()->EachParent([&](ContentParent* aParent) {
+ Unused << aParent->SendHistoryCommitIndexAndLength(this, index, length,
+ aChangeID);
+ });
+}
+
+void CanonicalBrowsingContext::SynchronizeLayoutHistoryState() {
+ if (mActiveEntry) {
+ if (IsInProcess()) {
+ nsIDocShell* docShell = GetDocShell();
+ if (docShell) {
+ docShell->PersistLayoutHistoryState();
+
+ nsCOMPtr<nsILayoutHistoryState> state;
+ docShell->GetLayoutHistoryState(getter_AddRefs(state));
+ if (state) {
+ mActiveEntry->SetLayoutHistoryState(state);
+ }
+ }
+ } else if (ContentParent* cp = GetContentParent()) {
+ cp->SendGetLayoutHistoryState(this)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [activeEntry = mActiveEntry](
+ const std::tuple<RefPtr<nsILayoutHistoryState>, Maybe<Wireframe>>&
+ aResult) {
+ if (std::get<0>(aResult)) {
+ activeEntry->SetLayoutHistoryState(std::get<0>(aResult));
+ }
+ if (std::get<1>(aResult)) {
+ activeEntry->SetWireframe(std::get<1>(aResult));
+ }
+ },
+ []() {});
+ }
+ }
+}
+
+void CanonicalBrowsingContext::ResetScalingZoom() {
+ // This currently only ever gets called in the parent process, and we
+ // pass the message on to the WindowGlobalChild for the rootmost browsing
+ // context.
+ if (WindowGlobalParent* topWindow = GetTopWindowContext()) {
+ Unused << topWindow->SendResetScalingZoom();
+ }
+}
+
+void CanonicalBrowsingContext::SetRestoreData(SessionStoreRestoreData* aData,
+ ErrorResult& aError) {
+ MOZ_DIAGNOSTIC_ASSERT(aData);
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
+ RefPtr<Promise> promise = Promise::Create(global, aError);
+ if (aError.Failed()) {
+ return;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(SetHasRestoreData(true)))) {
+ aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ mRestoreState = new RestoreState();
+ mRestoreState->mData = aData;
+ mRestoreState->mPromise = promise;
+}
+
+already_AddRefed<Promise> CanonicalBrowsingContext::GetRestorePromise() {
+ if (mRestoreState) {
+ return do_AddRef(mRestoreState->mPromise);
+ }
+ return nullptr;
+}
+
+void CanonicalBrowsingContext::ClearRestoreState() {
+ if (!mRestoreState) {
+ MOZ_DIAGNOSTIC_ASSERT(!GetHasRestoreData());
+ return;
+ }
+ if (mRestoreState->mPromise) {
+ mRestoreState->mPromise->MaybeRejectWithUndefined();
+ }
+ mRestoreState = nullptr;
+ MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false));
+}
+
+void CanonicalBrowsingContext::RequestRestoreTabContent(
+ WindowGlobalParent* aWindow) {
+ MOZ_DIAGNOSTIC_ASSERT(IsTop());
+
+ if (IsDiscarded() || !mRestoreState || !mRestoreState->mData) {
+ return;
+ }
+
+ CanonicalBrowsingContext* context = aWindow->GetBrowsingContext();
+ MOZ_DIAGNOSTIC_ASSERT(!context->IsDiscarded());
+
+ RefPtr<SessionStoreRestoreData> data =
+ mRestoreState->mData->FindDataForChild(context);
+
+ if (context->IsTop()) {
+ MOZ_DIAGNOSTIC_ASSERT(context == this);
+
+ // We need to wait until the appropriate load event has fired before we
+ // can "complete" the restore process, so if we're holding an empty data
+ // object, just resolve the promise immediately.
+ if (mRestoreState->mData->IsEmpty()) {
+ MOZ_DIAGNOSTIC_ASSERT(!data || data->IsEmpty());
+ mRestoreState->Resolve();
+ ClearRestoreState();
+ return;
+ }
+
+ // Since we're following load event order, we'll only arrive here for a
+ // toplevel context after we've already sent down data for all child frames,
+ // so it's safe to clear this reference now. The completion callback below
+ // relies on the mData field being null to determine if all requests have
+ // been sent out.
+ mRestoreState->ClearData();
+ MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false));
+ }
+
+ if (data && !data->IsEmpty()) {
+ auto onTabRestoreComplete = [self = RefPtr{this},
+ state = RefPtr{mRestoreState}](auto) {
+ state->mResolves++;
+ if (!state->mData && state->mRequests == state->mResolves) {
+ state->Resolve();
+ if (state == self->mRestoreState) {
+ self->ClearRestoreState();
+ }
+ }
+ };
+
+ mRestoreState->mRequests++;
+
+ if (data->CanRestoreInto(aWindow->GetDocumentURI())) {
+ if (!aWindow->IsInProcess()) {
+ aWindow->SendRestoreTabContent(WrapNotNull(data.get()),
+ onTabRestoreComplete,
+ onTabRestoreComplete);
+ return;
+ }
+ data->RestoreInto(context);
+ }
+
+ // This must be called both when we're doing an in-process restore, and when
+ // we didn't do a restore at all due to a URL mismatch.
+ onTabRestoreComplete(true);
+ }
+}
+
+void CanonicalBrowsingContext::RestoreState::Resolve() {
+ MOZ_DIAGNOSTIC_ASSERT(mPromise);
+ mPromise->MaybeResolveWithUndefined();
+ mPromise = nullptr;
+}
+
+nsresult CanonicalBrowsingContext::WriteSessionStorageToSessionStore(
+ const nsTArray<SSCacheCopy>& aSesssionStorage, uint32_t aEpoch) {
+ nsCOMPtr<nsISessionStoreFunctions> funcs = do_ImportESModule(
+ "resource://gre/modules/SessionStoreFunctions.sys.mjs", fallible);
+ if (!funcs) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(funcs);
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(wrapped->GetJSObjectGlobal())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JS::Value> key(jsapi.cx(), Top()->PermanentKey());
+
+ Record<nsCString, Record<nsString, nsString>> storage;
+ JS::Rooted<JS::Value> update(jsapi.cx());
+
+ if (!aSesssionStorage.IsEmpty()) {
+ SessionStoreUtils::ConstructSessionStorageValues(this, aSesssionStorage,
+ storage);
+ if (!ToJSValue(jsapi.cx(), storage, &update)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ update.setNull();
+ }
+
+ return funcs->UpdateSessionStoreForStorage(Top()->GetEmbedderElement(), this,
+ key, aEpoch, update);
+}
+
+void CanonicalBrowsingContext::UpdateSessionStoreSessionStorage(
+ const std::function<void()>& aDone) {
+ if (!StaticPrefs::browser_sessionstore_collect_session_storage_AtStartup()) {
+ aDone();
+ return;
+ }
+
+ using DataPromise = BackgroundSessionStorageManager::DataPromise;
+ BackgroundSessionStorageManager::GetData(
+ this, StaticPrefs::browser_sessionstore_dom_storage_limit(),
+ /* aClearSessionStoreTimer = */ true)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, aDone, epoch = GetSessionStoreEpoch()](
+ const DataPromise::ResolveOrRejectValue& valueList) {
+ if (valueList.IsResolve()) {
+ self->WriteSessionStorageToSessionStore(
+ valueList.ResolveValue(), epoch);
+ }
+ aDone();
+ });
+}
+
+/* static */
+void CanonicalBrowsingContext::UpdateSessionStoreForStorage(
+ uint64_t aBrowsingContextId) {
+ RefPtr<CanonicalBrowsingContext> browsingContext = Get(aBrowsingContextId);
+
+ if (!browsingContext) {
+ return;
+ }
+
+ browsingContext->UpdateSessionStoreSessionStorage([]() {});
+}
+
+void CanonicalBrowsingContext::MaybeScheduleSessionStoreUpdate() {
+ if (!StaticPrefs::browser_sessionstore_platform_collection_AtStartup()) {
+ return;
+ }
+
+ if (!IsTop()) {
+ Top()->MaybeScheduleSessionStoreUpdate();
+ return;
+ }
+
+ if (IsInBFCache()) {
+ return;
+ }
+
+ if (mSessionStoreSessionStorageUpdateTimer) {
+ return;
+ }
+
+ if (!StaticPrefs::browser_sessionstore_debug_no_auto_updates()) {
+ auto result = NS_NewTimerWithFuncCallback(
+ [](nsITimer*, void* aClosure) {
+ auto* context = static_cast<CanonicalBrowsingContext*>(aClosure);
+ context->UpdateSessionStoreSessionStorage([]() {});
+ },
+ this, StaticPrefs::browser_sessionstore_interval(),
+ nsITimer::TYPE_ONE_SHOT,
+ "CanonicalBrowsingContext::MaybeScheduleSessionStoreUpdate");
+
+ if (result.isErr()) {
+ return;
+ }
+
+ mSessionStoreSessionStorageUpdateTimer = result.unwrap();
+ }
+}
+
+void CanonicalBrowsingContext::CancelSessionStoreUpdate() {
+ if (mSessionStoreSessionStorageUpdateTimer) {
+ mSessionStoreSessionStorageUpdateTimer->Cancel();
+ mSessionStoreSessionStorageUpdateTimer = nullptr;
+ }
+}
+
+void CanonicalBrowsingContext::SetContainerFeaturePolicy(
+ FeaturePolicy* aContainerFeaturePolicy) {
+ mContainerFeaturePolicy = aContainerFeaturePolicy;
+
+ if (WindowGlobalParent* current = GetCurrentWindowGlobal()) {
+ Unused << current->SendSetContainerFeaturePolicy(mContainerFeaturePolicy);
+ }
+}
+
+void CanonicalBrowsingContext::SetCrossGroupOpenerId(uint64_t aOpenerId) {
+ MOZ_DIAGNOSTIC_ASSERT(IsTopContent());
+ MOZ_DIAGNOSTIC_ASSERT(mCrossGroupOpenerId == 0,
+ "Can only set CrossGroupOpenerId once");
+ mCrossGroupOpenerId = aOpenerId;
+}
+
+void CanonicalBrowsingContext::SetCrossGroupOpener(
+ CanonicalBrowsingContext& aCrossGroupOpener, ErrorResult& aRv) {
+ if (!IsTopContent()) {
+ aRv.ThrowNotAllowedError(
+ "Can only set crossGroupOpener on toplevel content");
+ return;
+ }
+ if (mCrossGroupOpenerId != 0) {
+ aRv.ThrowNotAllowedError("Can only set crossGroupOpener once");
+ return;
+ }
+
+ SetCrossGroupOpenerId(aCrossGroupOpener.Id());
+}
+
+auto CanonicalBrowsingContext::FindUnloadingHost(uint64_t aChildID)
+ -> nsTArray<UnloadingHost>::iterator {
+ return std::find_if(
+ mUnloadingHosts.begin(), mUnloadingHosts.end(),
+ [&](const auto& host) { return host.mChildID == aChildID; });
+}
+
+void CanonicalBrowsingContext::ClearUnloadingHost(uint64_t aChildID) {
+ // Notify any callbacks which were waiting for the host to finish unloading
+ // that it has.
+ auto found = FindUnloadingHost(aChildID);
+ if (found != mUnloadingHosts.end()) {
+ auto callbacks = std::move(found->mCallbacks);
+ mUnloadingHosts.RemoveElementAt(found);
+ for (const auto& callback : callbacks) {
+ callback();
+ }
+ }
+}
+
+void CanonicalBrowsingContext::StartUnloadingHost(uint64_t aChildID) {
+ MOZ_DIAGNOSTIC_ASSERT(FindUnloadingHost(aChildID) == mUnloadingHosts.end());
+ mUnloadingHosts.AppendElement(UnloadingHost{aChildID, {}});
+}
+
+void CanonicalBrowsingContext::BrowserParentDestroyed(
+ BrowserParent* aBrowserParent, bool aAbnormalShutdown) {
+ ClearUnloadingHost(aBrowserParent->Manager()->ChildID());
+
+ // Handling specific to when the current BrowserParent has been destroyed.
+ if (mCurrentBrowserParent == aBrowserParent) {
+ mCurrentBrowserParent = nullptr;
+
+ // If this BrowserParent is for a subframe, attempt to recover from a
+ // subframe crash by rendering the subframe crashed page in the embedding
+ // content.
+ if (aAbnormalShutdown) {
+ ShowSubframeCrashedUI(aBrowserParent->GetBrowserBridgeParent());
+ }
+ }
+}
+
+void CanonicalBrowsingContext::ShowSubframeCrashedUI(
+ BrowserBridgeParent* aBridge) {
+ if (!aBridge || IsDiscarded() || !aBridge->CanSend()) {
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!aBridge->GetBrowsingContext() ||
+ aBridge->GetBrowsingContext() == this);
+
+ // There is no longer a current inner window within this
+ // BrowsingContext, update the `CurrentInnerWindowId` field to reflect
+ // this.
+ MOZ_ALWAYS_SUCCEEDS(SetCurrentInnerWindowId(0));
+
+ // The owning process will now be the embedder to render the subframe
+ // crashed page, switch ownership back over.
+ SetOwnerProcessId(aBridge->Manager()->Manager()->ChildID());
+ SetCurrentBrowserParent(aBridge->Manager());
+
+ Unused << aBridge->SendSubFrameCrashed();
+}
+
+static void LogBFCacheBlockingForDoc(BrowsingContext* aBrowsingContext,
+ uint32_t aBFCacheCombo, bool aIsSubDoc) {
+ if (aIsSubDoc) {
+ nsAutoCString uri("[no uri]");
+ nsCOMPtr<nsIURI> currentURI =
+ aBrowsingContext->Canonical()->GetCurrentURI();
+ if (currentURI) {
+ uri = currentURI->GetSpecOrDefault();
+ }
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ (" ** Blocked for document %s", uri.get()));
+ }
+ if (aBFCacheCombo & BFCacheStatus::EVENT_HANDLING_SUPPRESSED) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ (" * event handling suppression"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::SUSPENDED) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * suspended Window"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::UNLOAD_LISTENER) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * unload listener"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::REQUEST) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * requests in the loadgroup"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::ACTIVE_GET_USER_MEDIA) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * GetUserMedia"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::ACTIVE_PEER_CONNECTION) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * PeerConnection"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::CONTAINS_EME_CONTENT) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * EME content"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::CONTAINS_MSE_CONTENT) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * MSE use"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * Speech use"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::HAS_USED_VR) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * used VR"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::BEFOREUNLOAD_LISTENER) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * beforeunload listener"));
+ }
+ if (aBFCacheCombo & BFCacheStatus::ACTIVE_LOCK) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * has active Web Locks"));
+ }
+}
+
+bool CanonicalBrowsingContext::AllowedInBFCache(
+ const Maybe<uint64_t>& aChannelId, nsIURI* aNewURI) {
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Debug))) {
+ nsAutoCString uri("[no uri]");
+ nsCOMPtr<nsIURI> currentURI = GetCurrentURI();
+ if (currentURI) {
+ uri = currentURI->GetSpecOrDefault();
+ }
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, ("Checking %s", uri.get()));
+ }
+
+ if (IsInProcess()) {
+ return false;
+ }
+
+ uint32_t bfcacheCombo = 0;
+ if (mRestoreState) {
+ bfcacheCombo |= BFCacheStatus::RESTORING;
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * during session restore"));
+ }
+
+ if (Group()->Toplevels().Length() > 1) {
+ bfcacheCombo |= BFCacheStatus::NOT_ONLY_TOPLEVEL_IN_BCG;
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ (" * auxiliary BrowsingContexts"));
+ }
+
+ // There are not a lot of about:* pages that are allowed to load in
+ // subframes, so it's OK to allow those few about:* pages enter BFCache.
+ MOZ_ASSERT(IsTop(), "Trying to put a non top level BC into BFCache");
+
+ WindowGlobalParent* wgp = GetCurrentWindowGlobal();
+ if (wgp && wgp->GetDocumentURI()) {
+ nsCOMPtr<nsIURI> currentURI = wgp->GetDocumentURI();
+ // Exempt about:* pages from bfcache, with the exception of about:blank
+ if (currentURI->SchemeIs("about") &&
+ !currentURI->GetSpecOrDefault().EqualsLiteral("about:blank")) {
+ bfcacheCombo |= BFCacheStatus::ABOUT_PAGE;
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * about:* page"));
+ }
+
+ if (aNewURI) {
+ bool equalUri = false;
+ aNewURI->Equals(currentURI, &equalUri);
+ if (equalUri) {
+ // When loading the same uri, disable bfcache so that
+ // nsDocShell::OnNewURI transforms the load to LOAD_NORMAL_REPLACE.
+ return false;
+ }
+ }
+ }
+
+ // For telemetry we're collecting all the flags for all the BCs hanging
+ // from this top-level BC.
+ PreOrderWalk([&](BrowsingContext* aBrowsingContext) {
+ WindowGlobalParent* wgp =
+ aBrowsingContext->Canonical()->GetCurrentWindowGlobal();
+ uint32_t subDocBFCacheCombo = wgp ? wgp->GetBFCacheStatus() : 0;
+ if (wgp) {
+ const Maybe<uint64_t>& singleChannelId = wgp->GetSingleChannelId();
+ if (singleChannelId.isSome()) {
+ if (singleChannelId.value() == 0 || aChannelId.isNothing() ||
+ singleChannelId.value() != aChannelId.value()) {
+ subDocBFCacheCombo |= BFCacheStatus::REQUEST;
+ }
+ }
+ }
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Debug))) {
+ LogBFCacheBlockingForDoc(aBrowsingContext, subDocBFCacheCombo,
+ aBrowsingContext != this);
+ }
+
+ bfcacheCombo |= subDocBFCacheCombo;
+ });
+
+ nsDocShell::ReportBFCacheComboTelemetry(bfcacheCombo);
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Debug))) {
+ nsAutoCString uri("[no uri]");
+ nsCOMPtr<nsIURI> currentURI = GetCurrentURI();
+ if (currentURI) {
+ uri = currentURI->GetSpecOrDefault();
+ }
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ (" +> %s %s be blocked from going into the BFCache", uri.get(),
+ bfcacheCombo == 0 ? "shouldn't" : "should"));
+ }
+
+ if (StaticPrefs::docshell_shistory_bfcache_allow_unload_listeners()) {
+ bfcacheCombo &= ~BFCacheStatus::UNLOAD_LISTENER;
+ }
+
+ return bfcacheCombo == 0;
+}
+
+void CanonicalBrowsingContext::SetTouchEventsOverride(
+ dom::TouchEventsOverride aOverride, ErrorResult& aRv) {
+ SetTouchEventsOverrideInternal(aOverride, aRv);
+}
+
+void CanonicalBrowsingContext::SetTargetTopLevelLinkClicksToBlank(
+ bool aTargetTopLevelLinkClicksToBlank, ErrorResult& aRv) {
+ SetTargetTopLevelLinkClicksToBlankInternal(aTargetTopLevelLinkClicksToBlank,
+ aRv);
+}
+
+void CanonicalBrowsingContext::AddPageAwakeRequest() {
+ MOZ_ASSERT(IsTop());
+ auto count = GetPageAwakeRequestCount();
+ MOZ_ASSERT(count < UINT32_MAX);
+ Unused << SetPageAwakeRequestCount(++count);
+}
+
+void CanonicalBrowsingContext::RemovePageAwakeRequest() {
+ MOZ_ASSERT(IsTop());
+ auto count = GetPageAwakeRequestCount();
+ MOZ_ASSERT(count > 0);
+ Unused << SetPageAwakeRequestCount(--count);
+}
+
+void CanonicalBrowsingContext::CloneDocumentTreeInto(
+ CanonicalBrowsingContext* aSource, const nsACString& aRemoteType,
+ embedding::PrintData&& aPrintData) {
+ NavigationIsolationOptions options;
+ options.mRemoteType = aRemoteType;
+
+ mClonePromise =
+ ChangeRemoteness(options, /* aPendingSwitchId = */ 0)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [source = MaybeDiscardedBrowsingContext{aSource},
+ data = std::move(aPrintData)](
+ BrowserParent* aBp) -> RefPtr<GenericNonExclusivePromise> {
+ RefPtr<BrowserBridgeParent> bridge =
+ aBp->GetBrowserBridgeParent();
+ return aBp->SendCloneDocumentTreeIntoSelf(source, data)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [bridge](
+ BrowserParent::CloneDocumentTreeIntoSelfPromise::
+ ResolveOrRejectValue&& aValue) {
+ // We're cloning a remote iframe, so we created a
+ // BrowserBridge which makes us register an OOP load
+ // (see Document::OOPChildLoadStarted), even though
+ // this isn't a real load. We call
+ // SendMaybeFireEmbedderLoadEvents here so that we do
+ // register the end of the load (see
+ // Document::OOPChildLoadDone).
+ if (bridge) {
+ Unused << bridge->SendMaybeFireEmbedderLoadEvents(
+ EmbedderElementEventType::NoEvent);
+ }
+ if (aValue.IsResolve() && aValue.ResolveValue()) {
+ return GenericNonExclusivePromise::CreateAndResolve(
+ true, __func__);
+ }
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_FAILURE, __func__);
+ });
+ },
+ [](nsresult aRv) -> RefPtr<GenericNonExclusivePromise> {
+ NS_WARNING(
+ nsPrintfCString("Remote clone failed: %x\n", unsigned(aRv))
+ .get());
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_FAILURE, __func__);
+ });
+
+ mClonePromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr{this}]() { self->mClonePromise = nullptr; });
+}
+
+bool CanonicalBrowsingContext::StartApzAutoscroll(float aAnchorX,
+ float aAnchorY,
+ nsViewID aScrollId,
+ uint32_t aPresShellId) {
+ nsCOMPtr<nsIWidget> widget;
+ mozilla::layers::LayersId layersId{0};
+
+ if (IsInProcess()) {
+ nsCOMPtr<nsPIDOMWindowOuter> outer = GetDOMWindow();
+ if (!outer) {
+ return false;
+ }
+
+ widget = widget::WidgetUtils::DOMWindowToWidget(outer);
+ if (widget) {
+ layersId = widget->GetRootLayerTreeId();
+ }
+ } else {
+ RefPtr<BrowserParent> parent = GetBrowserParent();
+ if (!parent) {
+ return false;
+ }
+
+ widget = parent->GetWidget();
+ layersId = parent->GetLayersId();
+ }
+
+ if (!widget || !widget->AsyncPanZoomEnabled()) {
+ return false;
+ }
+
+ // The anchor coordinates that are passed in are relative to the origin of the
+ // screen, but we are sending them to APZ which only knows about coordinates
+ // relative to the widget, so convert them accordingly.
+ const LayoutDeviceIntPoint anchor =
+ RoundedToInt(LayoutDevicePoint(aAnchorX, aAnchorY)) -
+ widget->WidgetToScreenOffset();
+
+ mozilla::layers::ScrollableLayerGuid guid(layersId, aPresShellId, aScrollId);
+
+ return widget->StartAsyncAutoscroll(
+ ViewAs<ScreenPixel>(
+ anchor, PixelCastJustification::LayoutDeviceIsScreenForBounds),
+ guid);
+}
+
+void CanonicalBrowsingContext::StopApzAutoscroll(nsViewID aScrollId,
+ uint32_t aPresShellId) {
+ nsCOMPtr<nsIWidget> widget;
+ mozilla::layers::LayersId layersId{0};
+
+ if (IsInProcess()) {
+ nsCOMPtr<nsPIDOMWindowOuter> outer = GetDOMWindow();
+ if (!outer) {
+ return;
+ }
+
+ widget = widget::WidgetUtils::DOMWindowToWidget(outer);
+ if (widget) {
+ layersId = widget->GetRootLayerTreeId();
+ }
+ } else {
+ RefPtr<BrowserParent> parent = GetBrowserParent();
+ if (!parent) {
+ return;
+ }
+
+ widget = parent->GetWidget();
+ layersId = parent->GetLayersId();
+ }
+
+ if (!widget || !widget->AsyncPanZoomEnabled()) {
+ return;
+ }
+
+ mozilla::layers::ScrollableLayerGuid guid(layersId, aPresShellId, aScrollId);
+ widget->StopAsyncAutoscroll(guid);
+}
+
+already_AddRefed<nsISHEntry>
+CanonicalBrowsingContext::GetMostRecentLoadingSessionHistoryEntry() {
+ if (mLoadingEntries.IsEmpty()) {
+ return nullptr;
+ }
+
+ RefPtr<SessionHistoryEntry> entry = mLoadingEntries.LastElement().mEntry;
+ return entry.forget();
+}
+
+already_AddRefed<BounceTrackingState>
+CanonicalBrowsingContext::GetBounceTrackingState() {
+ if (!mWebProgress) {
+ return nullptr;
+ }
+ return mWebProgress->GetBounceTrackingState();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(CanonicalBrowsingContext)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(CanonicalBrowsingContext,
+ BrowsingContext)
+ tmp->mPermanentKey.setNull();
+ if (tmp->mSessionHistory) {
+ tmp->mSessionHistory->SetBrowsingContext(nullptr);
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSessionHistory, mContainerFeaturePolicy,
+ mCurrentBrowserParent, mWebProgress,
+ mSessionStoreSessionStorageUpdateTimer)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(CanonicalBrowsingContext,
+ BrowsingContext)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSessionHistory, mContainerFeaturePolicy,
+ mCurrentBrowserParent, mWebProgress,
+ mSessionStoreSessionStorageUpdateTimer)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(CanonicalBrowsingContext,
+ BrowsingContext)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPermanentKey)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ADDREF_INHERITED(CanonicalBrowsingContext, BrowsingContext)
+NS_IMPL_RELEASE_INHERITED(CanonicalBrowsingContext, BrowsingContext)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanonicalBrowsingContext)
+NS_INTERFACE_MAP_END_INHERITING(BrowsingContext)
+
+} // namespace mozilla::dom
diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h
new file mode 100644
index 0000000000..132c9f2157
--- /dev/null
+++ b/docshell/base/CanonicalBrowsingContext.h
@@ -0,0 +1,620 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CanonicalBrowsingContext_h
+#define mozilla_dom_CanonicalBrowsingContext_h
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/MediaControlKeySource.h"
+#include "mozilla/dom/BrowsingContextWebProgress.h"
+#include "mozilla/dom/ProcessIsolation.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/SessionHistoryEntry.h"
+#include "mozilla/dom/SessionStoreRestoreData.h"
+#include "mozilla/dom/SessionStoreUtils.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/MozPromise.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsISecureBrowserUI.h"
+
+class nsIBrowserDOMWindow;
+class nsISHistory;
+class nsIWidget;
+class nsIPrintSettings;
+class nsSHistory;
+class nsBrowserStatusFilter;
+class nsSecureBrowserUI;
+class CallerWillNotifyHistoryIndexAndLengthChanges;
+class nsITimer;
+
+namespace mozilla {
+enum class CallState;
+class BounceTrackingState;
+
+namespace embedding {
+class PrintData;
+}
+
+namespace net {
+class DocumentLoadListener;
+}
+
+namespace dom {
+
+class BrowserParent;
+class BrowserBridgeParent;
+class FeaturePolicy;
+struct LoadURIOptions;
+class MediaController;
+struct LoadingSessionHistoryInfo;
+class SSCacheCopy;
+class WindowGlobalParent;
+class SessionStoreFormData;
+class SessionStoreScrollData;
+
+// CanonicalBrowsingContext is a BrowsingContext living in the parent
+// process, with whatever extra data that a BrowsingContext in the
+// parent needs.
+class CanonicalBrowsingContext final : public BrowsingContext {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(
+ CanonicalBrowsingContext, BrowsingContext)
+
+ static already_AddRefed<CanonicalBrowsingContext> Get(uint64_t aId);
+ static CanonicalBrowsingContext* Cast(BrowsingContext* aContext);
+ static const CanonicalBrowsingContext* Cast(const BrowsingContext* aContext);
+ static already_AddRefed<CanonicalBrowsingContext> Cast(
+ already_AddRefed<BrowsingContext>&& aContext);
+
+ bool IsOwnedByProcess(uint64_t aProcessId) const {
+ return mProcessId == aProcessId;
+ }
+ bool IsEmbeddedInProcess(uint64_t aProcessId) const {
+ return mEmbedderProcessId == aProcessId;
+ }
+ uint64_t OwnerProcessId() const { return mProcessId; }
+ uint64_t EmbedderProcessId() const { return mEmbedderProcessId; }
+ ContentParent* GetContentParent() const;
+
+ void GetCurrentRemoteType(nsACString& aRemoteType, ErrorResult& aRv) const;
+
+ void SetOwnerProcessId(uint64_t aProcessId);
+
+ // The ID of the BrowsingContext which caused this BrowsingContext to be
+ // opened, or `0` if this is unknown.
+ // Only set for toplevel content BrowsingContexts, and may be from a different
+ // BrowsingContextGroup.
+ uint64_t GetCrossGroupOpenerId() const { return mCrossGroupOpenerId; }
+ void SetCrossGroupOpenerId(uint64_t aOpenerId);
+ void SetCrossGroupOpener(CanonicalBrowsingContext& aCrossGroupOpener,
+ ErrorResult& aRv);
+
+ void GetWindowGlobals(nsTArray<RefPtr<WindowGlobalParent>>& aWindows);
+
+ // The current active WindowGlobal.
+ WindowGlobalParent* GetCurrentWindowGlobal() const;
+
+ // Same as the methods on `BrowsingContext`, but with the types already cast
+ // to the parent process type.
+ CanonicalBrowsingContext* GetParent() {
+ return Cast(BrowsingContext::GetParent());
+ }
+ CanonicalBrowsingContext* Top() { return Cast(BrowsingContext::Top()); }
+ WindowGlobalParent* GetParentWindowContext();
+ WindowGlobalParent* GetTopWindowContext();
+
+ already_AddRefed<nsIWidget> GetParentProcessWidgetContaining();
+ already_AddRefed<nsIBrowserDOMWindow> GetBrowserDOMWindow();
+
+ // Same as `GetParentWindowContext`, but will also cross <browser> and
+ // content/chrome boundaries.
+ already_AddRefed<WindowGlobalParent> GetEmbedderWindowGlobal() const;
+
+ CanonicalBrowsingContext* GetParentCrossChromeBoundary();
+ CanonicalBrowsingContext* TopCrossChromeBoundary();
+ Nullable<WindowProxyHolder> GetTopChromeWindow();
+
+ nsISHistory* GetSessionHistory();
+ SessionHistoryEntry* GetActiveSessionHistoryEntry();
+ void SetActiveSessionHistoryEntry(SessionHistoryEntry* aEntry);
+
+ bool ManuallyManagesActiveness() const;
+
+ UniquePtr<LoadingSessionHistoryInfo> CreateLoadingSessionHistoryEntryForLoad(
+ nsDocShellLoadState* aLoadState, SessionHistoryEntry* aExistingEntry,
+ nsIChannel* aChannel);
+
+ UniquePtr<LoadingSessionHistoryInfo> ReplaceLoadingSessionHistoryEntryForLoad(
+ LoadingSessionHistoryInfo* aInfo, nsIChannel* aNewChannel);
+
+ using PrintPromise = MozPromise</* unused */ bool, nsresult, false>;
+ MOZ_CAN_RUN_SCRIPT RefPtr<PrintPromise> Print(nsIPrintSettings*);
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PrintJS(nsIPrintSettings*,
+ ErrorResult&);
+
+ // Call the given callback on all top-level descendant BrowsingContexts.
+ // Return Callstate::Stop from the callback to stop calling further children.
+ //
+ // If aIncludeNestedBrowsers is true, then all top descendants are included,
+ // even those inside a nested top browser.
+ void CallOnAllTopDescendants(
+ const FunctionRef<CallState(CanonicalBrowsingContext*)>& aCallback,
+ bool aIncludeNestedBrowsers);
+
+ void SessionHistoryCommit(uint64_t aLoadId, const nsID& aChangeID,
+ uint32_t aLoadType, bool aPersist,
+ bool aCloneEntryChildren, bool aChannelExpired,
+ uint32_t aCacheKey);
+
+ // Calls the session history listeners' OnHistoryReload, storing the result in
+ // aCanReload. If aCanReload is set to true and we have an active or a loading
+ // entry then aLoadState will be initialized from that entry, and
+ // aReloadActiveEntry will be true if we have an active entry. If aCanReload
+ // is true and aLoadState and aReloadActiveEntry are not set then we should
+ // attempt to reload based on the current document in the docshell.
+ void NotifyOnHistoryReload(
+ bool aForceReload, bool& aCanReload,
+ Maybe<NotNull<RefPtr<nsDocShellLoadState>>>& aLoadState,
+ Maybe<bool>& aReloadActiveEntry);
+
+ // See BrowsingContext::SetActiveSessionHistoryEntry.
+ void SetActiveSessionHistoryEntry(const Maybe<nsPoint>& aPreviousScrollPos,
+ SessionHistoryInfo* aInfo,
+ uint32_t aLoadType,
+ uint32_t aUpdatedCacheKey,
+ const nsID& aChangeID);
+
+ void ReplaceActiveSessionHistoryEntry(SessionHistoryInfo* aInfo);
+
+ void RemoveDynEntriesFromActiveSessionHistoryEntry();
+
+ void RemoveFromSessionHistory(const nsID& aChangeID);
+
+ Maybe<int32_t> HistoryGo(int32_t aOffset, uint64_t aHistoryEpoch,
+ bool aRequireUserInteraction, bool aUserActivation,
+ Maybe<ContentParentId> aContentId);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Dispatches a wheel zoom change to the embedder element.
+ void DispatchWheelZoomChange(bool aIncrease);
+
+ // This function is used to start the autoplay media which are delayed to
+ // start. If needed, it would also notify the content browsing context which
+ // are related with the canonical browsing content tree to start delayed
+ // autoplay media.
+ void NotifyStartDelayedAutoplayMedia();
+
+ // This function is used to mute or unmute all media within a tab. It would
+ // set the media mute property for the top level window and propagate it to
+ // other top level windows in other processes.
+ void NotifyMediaMutedChanged(bool aMuted, ErrorResult& aRv);
+
+ // Return the number of unique site origins by iterating all given BCs,
+ // including their subtrees.
+ static uint32_t CountSiteOrigins(
+ GlobalObject& aGlobal,
+ const Sequence<mozilla::OwningNonNull<BrowsingContext>>& aRoots);
+
+ // Return true if a private browsing session is active.
+ static bool IsPrivateBrowsingActive();
+
+ // This function would propogate the action to its all child browsing contexts
+ // in content processes.
+ void UpdateMediaControlAction(const MediaControlAction& aAction);
+
+ // Triggers a load in the process
+ using BrowsingContext::LoadURI;
+ void FixupAndLoadURIString(const nsAString& aURI,
+ const LoadURIOptions& aOptions,
+ ErrorResult& aError);
+ void LoadURI(nsIURI* aURI, const LoadURIOptions& aOptions,
+ ErrorResult& aError);
+
+ void GoBack(const Optional<int32_t>& aCancelContentJSEpoch,
+ bool aRequireUserInteraction, bool aUserActivation);
+ void GoForward(const Optional<int32_t>& aCancelContentJSEpoch,
+ bool aRequireUserInteraction, bool aUserActivation);
+ void GoToIndex(int32_t aIndex, const Optional<int32_t>& aCancelContentJSEpoch,
+ bool aUserActivation);
+ void Reload(uint32_t aReloadFlags);
+ void Stop(uint32_t aStopFlags);
+
+ // Get the publicly exposed current URI loaded in this BrowsingContext.
+ already_AddRefed<nsIURI> GetCurrentURI() const;
+ void SetCurrentRemoteURI(nsIURI* aCurrentRemoteURI);
+
+ BrowserParent* GetBrowserParent() const;
+ void SetCurrentBrowserParent(BrowserParent* aBrowserParent);
+
+ // Internal method to change which process a BrowsingContext is being loaded
+ // in. The returned promise will resolve when the process switch is completed.
+ //
+ // A NOT_REMOTE_TYPE aRemoteType argument will perform a process switch into
+ // the parent process, and the method will resolve with a null BrowserParent.
+ using RemotenessPromise = MozPromise<RefPtr<BrowserParent>, nsresult, false>;
+ RefPtr<RemotenessPromise> ChangeRemoteness(
+ const NavigationIsolationOptions& aOptions, uint64_t aPendingSwitchId);
+
+ // Return a media controller from the top-level browsing context that can
+ // control all media belonging to this browsing context tree. Return nullptr
+ // if the top-level browsing context has been discarded.
+ MediaController* GetMediaController();
+ bool HasCreatedMediaController() const;
+
+ // Attempts to start loading the given load state in this BrowsingContext,
+ // without requiring any communication from a docshell. This will handle
+ // computing the right process to load in, and organising handoff to
+ // the right docshell when we get a response.
+ bool LoadInParent(nsDocShellLoadState* aLoadState, bool aSetNavigating);
+
+ // Attempts to start loading the given load state in this BrowsingContext,
+ // in parallel with a DocumentChannelChild being created in the docshell.
+ // Requires the DocumentChannel to connect with this load for it to
+ // complete successfully.
+ bool AttemptSpeculativeLoadInParent(nsDocShellLoadState* aLoadState);
+
+ // Get or create a secure browser UI for this BrowsingContext
+ nsISecureBrowserUI* GetSecureBrowserUI();
+
+ BrowsingContextWebProgress* GetWebProgress() { return mWebProgress; }
+
+ // Called when the current URI changes (from an
+ // nsIWebProgressListener::OnLocationChange event, so that we
+ // can update our security UI for the new location, or when the
+ // mixed content/https-only state for our current window is changed.
+ void UpdateSecurityState();
+
+ void MaybeAddAsProgressListener(nsIWebProgress* aWebProgress);
+
+ // Called when a navigation forces us to recreate our browsing
+ // context (for example, when switching in or out of the parent
+ // process).
+ // aNewContext is the newly created BrowsingContext that is replacing
+ // us.
+ void ReplacedBy(CanonicalBrowsingContext* aNewContext,
+ const NavigationIsolationOptions& aRemotenessOptions);
+
+ bool HasHistoryEntry(nsISHEntry* aEntry);
+ bool HasLoadingHistoryEntry(nsISHEntry* aEntry) {
+ for (const LoadingSessionHistoryEntry& loading : mLoadingEntries) {
+ if (loading.mEntry == aEntry) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void SwapHistoryEntries(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry);
+
+ void AddLoadingSessionHistoryEntry(uint64_t aLoadId,
+ SessionHistoryEntry* aEntry);
+
+ void GetLoadingSessionHistoryInfoFromParent(
+ Maybe<LoadingSessionHistoryInfo>& aLoadingInfo);
+
+ void HistoryCommitIndexAndLength();
+
+ void SynchronizeLayoutHistoryState();
+
+ void ResetScalingZoom();
+
+ void SetContainerFeaturePolicy(FeaturePolicy* aContainerFeaturePolicy);
+ FeaturePolicy* GetContainerFeaturePolicy() const {
+ return mContainerFeaturePolicy;
+ }
+
+ void SetRestoreData(SessionStoreRestoreData* aData, ErrorResult& aError);
+ void ClearRestoreState();
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void RequestRestoreTabContent(
+ WindowGlobalParent* aWindow);
+ already_AddRefed<Promise> GetRestorePromise();
+
+ nsresult WriteSessionStorageToSessionStore(
+ const nsTArray<SSCacheCopy>& aSesssionStorage, uint32_t aEpoch);
+
+ void UpdateSessionStoreSessionStorage(const std::function<void()>& aDone);
+
+ static void UpdateSessionStoreForStorage(uint64_t aBrowsingContextId);
+
+ // Called when a BrowserParent for this BrowsingContext has been fully
+ // destroyed (i.e. `ActorDestroy` was called).
+ void BrowserParentDestroyed(BrowserParent* aBrowserParent,
+ bool aAbnormalShutdown);
+
+ void StartUnloadingHost(uint64_t aChildID);
+ void ClearUnloadingHost(uint64_t aChildID);
+
+ bool AllowedInBFCache(const Maybe<uint64_t>& aChannelId, nsIURI* aNewURI);
+
+ // Methods for getting and setting the active state for top level
+ // browsing contexts, for the process priority manager.
+ bool IsPriorityActive() const {
+ MOZ_RELEASE_ASSERT(IsTop());
+ return mPriorityActive;
+ }
+ void SetPriorityActive(bool aIsActive) {
+ MOZ_RELEASE_ASSERT(IsTop());
+ mPriorityActive = aIsActive;
+ }
+
+ void SetIsActive(bool aIsActive, ErrorResult& aRv) {
+ MOZ_ASSERT(ManuallyManagesActiveness(),
+ "Shouldn't be setting active status of this browsing context if "
+ "not manually managed");
+ SetIsActiveInternal(aIsActive, aRv);
+ }
+
+ void SetIsActiveInternal(bool aIsActive, ErrorResult& aRv) {
+ SetExplicitActive(aIsActive ? ExplicitActiveStatus::Active
+ : ExplicitActiveStatus::Inactive,
+ aRv);
+ }
+
+ void SetTouchEventsOverride(dom::TouchEventsOverride, ErrorResult& aRv);
+ void SetTargetTopLevelLinkClicksToBlank(bool aTargetTopLevelLinkClicksToBlank,
+ ErrorResult& aRv);
+
+ bool IsReplaced() const { return mIsReplaced; }
+
+ const JS::Heap<JS::Value>& PermanentKey() { return mPermanentKey; }
+ void ClearPermanentKey() { mPermanentKey.setNull(); }
+ void MaybeSetPermanentKey(Element* aEmbedder);
+
+ // When request for page awake, it would increase a count that is used to
+ // prevent whole browsing context tree from being suspended. The request can
+ // be called multiple times. When calling the revoke, it would decrease the
+ // count and once the count reaches to zero, the browsing context tree could
+ // be suspended when the tree is inactive.
+ void AddPageAwakeRequest();
+ void RemovePageAwakeRequest();
+
+ void CloneDocumentTreeInto(CanonicalBrowsingContext* aSource,
+ const nsACString& aRemoteType,
+ embedding::PrintData&& aPrintData);
+
+ // Returns a Promise which resolves when cloning documents for printing
+ // finished if this browsing context is cloning document tree.
+ RefPtr<GenericNonExclusivePromise> GetClonePromise() const {
+ return mClonePromise;
+ }
+
+ bool StartApzAutoscroll(float aAnchorX, float aAnchorY, nsViewID aScrollId,
+ uint32_t aPresShellId);
+ void StopApzAutoscroll(nsViewID aScrollId, uint32_t aPresShellId);
+
+ void AddFinalDiscardListener(std::function<void(uint64_t)>&& aListener);
+
+ bool ForceAppWindowActive() const { return mForceAppWindowActive; }
+ void SetForceAppWindowActive(bool, ErrorResult&);
+ void RecomputeAppWindowVisibility();
+
+ already_AddRefed<nsISHEntry> GetMostRecentLoadingSessionHistoryEntry();
+
+ already_AddRefed<BounceTrackingState> GetBounceTrackingState();
+
+ protected:
+ // Called when the browsing context is being discarded.
+ void CanonicalDiscard();
+
+ // Called when the browsing context is being attached.
+ void CanonicalAttach();
+
+ // Called when the browsing context private mode is changed after
+ // being attached, but before being discarded.
+ void AdjustPrivateBrowsingCount(bool aPrivateBrowsing);
+
+ using Type = BrowsingContext::Type;
+ CanonicalBrowsingContext(WindowContext* aParentWindow,
+ BrowsingContextGroup* aGroup,
+ uint64_t aBrowsingContextId,
+ uint64_t aOwnerProcessId,
+ uint64_t aEmbedderProcessId, Type aType,
+ FieldValues&& aInit);
+
+ private:
+ friend class BrowsingContext;
+
+ virtual ~CanonicalBrowsingContext();
+
+ class PendingRemotenessChange {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(PendingRemotenessChange)
+
+ PendingRemotenessChange(CanonicalBrowsingContext* aTarget,
+ RemotenessPromise::Private* aPromise,
+ uint64_t aPendingSwitchId,
+ const NavigationIsolationOptions& aOptions);
+
+ void Cancel(nsresult aRv);
+
+ private:
+ friend class CanonicalBrowsingContext;
+
+ ~PendingRemotenessChange();
+ void ProcessLaunched();
+ void ProcessReady();
+ void MaybeFinish();
+ void Clear();
+
+ nsresult FinishTopContent();
+ nsresult FinishSubframe();
+
+ RefPtr<CanonicalBrowsingContext> mTarget;
+ RefPtr<RemotenessPromise::Private> mPromise;
+ RefPtr<ContentParent> mContentParent;
+ RefPtr<BrowsingContextGroup> mSpecificGroup;
+
+ bool mProcessReady = false;
+ bool mWaitingForPrepareToChange = false;
+
+ uint64_t mPendingSwitchId;
+ NavigationIsolationOptions mOptions;
+ };
+
+ struct RestoreState {
+ NS_INLINE_DECL_REFCOUNTING(RestoreState)
+
+ void ClearData() { mData = nullptr; }
+ void Resolve();
+
+ RefPtr<SessionStoreRestoreData> mData;
+ RefPtr<Promise> mPromise;
+ uint32_t mRequests = 0;
+ uint32_t mResolves = 0;
+
+ private:
+ ~RestoreState() = default;
+ };
+
+ friend class net::DocumentLoadListener;
+ // Called when a DocumentLoadListener is created to start a load for
+ // this browsing context. Returns false if a higher priority load is
+ // already in-progress and the new one has been rejected.
+ bool StartDocumentLoad(net::DocumentLoadListener* aLoad);
+ // Called once DocumentLoadListener completes handling a load, and it
+ // is either complete, or handed off to the final channel to deliver
+ // data to the destination docshell.
+ // If aContinueNavigating it set, the reference to the DocumentLoadListener
+ // will be cleared to prevent it being cancelled, however the current load ID
+ // will be preserved until `EndDocumentLoad` is called again.
+ void EndDocumentLoad(bool aContinueNavigating);
+
+ bool SupportsLoadingInParent(nsDocShellLoadState* aLoadState,
+ uint64_t* aOuterWindowId);
+
+ void HistoryCommitIndexAndLength(
+ const nsID& aChangeID,
+ const CallerWillNotifyHistoryIndexAndLengthChanges& aProofOfCaller);
+
+ struct UnloadingHost {
+ uint64_t mChildID;
+ nsTArray<std::function<void()>> mCallbacks;
+ };
+ nsTArray<UnloadingHost>::iterator FindUnloadingHost(uint64_t aChildID);
+
+ // Called when we want to show the subframe crashed UI as our previous browser
+ // has become unloaded for one reason or another.
+ void ShowSubframeCrashedUI(BrowserBridgeParent* aBridge);
+
+ void MaybeScheduleSessionStoreUpdate();
+
+ void CancelSessionStoreUpdate();
+
+ void AddPendingDiscard();
+
+ void RemovePendingDiscard();
+
+ bool ShouldAddEntryForRefresh(const SessionHistoryEntry* aEntry) {
+ return ShouldAddEntryForRefresh(aEntry->Info().GetURI(),
+ aEntry->Info().HasPostData());
+ }
+ bool ShouldAddEntryForRefresh(nsIURI* aNewURI, bool aHasPostData) {
+ nsCOMPtr<nsIURI> currentURI = GetCurrentURI();
+ return BrowsingContext::ShouldAddEntryForRefresh(currentURI, aNewURI,
+ aHasPostData);
+ }
+
+ already_AddRefed<nsDocShellLoadState> CreateLoadInfo(
+ SessionHistoryEntry* aEntry);
+
+ // XXX(farre): Store a ContentParent pointer here rather than mProcessId?
+ // Indicates which process owns the docshell.
+ uint64_t mProcessId;
+
+ // Indicates which process owns the embedder element.
+ uint64_t mEmbedderProcessId;
+
+ uint64_t mCrossGroupOpenerId = 0;
+
+ // This function will make the top window context reset its
+ // "SHEntryHasUserInteraction" cache that prevents documents from repeatedly
+ // setting user interaction on SH entries. Should be called anytime SH
+ // entries are added or replaced.
+ void ResetSHEntryHasUserInteractionCache();
+
+ RefPtr<BrowserParent> mCurrentBrowserParent;
+
+ nsTArray<UnloadingHost> mUnloadingHosts;
+
+ // The current URI loaded in this BrowsingContext. This value is only set for
+ // BrowsingContexts loaded in content processes.
+ nsCOMPtr<nsIURI> mCurrentRemoteURI;
+
+ // The current remoteness change which is in a pending state.
+ RefPtr<PendingRemotenessChange> mPendingRemotenessChange;
+
+ RefPtr<nsSHistory> mSessionHistory;
+
+ // Tab media controller is used to control all media existing in the same
+ // browsing context tree, so it would only exist in the top level browsing
+ // context.
+ RefPtr<MediaController> mTabMediaController;
+
+ RefPtr<net::DocumentLoadListener> mCurrentLoad;
+
+ struct LoadingSessionHistoryEntry {
+ uint64_t mLoadId = 0;
+ RefPtr<SessionHistoryEntry> mEntry;
+ };
+ nsTArray<LoadingSessionHistoryEntry> mLoadingEntries;
+ RefPtr<SessionHistoryEntry> mActiveEntry;
+
+ RefPtr<nsSecureBrowserUI> mSecureBrowserUI;
+ RefPtr<BrowsingContextWebProgress> mWebProgress;
+
+ nsCOMPtr<nsIWebProgressListener> mDocShellProgressBridge;
+ RefPtr<nsBrowserStatusFilter> mStatusFilter;
+
+ RefPtr<FeaturePolicy> mContainerFeaturePolicy;
+
+ friend class BrowserSessionStore;
+ WeakPtr<SessionStoreFormData>& GetSessionStoreFormDataRef() {
+ return mFormdata;
+ }
+ WeakPtr<SessionStoreScrollData>& GetSessionStoreScrollDataRef() {
+ return mScroll;
+ }
+
+ WeakPtr<SessionStoreFormData> mFormdata;
+ WeakPtr<SessionStoreScrollData> mScroll;
+
+ RefPtr<RestoreState> mRestoreState;
+
+ nsCOMPtr<nsITimer> mSessionStoreSessionStorageUpdateTimer;
+
+ // If this is a top level context, this is true if our browser ID is marked as
+ // active in the process priority manager.
+ bool mPriorityActive = false;
+
+ // See CanonicalBrowsingContext.forceAppWindowActive.
+ bool mForceAppWindowActive = false;
+
+ bool mIsReplaced = false;
+
+ // A Promise created when cloning documents for printing.
+ RefPtr<GenericNonExclusivePromise> mClonePromise;
+
+ JS::Heap<JS::Value> mPermanentKey;
+
+ uint32_t mPendingDiscards = 0;
+
+ bool mFullyDiscarded = false;
+
+ nsTArray<std::function<void(uint64_t)>> mFullyDiscardedListeners;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // !defined(mozilla_dom_CanonicalBrowsingContext_h)
diff --git a/docshell/base/ChildProcessChannelListener.cpp b/docshell/base/ChildProcessChannelListener.cpp
new file mode 100644
index 0000000000..628d75abb1
--- /dev/null
+++ b/docshell/base/ChildProcessChannelListener.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ChildProcessChannelListener.h"
+
+#include "mozilla/ipc/Endpoint.h"
+#include "nsDocShellLoadState.h"
+
+namespace mozilla {
+namespace dom {
+
+static StaticRefPtr<ChildProcessChannelListener> sCPCLSingleton;
+
+void ChildProcessChannelListener::RegisterCallback(uint64_t aIdentifier,
+ Callback&& aCallback) {
+ if (auto args = mChannelArgs.Extract(aIdentifier)) {
+ nsresult rv =
+ aCallback(args->mLoadState, std::move(args->mStreamFilterEndpoints),
+ args->mTiming);
+ args->mResolver(rv);
+ } else {
+ mCallbacks.InsertOrUpdate(aIdentifier, std::move(aCallback));
+ }
+}
+
+void ChildProcessChannelListener::OnChannelReady(
+ nsDocShellLoadState* aLoadState, uint64_t aIdentifier,
+ nsTArray<Endpoint>&& aStreamFilterEndpoints, nsDOMNavigationTiming* aTiming,
+ Resolver&& aResolver) {
+ if (auto callback = mCallbacks.Extract(aIdentifier)) {
+ nsresult rv =
+ (*callback)(aLoadState, std::move(aStreamFilterEndpoints), aTiming);
+ aResolver(rv);
+ } else {
+ mChannelArgs.InsertOrUpdate(
+ aIdentifier, CallbackArgs{aLoadState, std::move(aStreamFilterEndpoints),
+ aTiming, std::move(aResolver)});
+ }
+}
+
+ChildProcessChannelListener::~ChildProcessChannelListener() {
+ for (const auto& args : mChannelArgs.Values()) {
+ args.mResolver(NS_ERROR_FAILURE);
+ }
+}
+
+already_AddRefed<ChildProcessChannelListener>
+ChildProcessChannelListener::GetSingleton() {
+ if (!sCPCLSingleton) {
+ sCPCLSingleton = new ChildProcessChannelListener();
+ ClearOnShutdown(&sCPCLSingleton);
+ }
+ RefPtr<ChildProcessChannelListener> cpcl = sCPCLSingleton;
+ return cpcl.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/docshell/base/ChildProcessChannelListener.h b/docshell/base/ChildProcessChannelListener.h
new file mode 100644
index 0000000000..c00c2ff5a7
--- /dev/null
+++ b/docshell/base/ChildProcessChannelListener.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ChildProcessChannelListener_h
+#define mozilla_dom_ChildProcessChannelListener_h
+
+#include <functional>
+
+#include "mozilla/extensions/StreamFilterParent.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsTHashMap.h"
+#include "nsIChannel.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+
+namespace mozilla::dom {
+
+class ChildProcessChannelListener final {
+ NS_INLINE_DECL_REFCOUNTING(ChildProcessChannelListener)
+
+ using Endpoint = mozilla::ipc::Endpoint<extensions::PStreamFilterParent>;
+ using Resolver = std::function<void(const nsresult&)>;
+ using Callback = std::function<nsresult(
+ nsDocShellLoadState*, nsTArray<Endpoint>&&, nsDOMNavigationTiming*)>;
+
+ void RegisterCallback(uint64_t aIdentifier, Callback&& aCallback);
+
+ void OnChannelReady(nsDocShellLoadState* aLoadState, uint64_t aIdentifier,
+ nsTArray<Endpoint>&& aStreamFilterEndpoints,
+ nsDOMNavigationTiming* aTiming, Resolver&& aResolver);
+
+ static already_AddRefed<ChildProcessChannelListener> GetSingleton();
+
+ private:
+ ChildProcessChannelListener() = default;
+ ~ChildProcessChannelListener();
+ struct CallbackArgs {
+ RefPtr<nsDocShellLoadState> mLoadState;
+ nsTArray<Endpoint> mStreamFilterEndpoints;
+ RefPtr<nsDOMNavigationTiming> mTiming;
+ Resolver mResolver;
+ };
+
+ // TODO Backtrack.
+ nsTHashMap<nsUint64HashKey, Callback> mCallbacks;
+ nsTHashMap<nsUint64HashKey, CallbackArgs> mChannelArgs;
+};
+
+} // namespace mozilla::dom
+
+#endif // !defined(mozilla_dom_ChildProcessChannelListener_h)
diff --git a/docshell/base/IHistory.h b/docshell/base/IHistory.h
new file mode 100644
index 0000000000..30da778f45
--- /dev/null
+++ b/docshell/base/IHistory.h
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_IHistory_h_
+#define mozilla_IHistory_h_
+
+#include "nsISupports.h"
+#include "nsURIHashKey.h"
+#include "nsTHashSet.h"
+#include "nsTObserverArray.h"
+
+class nsIURI;
+class nsIWidget;
+
+namespace mozilla {
+
+namespace dom {
+class ContentParent;
+class Document;
+class Link;
+} // namespace dom
+
+// 0057c9d3-b98e-4933-bdc5-0275d06705e1
+#define IHISTORY_IID \
+ { \
+ 0x0057c9d3, 0xb98e, 0x4933, { \
+ 0xbd, 0xc5, 0x02, 0x75, 0xd0, 0x67, 0x05, 0xe1 \
+ } \
+ }
+
+class IHistory : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(IHISTORY_IID)
+
+ using ContentParentSet = nsTHashSet<RefPtr<dom::ContentParent>>;
+
+ /**
+ * Registers the Link for notifications about the visited-ness of aURI.
+ * Consumers should assume that the URI is unvisited after calling this, and
+ * they will be notified if that state (unvisited) changes by having
+ * VisitedQueryFinished called on themselves. Note that it may call
+ * synchronously if the answer is already known.
+ *
+ * @note VisitedQueryFinished must not call RegisterVisitedCallback or
+ * UnregisterVisitedCallback.
+ *
+ * @pre aURI must not be null.
+ * @pre aLink may be null only in the parent (chrome) process.
+ *
+ * @param aURI
+ * The URI to check.
+ * @param aLink
+ * The link to update whenever the history status changes. The
+ * implementation will only hold onto a raw pointer, so if this
+ * object should be destroyed, be sure to call
+ * UnregisterVistedCallback first.
+ */
+ virtual void RegisterVisitedCallback(nsIURI* aURI, dom::Link* aLink) = 0;
+
+ /**
+ * Schedules a single visited query from a given child process.
+ *
+ * @param aURI the URI to query.
+ * @param ContentParent the process interested in knowing about the visited
+ * state of this URI.
+ */
+ virtual void ScheduleVisitedQuery(nsIURI* aURI, dom::ContentParent*) = 0;
+
+ /**
+ * Unregisters a previously registered Link object. This must be called
+ * before destroying the registered object, and asserts when misused.
+ *
+ * @pre aURI must not be null.
+ * @pre aLink must not be null.
+ *
+ * @param aURI
+ * The URI that aLink was registered for.
+ * @param aLink
+ * The link object to unregister for aURI.
+ */
+ virtual void UnregisterVisitedCallback(nsIURI* aURI, dom::Link* aLink) = 0;
+
+ enum class VisitedStatus : uint8_t {
+ Unknown,
+ Visited,
+ Unvisited,
+ };
+
+ /**
+ * Notifies about the visited status of a given URI. The visited status cannot
+ * be unknown, otherwise there's no point in notifying of anything.
+ *
+ * @param ContentParentSet a set of content processes that are interested on
+ * this change. If null, it is broadcasted to all
+ * child processes.
+ */
+ virtual void NotifyVisited(nsIURI*, VisitedStatus,
+ const ContentParentSet* = nullptr) = 0;
+
+ enum VisitFlags {
+ /**
+ * Indicates whether the URI was loaded in a top-level window.
+ */
+ TOP_LEVEL = 1 << 0,
+ /**
+ * Indicates whether the URI is the target of a permanent redirect.
+ */
+ REDIRECT_PERMANENT = 1 << 1,
+ /**
+ * Indicates whether the URI is the target of a temporary redirect.
+ */
+ REDIRECT_TEMPORARY = 1 << 2,
+ /**
+ * Indicates the URI will redirect (Response code 3xx).
+ */
+ REDIRECT_SOURCE = 1 << 3,
+ /**
+ * Indicates the URI caused an error that is unlikely fixable by a
+ * retry, like a not found or unfetchable page.
+ */
+ UNRECOVERABLE_ERROR = 1 << 4,
+ /**
+ * If REDIRECT_SOURCE is set, this indicates that the redirect is permanent.
+ * Note this differs from REDIRECT_PERMANENT because that one refers to how
+ * we reached the URI, while this is used when the URI itself redirects.
+ */
+ REDIRECT_SOURCE_PERMANENT = 1 << 5,
+ /**
+ * If REDIRECT_SOURCE is set, this indicates that this is a special redirect
+ * caused by HSTS or HTTPS-Only/First upgrading to the HTTPS version of the
+ * URI.
+ */
+ REDIRECT_SOURCE_UPGRADED = 1 << 6
+ };
+
+ /**
+ * Adds a history visit for the URI.
+ *
+ * @pre aURI must not be null.
+ *
+ * @param aWidget
+ * The widget for the DocShell.
+ * @param aURI
+ * The URI of the page being visited.
+ * @param aLastVisitedURI
+ * The URI of the last visit in the chain.
+ * @param aFlags
+ * The VisitFlags describing this visit.
+ * @param aBrowserId
+ * The id of browser used for this visit.
+ */
+ NS_IMETHOD VisitURI(nsIWidget* aWidget, nsIURI* aURI, nsIURI* aLastVisitedURI,
+ uint32_t aFlags, uint64_t aBrowserId) = 0;
+
+ /**
+ * Set the title of the URI.
+ *
+ * @pre aURI must not be null.
+ *
+ * @param aURI
+ * The URI to set the title for.
+ * @param aTitle
+ * The title string.
+ */
+ NS_IMETHOD SetURITitle(nsIURI* aURI, const nsAString& aTitle) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(IHistory, IHISTORY_IID)
+
+} // namespace mozilla
+
+#endif // mozilla_IHistory_h_
diff --git a/docshell/base/LoadContext.cpp b/docshell/base/LoadContext.cpp
new file mode 100644
index 0000000000..2e501be221
--- /dev/null
+++ b/docshell/base/LoadContext.cpp
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Assertions.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/LoadContext.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ScriptSettings.h" // for AutoJSAPI
+#include "mozilla/dom/BrowsingContext.h"
+#include "nsContentUtils.h"
+#include "xpcpublic.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(LoadContext, nsILoadContext, nsIInterfaceRequestor)
+
+LoadContext::LoadContext(const IPC::SerializedLoadContext& aToCopy,
+ dom::Element* aTopFrameElement,
+ OriginAttributes& aAttrs)
+ : mTopFrameElement(do_GetWeakReference(aTopFrameElement)),
+ mIsContent(aToCopy.mIsContent),
+ mUseRemoteTabs(aToCopy.mUseRemoteTabs),
+ mUseRemoteSubframes(aToCopy.mUseRemoteSubframes),
+ mUseTrackingProtection(aToCopy.mUseTrackingProtection),
+#ifdef DEBUG
+ mIsNotNull(aToCopy.mIsNotNull),
+#endif
+ mOriginAttributes(aAttrs) {
+}
+
+LoadContext::LoadContext(OriginAttributes& aAttrs)
+ : mTopFrameElement(nullptr),
+ mIsContent(false),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mUseTrackingProtection(false),
+#ifdef DEBUG
+ mIsNotNull(true),
+#endif
+ mOriginAttributes(aAttrs) {
+}
+
+LoadContext::LoadContext(nsIPrincipal* aPrincipal,
+ nsILoadContext* aOptionalBase)
+ : mTopFrameElement(nullptr),
+ mIsContent(true),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mUseTrackingProtection(false),
+#ifdef DEBUG
+ mIsNotNull(true),
+#endif
+ mOriginAttributes(aPrincipal->OriginAttributesRef()) {
+ if (!aOptionalBase) {
+ return;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(aOptionalBase->GetIsContent(&mIsContent));
+ MOZ_ALWAYS_SUCCEEDS(aOptionalBase->GetUseRemoteTabs(&mUseRemoteTabs));
+ MOZ_ALWAYS_SUCCEEDS(
+ aOptionalBase->GetUseRemoteSubframes(&mUseRemoteSubframes));
+ MOZ_ALWAYS_SUCCEEDS(
+ aOptionalBase->GetUseTrackingProtection(&mUseTrackingProtection));
+}
+
+LoadContext::~LoadContext() = default;
+
+//-----------------------------------------------------------------------------
+// LoadContext::nsILoadContext
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+LoadContext::GetAssociatedWindow(mozIDOMWindowProxy**) {
+ MOZ_ASSERT(mIsNotNull);
+
+ // can't support this in the parent process
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+LoadContext::GetTopWindow(mozIDOMWindowProxy**) {
+ MOZ_ASSERT(mIsNotNull);
+
+ // can't support this in the parent process
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+LoadContext::GetTopFrameElement(dom::Element** aElement) {
+ nsCOMPtr<dom::Element> element = do_QueryReferent(mTopFrameElement);
+ element.forget(aElement);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadContext::GetIsContent(bool* aIsContent) {
+ MOZ_ASSERT(mIsNotNull);
+
+ NS_ENSURE_ARG_POINTER(aIsContent);
+
+ *aIsContent = mIsContent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadContext::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) {
+ MOZ_ASSERT(mIsNotNull);
+
+ NS_ENSURE_ARG_POINTER(aUsePrivateBrowsing);
+
+ *aUsePrivateBrowsing = mOriginAttributes.mPrivateBrowsingId > 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadContext::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) {
+ MOZ_ASSERT(mIsNotNull);
+
+ // We shouldn't need this on parent...
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+LoadContext::SetPrivateBrowsing(bool aUsePrivateBrowsing) {
+ MOZ_ASSERT(mIsNotNull);
+
+ // We shouldn't need this on parent...
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+LoadContext::GetUseRemoteTabs(bool* aUseRemoteTabs) {
+ MOZ_ASSERT(mIsNotNull);
+
+ NS_ENSURE_ARG_POINTER(aUseRemoteTabs);
+
+ *aUseRemoteTabs = mUseRemoteTabs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadContext::SetRemoteTabs(bool aUseRemoteTabs) {
+ MOZ_ASSERT(mIsNotNull);
+
+ // We shouldn't need this on parent...
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+LoadContext::GetUseRemoteSubframes(bool* aUseRemoteSubframes) {
+ MOZ_ASSERT(mIsNotNull);
+
+ NS_ENSURE_ARG_POINTER(aUseRemoteSubframes);
+
+ *aUseRemoteSubframes = mUseRemoteSubframes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadContext::SetRemoteSubframes(bool aUseRemoteSubframes) {
+ MOZ_ASSERT(mIsNotNull);
+
+ // We shouldn't need this on parent...
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+LoadContext::GetScriptableOriginAttributes(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aAttrs) {
+ bool ok = ToJSValue(aCx, mOriginAttributes, aAttrs);
+ NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+LoadContext::GetOriginAttributes(mozilla::OriginAttributes& aAttrs) {
+ aAttrs = mOriginAttributes;
+}
+
+NS_IMETHODIMP
+LoadContext::GetUseTrackingProtection(bool* aUseTrackingProtection) {
+ MOZ_ASSERT(mIsNotNull);
+
+ NS_ENSURE_ARG_POINTER(aUseTrackingProtection);
+
+ *aUseTrackingProtection = mUseTrackingProtection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadContext::SetUseTrackingProtection(bool aUseTrackingProtection) {
+ MOZ_ASSERT_UNREACHABLE("Should only be set through nsDocShell");
+
+ return NS_ERROR_UNEXPECTED;
+}
+
+//-----------------------------------------------------------------------------
+// LoadContext::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+NS_IMETHODIMP
+LoadContext::GetInterface(const nsIID& aIID, void** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = nullptr;
+
+ if (aIID.Equals(NS_GET_IID(nsILoadContext))) {
+ *aResult = static_cast<nsILoadContext*>(this);
+ NS_ADDREF_THIS();
+ return NS_OK;
+ }
+
+ return NS_NOINTERFACE;
+}
+
+static already_AddRefed<nsILoadContext> CreateInstance(bool aPrivate) {
+ OriginAttributes oa;
+ oa.mPrivateBrowsingId = aPrivate ? 1 : 0;
+
+ nsCOMPtr<nsILoadContext> lc = new LoadContext(oa);
+
+ return lc.forget();
+}
+
+already_AddRefed<nsILoadContext> CreateLoadContext() {
+ return CreateInstance(false);
+}
+
+already_AddRefed<nsILoadContext> CreatePrivateLoadContext() {
+ return CreateInstance(true);
+}
+
+} // namespace mozilla
diff --git a/docshell/base/LoadContext.h b/docshell/base/LoadContext.h
new file mode 100644
index 0000000000..5cb71ff347
--- /dev/null
+++ b/docshell/base/LoadContext.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef LoadContext_h
+#define LoadContext_h
+
+#include "SerializedLoadContext.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BasePrincipal.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsILoadContext.h"
+
+namespace mozilla::dom {
+class Element;
+}
+
+namespace mozilla {
+
+/**
+ * Class that provides nsILoadContext info in Parent process. Typically copied
+ * from Child via SerializedLoadContext.
+ *
+ * Note: this is not the "normal" or "original" nsILoadContext. That is
+ * typically provided by BrowsingContext. This is only used when the original
+ * docshell is in a different process and we need to copy certain values from
+ * it.
+ */
+
+class LoadContext final : public nsILoadContext, public nsIInterfaceRequestor {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSILOADCONTEXT
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ LoadContext(const IPC::SerializedLoadContext& aToCopy,
+ dom::Element* aTopFrameElement, OriginAttributes& aAttrs);
+
+ // Constructor taking reserved origin attributes.
+ explicit LoadContext(OriginAttributes& aAttrs);
+
+ // Constructor for creating a LoadContext with a given browser flag.
+ explicit LoadContext(nsIPrincipal* aPrincipal,
+ nsILoadContext* aOptionalBase = nullptr);
+
+ private:
+ ~LoadContext();
+
+ nsWeakPtr mTopFrameElement;
+ bool mIsContent;
+ bool mUseRemoteTabs;
+ bool mUseRemoteSubframes;
+ bool mUseTrackingProtection;
+#ifdef DEBUG
+ bool mIsNotNull;
+#endif
+ OriginAttributes mOriginAttributes;
+};
+
+already_AddRefed<nsILoadContext> CreateLoadContext();
+already_AddRefed<nsILoadContext> CreatePrivateLoadContext();
+
+} // namespace mozilla
+
+#endif // LoadContext_h
diff --git a/docshell/base/SerializedLoadContext.cpp b/docshell/base/SerializedLoadContext.cpp
new file mode 100644
index 0000000000..cb598b43fe
--- /dev/null
+++ b/docshell/base/SerializedLoadContext.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SerializedLoadContext.h"
+#include "nsNetUtil.h"
+#include "nsIChannel.h"
+#include "nsILoadContext.h"
+#include "nsIPrivateBrowsingChannel.h"
+#include "nsIWebSocketChannel.h"
+
+namespace IPC {
+
+SerializedLoadContext::SerializedLoadContext(nsILoadContext* aLoadContext)
+ : mIsContent(false),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mUseTrackingProtection(false) {
+ Init(aLoadContext);
+}
+
+SerializedLoadContext::SerializedLoadContext(nsIChannel* aChannel)
+ : mIsContent(false),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mUseTrackingProtection(false) {
+ if (!aChannel) {
+ Init(nullptr);
+ return;
+ }
+
+ nsCOMPtr<nsILoadContext> loadContext;
+ NS_QueryNotificationCallbacks(aChannel, loadContext);
+ Init(loadContext);
+
+ if (!loadContext) {
+ // Attempt to retrieve the private bit from the channel if it has been
+ // overriden.
+ bool isPrivate = false;
+ bool isOverriden = false;
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
+ if (pbChannel &&
+ NS_SUCCEEDED(
+ pbChannel->IsPrivateModeOverriden(&isPrivate, &isOverriden)) &&
+ isOverriden) {
+ mIsPrivateBitValid = true;
+ }
+ mOriginAttributes.SyncAttributesWithPrivateBrowsing(isPrivate);
+ }
+}
+
+SerializedLoadContext::SerializedLoadContext(nsIWebSocketChannel* aChannel)
+ : mIsContent(false),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mUseTrackingProtection(false) {
+ nsCOMPtr<nsILoadContext> loadContext;
+ if (aChannel) {
+ NS_QueryNotificationCallbacks(aChannel, loadContext);
+ }
+ Init(loadContext);
+}
+
+void SerializedLoadContext::Init(nsILoadContext* aLoadContext) {
+ if (aLoadContext) {
+ mIsNotNull = true;
+ mIsPrivateBitValid = true;
+ aLoadContext->GetIsContent(&mIsContent);
+ aLoadContext->GetUseRemoteTabs(&mUseRemoteTabs);
+ aLoadContext->GetUseRemoteSubframes(&mUseRemoteSubframes);
+ aLoadContext->GetUseTrackingProtection(&mUseTrackingProtection);
+ aLoadContext->GetOriginAttributes(mOriginAttributes);
+ } else {
+ mIsNotNull = false;
+ mIsPrivateBitValid = false;
+ // none of below values really matter when mIsNotNull == false:
+ // we won't be GetInterfaced to nsILoadContext
+ mIsContent = true;
+ mUseRemoteTabs = false;
+ mUseRemoteSubframes = false;
+ mUseTrackingProtection = false;
+ }
+}
+
+} // namespace IPC
diff --git a/docshell/base/SerializedLoadContext.h b/docshell/base/SerializedLoadContext.h
new file mode 100644
index 0000000000..d47ad08003
--- /dev/null
+++ b/docshell/base/SerializedLoadContext.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SerializedLoadContext_h
+#define SerializedLoadContext_h
+
+#include "base/basictypes.h"
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/BasePrincipal.h"
+
+class nsILoadContext;
+
+/*
+ * This file contains the IPC::SerializedLoadContext class, which is used to
+ * copy data across IPDL from Child process contexts so it is available in the
+ * Parent.
+ */
+
+class nsIChannel;
+class nsIWebSocketChannel;
+
+namespace IPC {
+
+class SerializedLoadContext {
+ public:
+ SerializedLoadContext()
+ : mIsNotNull(false),
+ mIsPrivateBitValid(false),
+ mIsContent(false),
+ mUseRemoteTabs(false),
+ mUseRemoteSubframes(false),
+ mUseTrackingProtection(false) {
+ Init(nullptr);
+ }
+
+ explicit SerializedLoadContext(nsILoadContext* aLoadContext);
+ explicit SerializedLoadContext(nsIChannel* aChannel);
+ explicit SerializedLoadContext(nsIWebSocketChannel* aChannel);
+
+ void Init(nsILoadContext* aLoadContext);
+
+ bool IsNotNull() const { return mIsNotNull; }
+ bool IsPrivateBitValid() const { return mIsPrivateBitValid; }
+
+ // used to indicate if child-side LoadContext * was null.
+ bool mIsNotNull;
+ // used to indicate if child-side mUsePrivateBrowsing flag is valid, even if
+ // mIsNotNull is false, i.e., child LoadContext was null.
+ bool mIsPrivateBitValid;
+ bool mIsContent;
+ bool mUseRemoteTabs;
+ bool mUseRemoteSubframes;
+ bool mUseTrackingProtection;
+ mozilla::OriginAttributes mOriginAttributes;
+};
+
+// Function to serialize over IPDL
+template <>
+struct ParamTraits<SerializedLoadContext> {
+ typedef SerializedLoadContext paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ nsAutoCString suffix;
+ aParam.mOriginAttributes.CreateSuffix(suffix);
+
+ WriteParam(aWriter, aParam.mIsNotNull);
+ WriteParam(aWriter, aParam.mIsContent);
+ WriteParam(aWriter, aParam.mIsPrivateBitValid);
+ WriteParam(aWriter, aParam.mUseRemoteTabs);
+ WriteParam(aWriter, aParam.mUseRemoteSubframes);
+ WriteParam(aWriter, aParam.mUseTrackingProtection);
+ WriteParam(aWriter, suffix);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ nsAutoCString suffix;
+ if (!ReadParam(aReader, &aResult->mIsNotNull) ||
+ !ReadParam(aReader, &aResult->mIsContent) ||
+ !ReadParam(aReader, &aResult->mIsPrivateBitValid) ||
+ !ReadParam(aReader, &aResult->mUseRemoteTabs) ||
+ !ReadParam(aReader, &aResult->mUseRemoteSubframes) ||
+ !ReadParam(aReader, &aResult->mUseTrackingProtection) ||
+ !ReadParam(aReader, &suffix)) {
+ return false;
+ }
+ return aResult->mOriginAttributes.PopulateFromSuffix(suffix);
+ }
+};
+
+} // namespace IPC
+
+#endif // SerializedLoadContext_h
diff --git a/docshell/base/SyncedContext.h b/docshell/base/SyncedContext.h
new file mode 100644
index 0000000000..679e07edc2
--- /dev/null
+++ b/docshell/base/SyncedContext.h
@@ -0,0 +1,402 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SyncedContext_h
+#define mozilla_dom_SyncedContext_h
+
+#include <array>
+#include <type_traits>
+#include <utility>
+#include "mozilla/Attributes.h"
+#include "mozilla/BitSet.h"
+#include "mozilla/EnumSet.h"
+#include "nsStringFwd.h"
+#include "nscore.h"
+
+// Referenced via macro definitions
+#include "mozilla/ErrorResult.h"
+
+class PickleIterator;
+
+namespace IPC {
+class Message;
+class MessageReader;
+class MessageWriter;
+} // namespace IPC
+
+namespace mozilla {
+namespace ipc {
+class IProtocol;
+class IPCResult;
+template <typename T>
+struct IPDLParamTraits;
+} // namespace ipc
+
+namespace dom {
+class ContentParent;
+class ContentChild;
+template <typename T>
+class MaybeDiscarded;
+
+namespace syncedcontext {
+
+template <size_t I>
+using Index = typename std::integral_constant<size_t, I>;
+
+// We're going to use the empty base optimization for synced fields of different
+// sizes, so we define an empty class for that purpose.
+template <size_t I, size_t S>
+struct Empty {};
+
+// A templated container for a synced field. I is the index and T is the type.
+template <size_t I, typename T>
+struct Field {
+ T mField{};
+};
+
+// SizedField is a Field with a helper to define either an "empty" field, or a
+// field of a given type.
+template <size_t I, typename T, size_t S>
+using SizedField = std::conditional_t<((sizeof(T) > 8) ? 8 : sizeof(T)) == S,
+ Field<I, T>, Empty<I, S>>;
+
+template <typename Context>
+class Transaction {
+ public:
+ using IndexSet = EnumSet<size_t, BitSet<Context::FieldValues::count>>;
+
+ // Set a field at the given index in this `Transaction`. Creating a
+ // `Transaction` object and setting multiple fields on it allows for
+ // multiple mutations to be performed atomically.
+ template <size_t I, typename U>
+ void Set(U&& aValue) {
+ mValues.Get(Index<I>{}) = std::forward<U>(aValue);
+ mModified += I;
+ }
+
+ // Apply the changes from this transaction to the specified Context in all
+ // processes. This method will call the correct `CanSet` and `DidSet` methods,
+ // as well as move the value.
+ //
+ // If the target has been discarded, changes will be ignored.
+ //
+ // NOTE: This method mutates `this`, clearing the modified field set.
+ [[nodiscard]] nsresult Commit(Context* aOwner);
+
+ // Called from `ContentParent` in response to a transaction from content.
+ mozilla::ipc::IPCResult CommitFromIPC(const MaybeDiscarded<Context>& aOwner,
+ ContentParent* aSource);
+
+ // Called from `ContentChild` in response to a transaction from the parent.
+ mozilla::ipc::IPCResult CommitFromIPC(const MaybeDiscarded<Context>& aOwner,
+ uint64_t aEpoch, ContentChild* aSource);
+
+ // Apply the changes from this transaction to the specified Context WITHOUT
+ // syncing the changes to other processes.
+ //
+ // Unlike `Commit`, this method will NOT call the corresponding `CanSet` or
+ // `DidSet` methods, and can be performed when the target context is
+ // unattached or discarded.
+ //
+ // NOTE: YOU PROBABLY DO NOT WANT TO USE THIS METHOD
+ void CommitWithoutSyncing(Context* aOwner);
+
+ private:
+ friend struct mozilla::ipc::IPDLParamTraits<Transaction<Context>>;
+
+ void Write(IPC::MessageWriter* aWriter,
+ mozilla::ipc::IProtocol* aActor) const;
+ bool Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor);
+
+ // You probably don't want to directly call this method - instead call
+ // `Commit`, which will perform the necessary synchronization.
+ //
+ // `Validate` must be called before calling this method.
+ void Apply(Context* aOwner, bool aFromIPC);
+
+ // Returns the set of fields which failed to validate, or an empty set if
+ // there were no validation errors.
+ //
+ // NOTE: This method mutates `this` if any changes were reverted.
+ IndexSet Validate(Context* aOwner, ContentParent* aSource);
+
+ template <typename F>
+ static void EachIndex(F&& aCallback) {
+ Context::FieldValues::EachIndex(aCallback);
+ }
+
+ template <size_t I>
+ static uint64_t& FieldEpoch(Index<I>, Context* aContext) {
+ return std::get<I>(aContext->mFields.mEpochs);
+ }
+
+ typename Context::FieldValues mValues;
+ IndexSet mModified;
+};
+
+template <typename Base, size_t Count>
+class FieldValues : public Base {
+ public:
+ // The number of fields stored by this type.
+ static constexpr size_t count = Count;
+
+ // The base type will define a series of `Get` methods for looking up a field
+ // by its field index.
+ using Base::Get;
+
+ // Calls a generic lambda with an `Index<I>` for each index less than the
+ // field count.
+ template <typename F>
+ static void EachIndex(F&& aCallback) {
+ EachIndexInner(std::make_index_sequence<count>(),
+ std::forward<F>(aCallback));
+ }
+
+ private:
+ friend struct mozilla::ipc::IPDLParamTraits<FieldValues<Base, Count>>;
+
+ void Write(IPC::MessageWriter* aWriter,
+ mozilla::ipc::IProtocol* aActor) const;
+ bool Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor);
+
+ template <typename F, size_t... Indexes>
+ static void EachIndexInner(std::index_sequence<Indexes...> aIndexes,
+ F&& aCallback) {
+ (aCallback(Index<Indexes>()), ...);
+ }
+};
+
+// Storage related to synchronized context fields. Contains both a tuple of
+// individual field values, and epoch information for field synchronization.
+template <typename Values>
+class FieldStorage {
+ public:
+ // Unsafely grab a reference directly to the internal values structure which
+ // can be modified without telling other processes about the change.
+ //
+ // This is only sound in specific code which is already messaging other
+ // processes, and doesn't need to worry about epochs or other properties of
+ // field synchronization.
+ Values& RawValues() { return mValues; }
+ const Values& RawValues() const { return mValues; }
+
+ // Get an individual field by index.
+ template <size_t I>
+ const auto& Get() const {
+ return RawValues().Get(Index<I>{});
+ }
+
+ // Set the value of a field without telling other processes about the change.
+ //
+ // This is only sound in specific code which is already messaging other
+ // processes, and doesn't need to worry about epochs or other properties of
+ // field synchronization.
+ template <size_t I, typename U>
+ void SetWithoutSyncing(U&& aValue) {
+ GetNonSyncingReference<I>() = std::move(aValue);
+ }
+
+ // Get a reference to a field that can modify without telling other
+ // processes about the change.
+ //
+ // This is only sound in specific code which is already messaging other
+ // processes, and doesn't need to worry about epochs or other properties of
+ // field synchronization.
+ template <size_t I>
+ auto& GetNonSyncingReference() {
+ return RawValues().Get(Index<I>{});
+ }
+
+ FieldStorage() = default;
+ explicit FieldStorage(Values&& aInit) : mValues(std::move(aInit)) {}
+
+ private:
+ template <typename Context>
+ friend class Transaction;
+
+ // Data Members
+ std::array<uint64_t, Values::count> mEpochs{};
+ Values mValues;
+};
+
+// Alternative return type enum for `CanSet` validators which allows specifying
+// more behaviour.
+enum class CanSetResult : uint8_t {
+ // The set attempt is denied. This is equivalent to returning `false`.
+ Deny,
+ // The set attempt is allowed. This is equivalent to returning `true`.
+ Allow,
+ // The set attempt is reverted non-fatally.
+ Revert,
+};
+
+// Helper type traits to use concrete types rather than generic forwarding
+// references for the `SetXXX` methods defined on the synced context type.
+//
+// This helps avoid potential issues where someone accidentally declares an
+// overload of these methods with slightly different types and different
+// behaviours. See bug 1659520.
+template <typename T>
+struct GetFieldSetterType {
+ using SetterArg = T;
+};
+template <>
+struct GetFieldSetterType<nsString> {
+ using SetterArg = const nsAString&;
+};
+template <>
+struct GetFieldSetterType<nsCString> {
+ using SetterArg = const nsACString&;
+};
+template <typename T>
+using FieldSetterType = typename GetFieldSetterType<T>::SetterArg;
+
+#define MOZ_DECL_SYNCED_CONTEXT_FIELD_INDEX(name, type) IDX_##name,
+
+#define MOZ_DECL_SYNCED_CONTEXT_FIELD_GETSET(name, type) \
+ const type& Get##name() const { return mFields.template Get<IDX_##name>(); } \
+ \
+ [[nodiscard]] nsresult Set##name( \
+ ::mozilla::dom::syncedcontext::FieldSetterType<type> aValue) { \
+ Transaction txn; \
+ txn.template Set<IDX_##name>(std::move(aValue)); \
+ return txn.Commit(this); \
+ } \
+ void Set##name(::mozilla::dom::syncedcontext::FieldSetterType<type> aValue, \
+ ErrorResult& aRv) { \
+ nsresult rv = this->Set##name(std::move(aValue)); \
+ if (NS_FAILED(rv)) { \
+ aRv.ThrowInvalidStateError("cannot set synced field '" #name \
+ "': context is discarded"); \
+ } \
+ }
+
+#define MOZ_DECL_SYNCED_CONTEXT_TRANSACTION_SET(name, type) \
+ template <typename U> \
+ void Set##name(U&& aValue) { \
+ this->template Set<IDX_##name>(std::forward<U>(aValue)); \
+ }
+#define MOZ_DECL_SYNCED_CONTEXT_INDEX_TO_NAME(name, type) \
+ case IDX_##name: \
+ return #name;
+
+#define MOZ_DECL_SYNCED_FIELD_INHERIT(name, type) \
+ public \
+ syncedcontext::SizedField<IDX_##name, type, Size>,
+
+#define MOZ_DECL_SYNCED_CONTEXT_BASE_FIELD_GETTER(name, type) \
+ type& Get(FieldIndex<IDX_##name>) { \
+ return Field<IDX_##name, type>::mField; \
+ } \
+ const type& Get(FieldIndex<IDX_##name>) const { \
+ return Field<IDX_##name, type>::mField; \
+ }
+
+// Declare a type as a synced context type.
+//
+// clazz is the name of the type being declared, and `eachfield` is a macro
+// which, when called with the name of the macro, will call that macro once for
+// each field in the synced context.
+#define MOZ_DECL_SYNCED_CONTEXT(clazz, eachfield) \
+ public: \
+ /* Index constants for referring to each field in generic code */ \
+ enum FieldIndexes { \
+ eachfield(MOZ_DECL_SYNCED_CONTEXT_FIELD_INDEX) SYNCED_FIELD_COUNT \
+ }; \
+ \
+ /* Helper for overloading methods like `CanSet` and `DidSet` */ \
+ template <size_t I> \
+ using FieldIndex = typename ::mozilla::dom::syncedcontext::Index<I>; \
+ \
+ /* Fields contain all synced fields defined by \
+ * `eachfield(MOZ_DECL_SYNCED_FIELD_INHERIT)`, but only those where the size \
+ * of the field is equal to size Size will be present. We use SizedField to \
+ * remove fields of the wrong size. */ \
+ template <size_t Size> \
+ struct Fields : eachfield(MOZ_DECL_SYNCED_FIELD_INHERIT) \
+ syncedcontext::Empty<SYNCED_FIELD_COUNT, Size> {}; \
+ \
+ /* Struct containing the data for all synced fields as members. We filter \
+ * sizes to lay out fields of size 1, then 2, then 4 and last 8 or greater. \
+ * This way we don't need to consider packing when defining fields, but \
+ * we'll just reorder them here. \
+ */ \
+ struct BaseFieldValues : public Fields<1>, \
+ public Fields<2>, \
+ public Fields<4>, \
+ public Fields<8> { \
+ template <size_t I> \
+ auto& Get() { \
+ return Get(FieldIndex<I>{}); \
+ } \
+ template <size_t I> \
+ const auto& Get() const { \
+ return Get(FieldIndex<I>{}); \
+ } \
+ eachfield(MOZ_DECL_SYNCED_CONTEXT_BASE_FIELD_GETTER) \
+ }; \
+ using FieldValues = \
+ typename ::mozilla::dom::syncedcontext::FieldValues<BaseFieldValues, \
+ SYNCED_FIELD_COUNT>; \
+ \
+ protected: \
+ friend class ::mozilla::dom::syncedcontext::Transaction<clazz>; \
+ ::mozilla::dom::syncedcontext::FieldStorage<FieldValues> mFields; \
+ \
+ public: \
+ /* Transaction types for bulk mutations */ \
+ using BaseTransaction = ::mozilla::dom::syncedcontext::Transaction<clazz>; \
+ class Transaction final : public BaseTransaction { \
+ public: \
+ eachfield(MOZ_DECL_SYNCED_CONTEXT_TRANSACTION_SET) \
+ }; \
+ \
+ /* Field name getter by field index */ \
+ static const char* FieldIndexToName(size_t aIndex) { \
+ switch (aIndex) { eachfield(MOZ_DECL_SYNCED_CONTEXT_INDEX_TO_NAME) } \
+ return "<unknown>"; \
+ } \
+ eachfield(MOZ_DECL_SYNCED_CONTEXT_FIELD_GETSET)
+
+} // namespace syncedcontext
+} // namespace dom
+
+namespace ipc {
+
+template <typename Context>
+struct IPDLParamTraits<dom::syncedcontext::Transaction<Context>> {
+ typedef dom::syncedcontext::Transaction<Context> paramType;
+
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const paramType& aParam) {
+ aParam.Write(aWriter, aActor);
+ }
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ paramType* aResult) {
+ return aResult->Read(aReader, aActor);
+ }
+};
+
+template <typename Base, size_t Count>
+struct IPDLParamTraits<dom::syncedcontext::FieldValues<Base, Count>> {
+ typedef dom::syncedcontext::FieldValues<Base, Count> paramType;
+
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const paramType& aParam) {
+ aParam.Write(aWriter, aActor);
+ }
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ paramType* aResult) {
+ return aResult->Read(aReader, aActor);
+ }
+};
+
+} // namespace ipc
+} // namespace mozilla
+
+#endif // !defined(mozilla_dom_SyncedContext_h)
diff --git a/docshell/base/SyncedContextInlines.h b/docshell/base/SyncedContextInlines.h
new file mode 100644
index 0000000000..e386e75b35
--- /dev/null
+++ b/docshell/base/SyncedContextInlines.h
@@ -0,0 +1,359 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SyncedContextInlines_h
+#define mozilla_dom_SyncedContextInlines_h
+
+#include "mozilla/dom/SyncedContext.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ContentChild.h"
+#include "nsReadableUtils.h"
+#include "mozilla/HalIPCUtils.h"
+
+namespace mozilla {
+namespace dom {
+namespace syncedcontext {
+
+template <typename T>
+struct IsMozillaMaybe : std::false_type {};
+template <typename T>
+struct IsMozillaMaybe<Maybe<T>> : std::true_type {};
+
+// A super-sketchy logging only function for generating a human-readable version
+// of the value of a synced context field.
+template <typename T>
+void FormatFieldValue(nsACString& aStr, const T& aValue) {
+ if constexpr (std::is_same_v<bool, T>) {
+ if (aValue) {
+ aStr.AppendLiteral("true");
+ } else {
+ aStr.AppendLiteral("false");
+ }
+ } else if constexpr (std::is_same_v<nsID, T>) {
+ aStr.Append(nsIDToCString(aValue).get());
+ } else if constexpr (std::is_integral_v<T>) {
+ aStr.AppendInt(aValue);
+ } else if constexpr (std::is_enum_v<T>) {
+ aStr.AppendInt(static_cast<std::underlying_type_t<T>>(aValue));
+ } else if constexpr (std::is_floating_point_v<T>) {
+ aStr.AppendFloat(aValue);
+ } else if constexpr (std::is_base_of_v<nsAString, T>) {
+ AppendUTF16toUTF8(aValue, aStr);
+ } else if constexpr (std::is_base_of_v<nsACString, T>) {
+ aStr.Append(aValue);
+ } else if constexpr (IsMozillaMaybe<T>::value) {
+ if (aValue) {
+ aStr.AppendLiteral("Some(");
+ FormatFieldValue(aStr, aValue.ref());
+ aStr.AppendLiteral(")");
+ } else {
+ aStr.AppendLiteral("Nothing");
+ }
+ } else {
+ aStr.AppendLiteral("???");
+ }
+}
+
+// Sketchy logging-only logic to generate a human-readable output of the actions
+// a synced context transaction is going to perform.
+template <typename Context>
+nsAutoCString FormatTransaction(
+ typename Transaction<Context>::IndexSet aModified,
+ const typename Context::FieldValues& aOldValues,
+ const typename Context::FieldValues& aNewValues) {
+ nsAutoCString result;
+ Context::FieldValues::EachIndex([&](auto idx) {
+ if (aModified.contains(idx)) {
+ result.Append(Context::FieldIndexToName(idx));
+ result.AppendLiteral("(");
+ FormatFieldValue(result, aOldValues.Get(idx));
+ result.AppendLiteral("->");
+ FormatFieldValue(result, aNewValues.Get(idx));
+ result.AppendLiteral(") ");
+ }
+ });
+ return result;
+}
+
+template <typename Context>
+nsCString FormatValidationError(
+ typename Transaction<Context>::IndexSet aFailedFields, const char* prefix) {
+ MOZ_ASSERT(!aFailedFields.isEmpty());
+ return nsDependentCString{prefix} +
+ StringJoin(", "_ns, aFailedFields,
+ [](nsACString& dest, const auto& idx) {
+ dest.Append(Context::FieldIndexToName(idx));
+ });
+}
+
+template <typename Context>
+nsresult Transaction<Context>::Commit(Context* aOwner) {
+ if (NS_WARN_IF(aOwner->IsDiscarded())) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ IndexSet failedFields = Validate(aOwner, nullptr);
+ if (!failedFields.isEmpty()) {
+ nsCString error = FormatValidationError<Context>(
+ failedFields, "CanSet failed for field(s): ");
+ MOZ_CRASH_UNSAFE_PRINTF("%s", error.get());
+ }
+
+ if (mModified.isEmpty()) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess()) {
+ ContentChild* cc = ContentChild::GetSingleton();
+
+ // Increment the field epoch for fields affected by this transaction.
+ uint64_t epoch = cc->NextBrowsingContextFieldEpoch();
+ EachIndex([&](auto idx) {
+ if (mModified.contains(idx)) {
+ FieldEpoch(idx, aOwner) = epoch;
+ }
+ });
+
+ // Tell our derived class to send the correct "Commit" IPC message.
+ aOwner->SendCommitTransaction(cc, *this, epoch);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ // Tell our derived class to send the correct "Commit" IPC messages.
+ BrowsingContextGroup* group = aOwner->Group();
+ group->EachParent([&](ContentParent* aParent) {
+ aOwner->SendCommitTransaction(aParent, *this,
+ aParent->GetBrowsingContextFieldEpoch());
+ });
+ }
+
+ Apply(aOwner, /* aFromIPC */ false);
+ return NS_OK;
+}
+
+template <typename Context>
+mozilla::ipc::IPCResult Transaction<Context>::CommitFromIPC(
+ const MaybeDiscarded<Context>& aOwner, ContentParent* aSource) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ if (aOwner.IsNullOrDiscarded()) {
+ MOZ_LOG(Context::GetSyncLog(), LogLevel::Debug,
+ ("IPC: Trying to send a message to dead or detached context"));
+ return IPC_OK();
+ }
+ Context* owner = aOwner.get();
+
+ // Validate that the set from content is allowed before continuing.
+ IndexSet failedFields = Validate(owner, aSource);
+ if (!failedFields.isEmpty()) {
+ nsCString error = FormatValidationError<Context>(
+ failedFields,
+ "Invalid Transaction from Child - CanSet failed for field(s): ");
+ // data-review+ at https://bugzilla.mozilla.org/show_bug.cgi?id=1618992#c7
+ return IPC_FAIL_UNSAFE_PRINTF(aSource, "%s", error.get());
+ }
+
+ // Validate may have dropped some fields from the transaction, check it's not
+ // empty before continuing.
+ if (mModified.isEmpty()) {
+ return IPC_OK();
+ }
+
+ BrowsingContextGroup* group = owner->Group();
+ group->EachOtherParent(aSource, [&](ContentParent* aParent) {
+ owner->SendCommitTransaction(aParent, *this,
+ aParent->GetBrowsingContextFieldEpoch());
+ });
+
+ Apply(owner, /* aFromIPC */ true);
+ return IPC_OK();
+}
+
+template <typename Context>
+mozilla::ipc::IPCResult Transaction<Context>::CommitFromIPC(
+ const MaybeDiscarded<Context>& aOwner, uint64_t aEpoch,
+ ContentChild* aSource) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess());
+ if (aOwner.IsNullOrDiscarded()) {
+ MOZ_LOG(Context::GetSyncLog(), LogLevel::Debug,
+ ("ChildIPC: Trying to send a message to dead or detached context"));
+ return IPC_OK();
+ }
+ Context* owner = aOwner.get();
+
+ // Clear any fields which have been obsoleted by the epoch.
+ EachIndex([&](auto idx) {
+ if (mModified.contains(idx) && FieldEpoch(idx, owner) > aEpoch) {
+ MOZ_LOG(
+ Context::GetSyncLog(), LogLevel::Debug,
+ ("Transaction::Obsoleted(#%" PRIx64 ", %" PRIu64 ">%" PRIu64 "): %s",
+ owner->Id(), FieldEpoch(idx, owner), aEpoch,
+ Context::FieldIndexToName(idx)));
+ mModified -= idx;
+ }
+ });
+
+ if (mModified.isEmpty()) {
+ return IPC_OK();
+ }
+
+ Apply(owner, /* aFromIPC */ true);
+ return IPC_OK();
+}
+
+template <typename Context>
+void Transaction<Context>::Apply(Context* aOwner, bool aFromIPC) {
+ MOZ_ASSERT(!mModified.isEmpty());
+
+ MOZ_LOG(
+ Context::GetSyncLog(), LogLevel::Debug,
+ ("Transaction::Apply(#%" PRIx64 ", %s): %s", aOwner->Id(),
+ aFromIPC ? "ipc" : "local",
+ FormatTransaction<Context>(mModified, aOwner->mFields.mValues, mValues)
+ .get()));
+
+ EachIndex([&](auto idx) {
+ if (mModified.contains(idx)) {
+ auto& txnField = mValues.Get(idx);
+ auto& ownerField = aOwner->mFields.mValues.Get(idx);
+ std::swap(ownerField, txnField);
+ aOwner->DidSet(idx);
+ aOwner->DidSet(idx, std::move(txnField));
+ }
+ });
+ mModified.clear();
+}
+
+template <typename Context>
+void Transaction<Context>::CommitWithoutSyncing(Context* aOwner) {
+ MOZ_LOG(
+ Context::GetSyncLog(), LogLevel::Debug,
+ ("Transaction::CommitWithoutSyncing(#%" PRIx64 "): %s", aOwner->Id(),
+ FormatTransaction<Context>(mModified, aOwner->mFields.mValues, mValues)
+ .get()));
+
+ EachIndex([&](auto idx) {
+ if (mModified.contains(idx)) {
+ aOwner->mFields.mValues.Get(idx) = std::move(mValues.Get(idx));
+ }
+ });
+ mModified.clear();
+}
+
+inline CanSetResult AsCanSetResult(CanSetResult aValue) { return aValue; }
+inline CanSetResult AsCanSetResult(bool aValue) {
+ return aValue ? CanSetResult::Allow : CanSetResult::Deny;
+}
+
+template <typename Context>
+typename Transaction<Context>::IndexSet Transaction<Context>::Validate(
+ Context* aOwner, ContentParent* aSource) {
+ IndexSet failedFields;
+ Transaction<Context> revertTxn;
+
+ EachIndex([&](auto idx) {
+ if (!mModified.contains(idx)) {
+ return;
+ }
+
+ switch (AsCanSetResult(aOwner->CanSet(idx, mValues.Get(idx), aSource))) {
+ case CanSetResult::Allow:
+ break;
+ case CanSetResult::Deny:
+ failedFields += idx;
+ break;
+ case CanSetResult::Revert:
+ revertTxn.mValues.Get(idx) = aOwner->mFields.mValues.Get(idx);
+ revertTxn.mModified += idx;
+ break;
+ }
+ });
+
+ // If any changes need to be reverted, log them, remove them from this
+ // transaction, and optionally send a message to the source process.
+ if (!revertTxn.mModified.isEmpty()) {
+ // NOTE: Logging with modified IndexSet from revert transaction, and values
+ // from this transaction, so we log the failed values we're going to revert.
+ MOZ_LOG(
+ Context::GetSyncLog(), LogLevel::Debug,
+ ("Transaction::PartialRevert(#%" PRIx64 ", pid %" PRIPID "): %s",
+ aOwner->Id(), aSource ? aSource->OtherPid() : base::kInvalidProcessId,
+ FormatTransaction<Context>(revertTxn.mModified, mValues,
+ revertTxn.mValues)
+ .get()));
+
+ mModified -= revertTxn.mModified;
+
+ if (aSource) {
+ aOwner->SendCommitTransaction(aSource, revertTxn,
+ aSource->GetBrowsingContextFieldEpoch());
+ }
+ }
+ return failedFields;
+}
+
+template <typename Context>
+void Transaction<Context>::Write(IPC::MessageWriter* aWriter,
+ mozilla::ipc::IProtocol* aActor) const {
+ // Record which field indices will be included, and then write those fields
+ // out.
+ typename IndexSet::serializedType modified = mModified.serialize();
+ WriteIPDLParam(aWriter, aActor, modified);
+ EachIndex([&](auto idx) {
+ if (mModified.contains(idx)) {
+ WriteIPDLParam(aWriter, aActor, mValues.Get(idx));
+ }
+ });
+}
+
+template <typename Context>
+bool Transaction<Context>::Read(IPC::MessageReader* aReader,
+ mozilla::ipc::IProtocol* aActor) {
+ // Read in which field indices were sent by the remote, followed by the fields
+ // identified by those indices.
+ typename IndexSet::serializedType modified =
+ typename IndexSet::serializedType{};
+ if (!ReadIPDLParam(aReader, aActor, &modified)) {
+ return false;
+ }
+ mModified.deserialize(modified);
+
+ bool ok = true;
+ EachIndex([&](auto idx) {
+ if (ok && mModified.contains(idx)) {
+ ok = ReadIPDLParam(aReader, aActor, &mValues.Get(idx));
+ }
+ });
+ return ok;
+}
+
+template <typename Base, size_t Count>
+void FieldValues<Base, Count>::Write(IPC::MessageWriter* aWriter,
+ mozilla::ipc::IProtocol* aActor) const {
+ // XXX The this-> qualification is necessary to work around a bug in older gcc
+ // versions causing an ICE.
+ EachIndex([&](auto idx) { WriteIPDLParam(aWriter, aActor, this->Get(idx)); });
+}
+
+template <typename Base, size_t Count>
+bool FieldValues<Base, Count>::Read(IPC::MessageReader* aReader,
+ mozilla::ipc::IProtocol* aActor) {
+ bool ok = true;
+ EachIndex([&](auto idx) {
+ if (ok) {
+ // XXX The this-> qualification is necessary to work around a bug in older
+ // gcc versions causing an ICE.
+ ok = ReadIPDLParam(aReader, aActor, &this->Get(idx));
+ }
+ });
+ return ok;
+}
+
+} // namespace syncedcontext
+} // namespace dom
+} // namespace mozilla
+
+#endif // !defined(mozilla_dom_SyncedContextInlines_h)
diff --git a/docshell/base/URIFixup.sys.mjs b/docshell/base/URIFixup.sys.mjs
new file mode 100644
index 0000000000..6fde38a310
--- /dev/null
+++ b/docshell/base/URIFixup.sys.mjs
@@ -0,0 +1,1306 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This component handles fixing up URIs, by correcting obvious typos and adding
+ * missing schemes.
+ * URI references:
+ * http://www.faqs.org/rfcs/rfc1738.html
+ * http://www.faqs.org/rfcs/rfc2396.html
+ */
+
+// TODO (Bug 1641220) getFixupURIInfo has a complex logic, that likely could be
+// simplified, but the risk of regressing its behavior is high.
+/* eslint complexity: ["error", 43] */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "externalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "defaultProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=default",
+ "nsIProtocolHandler"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "fileProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=file",
+ "nsIFileProtocolHandler"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "handlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "fixupSchemeTypos",
+ "browser.fixup.typo.scheme",
+ true
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "dnsFirstForSingleWords",
+ "browser.fixup.dns_first_for_single_words",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "keywordEnabled",
+ "keyword.enabled",
+ true
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "alternateProtocol",
+ "browser.fixup.alternate.protocol",
+ "https"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "dnsResolveFullyQualifiedNames",
+ "browser.urlbar.dnsResolveFullyQualifiedNames",
+ true
+);
+
+const {
+ FIXUP_FLAG_NONE,
+ FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP,
+ FIXUP_FLAGS_MAKE_ALTERNATE_URI,
+ FIXUP_FLAG_PRIVATE_CONTEXT,
+ FIXUP_FLAG_FIX_SCHEME_TYPOS,
+} = Ci.nsIURIFixup;
+
+const COMMON_PROTOCOLS = ["http", "https", "file"];
+
+// Regex used to identify user:password tokens in url strings.
+// This is not a strict valid characters check, because we try to fixup this
+// part of the url too.
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "userPasswordRegex",
+ () => /^([a-z+.-]+:\/{0,3})*([^\/@]+@).+/i
+);
+
+// Regex used to identify the string that starts with port expression.
+ChromeUtils.defineLazyGetter(lazy, "portRegex", () => /^:\d{1,5}([?#/]|$)/);
+
+// Regex used to identify numbers.
+ChromeUtils.defineLazyGetter(lazy, "numberRegex", () => /^[0-9]+(\.[0-9]+)?$/);
+
+// Regex used to identify tab separated content (having at least 2 tabs).
+ChromeUtils.defineLazyGetter(lazy, "maxOneTabRegex", () => /^[^\t]*\t?[^\t]*$/);
+
+// Regex used to test if a string with a protocol might instead be a url
+// without a protocol but with a port:
+//
+// <hostname>:<port> or
+// <hostname>:<port>/
+//
+// Where <hostname> is a string of alphanumeric characters and dashes
+// separated by dots.
+// and <port> is a 5 or less digits. This actually breaks the rfc2396
+// definition of a scheme which allows dots in schemes.
+//
+// Note:
+// People expecting this to work with
+// <user>:<password>@<host>:<port>/<url-path> will be disappointed!
+//
+// Note: Parser could be a lot tighter, tossing out silly hostnames
+// such as those containing consecutive dots and so on.
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "possiblyHostPortRegex",
+ () => /^[a-z0-9-]+(\.[a-z0-9-]+)*:[0-9]{1,5}([/?#]|$)/i
+);
+
+// Regex used to strip newlines.
+ChromeUtils.defineLazyGetter(lazy, "newLinesRegex", () => /[\r\n]/g);
+
+// Regex used to match a possible protocol.
+// This resembles the logic in Services.io.extractScheme, thus \t is admitted
+// and stripped later. We don't use Services.io.extractScheme because of
+// performance bottleneck caused by crossing XPConnect.
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "possibleProtocolRegex",
+ () => /^([a-z][a-z0-9.+\t-]*)(:|;)?(\/\/)?/i
+);
+
+// Regex used to match IPs. Note that these are not made to validate IPs, but
+// just to detect strings that look like an IP. They also skip protocol.
+// For IPv4 this also accepts a shorthand format with just 2 dots.
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "IPv4LikeRegex",
+ () => /^(?:[a-z+.-]+:\/*(?!\/))?(?:\d{1,3}\.){2,3}\d{1,3}(?::\d+|\/)?/i
+);
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "IPv6LikeRegex",
+ () =>
+ /^(?:[a-z+.-]+:\/*(?!\/))?\[(?:[0-9a-f]{0,4}:){0,7}[0-9a-f]{0,4}\]?(?::\d+|\/)?/i
+);
+
+// Regex used to detect spaces in URL credentials.
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "DetectSpaceInCredentialsRegex",
+ () => /^[^/]*\s[^/]*@/
+);
+
+// Cache of known domains.
+ChromeUtils.defineLazyGetter(lazy, "knownDomains", () => {
+ const branch = "browser.fixup.domainwhitelist.";
+ let domains = new Set(
+ Services.prefs
+ .getChildList(branch)
+ .filter(p => Services.prefs.getBoolPref(p, false))
+ .map(p => p.substring(branch.length))
+ );
+ // Hold onto the observer to avoid it being GC-ed.
+ domains._observer = {
+ observe(subject, topic, data) {
+ let domain = data.substring(branch.length);
+ if (Services.prefs.getBoolPref(data, false)) {
+ domains.add(domain);
+ } else {
+ domains.delete(domain);
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ Services.prefs.addObserver(branch, domains._observer, true);
+ return domains;
+});
+
+// Cache of known suffixes.
+// This works differently from the known domains, because when we examine a
+// domain we can't tell how many dot-separated parts constitute the suffix.
+// We create a Map keyed by the last dotted part, containing a Set of
+// all the suffixes ending with that part:
+// "two" => ["two"]
+// "three" => ["some.three", "three"]
+// When searching we can restrict the linear scan based on the last part.
+// The ideal structure for this would be a Directed Acyclic Word Graph, but
+// since we expect this list to be small it's not worth the complication.
+ChromeUtils.defineLazyGetter(lazy, "knownSuffixes", () => {
+ const branch = "browser.fixup.domainsuffixwhitelist.";
+ let suffixes = new Map();
+ let prefs = Services.prefs
+ .getChildList(branch)
+ .filter(p => Services.prefs.getBoolPref(p, false));
+ for (let pref of prefs) {
+ let suffix = pref.substring(branch.length);
+ let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1);
+ if (lastPart) {
+ let entries = suffixes.get(lastPart);
+ if (!entries) {
+ entries = new Set();
+ suffixes.set(lastPart, entries);
+ }
+ entries.add(suffix);
+ }
+ }
+ // Hold onto the observer to avoid it being GC-ed.
+ suffixes._observer = {
+ observe(subject, topic, data) {
+ let suffix = data.substring(branch.length);
+ let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1);
+ let entries = suffixes.get(lastPart);
+ if (Services.prefs.getBoolPref(data, false)) {
+ // Add the suffix.
+ if (!entries) {
+ entries = new Set();
+ suffixes.set(lastPart, entries);
+ }
+ entries.add(suffix);
+ } else if (entries) {
+ // Remove the suffix.
+ entries.delete(suffix);
+ if (!entries.size) {
+ suffixes.delete(lastPart);
+ }
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ Services.prefs.addObserver(branch, suffixes._observer, true);
+ return suffixes;
+});
+
+export function URIFixup() {
+ // There are cases that nsIExternalProtocolService.externalProtocolHandlerExists() does
+ // not work well and returns always true due to flatpak. In this case, in order to
+ // fallback to nsIHandlerService.exits(), we test whether can trust
+ // nsIExternalProtocolService here.
+ this._trustExternalProtocolService =
+ !lazy.externalProtocolService.externalProtocolHandlerExists(
+ `__dummy${Date.now()}__`
+ );
+}
+
+URIFixup.prototype = {
+ get FIXUP_FLAG_NONE() {
+ return FIXUP_FLAG_NONE;
+ },
+ get FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP() {
+ return FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ },
+ get FIXUP_FLAGS_MAKE_ALTERNATE_URI() {
+ return FIXUP_FLAGS_MAKE_ALTERNATE_URI;
+ },
+ get FIXUP_FLAG_PRIVATE_CONTEXT() {
+ return FIXUP_FLAG_PRIVATE_CONTEXT;
+ },
+ get FIXUP_FLAG_FIX_SCHEME_TYPOS() {
+ return FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ },
+
+ getFixupURIInfo(uriString, fixupFlags = FIXUP_FLAG_NONE) {
+ let isPrivateContext = fixupFlags & FIXUP_FLAG_PRIVATE_CONTEXT;
+ let untrimmedURIString = uriString;
+
+ // Eliminate embedded newlines, which single-line text fields now allow,
+ // and cleanup the empty spaces and tabs that might be on each end.
+ uriString = uriString.trim().replace(lazy.newLinesRegex, "");
+
+ if (!uriString) {
+ throw new Components.Exception(
+ "Should pass a non-null uri",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let info = new URIFixupInfo(uriString);
+
+ const { scheme, fixedSchemeUriString, fixupChangedProtocol } =
+ extractScheme(uriString, fixupFlags);
+ uriString = fixedSchemeUriString;
+ info.fixupChangedProtocol = fixupChangedProtocol;
+
+ if (scheme == "view-source") {
+ let { preferredURI, postData } = fixupViewSource(uriString, fixupFlags);
+ info.preferredURI = info.fixedURI = preferredURI;
+ info.postData = postData;
+ return info;
+ }
+
+ if (scheme.length < 2) {
+ // Check if it is a file path. We skip most schemes because the only case
+ // where a file path may look like having a scheme is "X:" on Windows.
+ let fileURI = fileURIFixup(uriString);
+ if (fileURI) {
+ info.preferredURI = info.fixedURI = fileURI;
+ info.fixupChangedProtocol = true;
+ return info;
+ }
+ }
+
+ const isCommonProtocol = COMMON_PROTOCOLS.includes(scheme);
+
+ let canHandleProtocol =
+ scheme &&
+ (isCommonProtocol ||
+ Services.io.getProtocolHandler(scheme) != lazy.defaultProtocolHandler ||
+ this._isKnownExternalProtocol(scheme));
+
+ if (
+ canHandleProtocol ||
+ // If it's an unknown handler and the given URL looks like host:port or
+ // has a user:password we can't pass it to the external protocol handler.
+ // We'll instead try fixing it with http later.
+ (!lazy.possiblyHostPortRegex.test(uriString) &&
+ !lazy.userPasswordRegex.test(uriString))
+ ) {
+ // Just try to create an URL out of it.
+ try {
+ info.fixedURI = Services.io.newURI(uriString);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
+ throw ex;
+ }
+ }
+ }
+
+ // We're dealing with a theoretically valid URI but we have no idea how to
+ // load it. (e.g. "christmas:humbug")
+ // It's more likely the user wants to search, and so we chuck this over to
+ // their preferred search provider.
+ // TODO (Bug 1588118): Should check FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
+ // instead of FIXUP_FLAG_FIX_SCHEME_TYPOS.
+ if (
+ info.fixedURI &&
+ lazy.keywordEnabled &&
+ fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS &&
+ scheme &&
+ !canHandleProtocol
+ ) {
+ tryKeywordFixupForURIInfo(uriString, info, isPrivateContext);
+ }
+
+ if (info.fixedURI) {
+ if (!info.preferredURI) {
+ maybeSetAlternateFixedURI(info, fixupFlags);
+ info.preferredURI = info.fixedURI;
+ }
+ fixupConsecutiveDotsHost(info);
+ return info;
+ }
+
+ // Fix up protocol string before calling KeywordURIFixup, because
+ // it cares about the hostname of such URIs.
+ // Prune duff protocol schemes:
+ // ://totallybroken.url.com
+ // //shorthand.url.com
+ let inputHadDuffProtocol =
+ uriString.startsWith("://") || uriString.startsWith("//");
+ if (inputHadDuffProtocol) {
+ uriString = uriString.replace(/^:?\/\//, "");
+ }
+
+ // Avoid fixing up content that looks like tab-separated values.
+ // Assume that 1 tab is accidental, but more than 1 implies this is
+ // supposed to be tab-separated content.
+ if (
+ !isCommonProtocol &&
+ lazy.maxOneTabRegex.test(uriString) &&
+ !lazy.DetectSpaceInCredentialsRegex.test(untrimmedURIString)
+ ) {
+ let uriWithProtocol = fixupURIProtocol(uriString);
+ if (uriWithProtocol) {
+ info.fixedURI = uriWithProtocol;
+ info.fixupChangedProtocol = true;
+ info.wasSchemelessInput = true;
+ maybeSetAlternateFixedURI(info, fixupFlags);
+ info.preferredURI = info.fixedURI;
+ // Check if it's a forced visit. The user can enforce a visit by
+ // appending a slash, but the string must be in a valid uri format.
+ if (uriString.endsWith("/")) {
+ fixupConsecutiveDotsHost(info);
+ return info;
+ }
+ }
+ }
+
+ // Handle "www.<something>" as a URI.
+ const asciiHost = info.fixedURI?.asciiHost;
+ if (
+ asciiHost?.length > 4 &&
+ asciiHost?.startsWith("www.") &&
+ asciiHost?.lastIndexOf(".") == 3
+ ) {
+ return info;
+ }
+
+ // Memoize the public suffix check, since it may be expensive and should
+ // only run once when necessary.
+ let suffixInfo;
+ function checkSuffix(info) {
+ if (!suffixInfo) {
+ suffixInfo = checkAndFixPublicSuffix(info);
+ }
+ return suffixInfo;
+ }
+
+ // See if it is a keyword and whether a keyword must be fixed up.
+ if (
+ lazy.keywordEnabled &&
+ fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP &&
+ !inputHadDuffProtocol &&
+ !checkSuffix(info).suffix &&
+ keywordURIFixup(uriString, info, isPrivateContext)
+ ) {
+ fixupConsecutiveDotsHost(info);
+ return info;
+ }
+
+ if (
+ info.fixedURI &&
+ (!info.fixupChangedProtocol || !checkSuffix(info).hasUnknownSuffix)
+ ) {
+ fixupConsecutiveDotsHost(info);
+ return info;
+ }
+
+ // If we still haven't been able to construct a valid URI, try to force a
+ // keyword match.
+ if (lazy.keywordEnabled && fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) {
+ tryKeywordFixupForURIInfo(info.originalInput, info, isPrivateContext);
+ }
+
+ if (!info.preferredURI) {
+ // We couldn't salvage anything.
+ throw new Components.Exception(
+ "Couldn't build a valid uri",
+ Cr.NS_ERROR_MALFORMED_URI
+ );
+ }
+
+ fixupConsecutiveDotsHost(info);
+ return info;
+ },
+
+ webNavigationFlagsToFixupFlags(href, navigationFlags) {
+ try {
+ Services.io.newURI(href);
+ // Remove LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP for valid uris.
+ navigationFlags &=
+ ~Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ } catch (ex) {}
+
+ let fixupFlags = FIXUP_FLAG_NONE;
+ if (
+ navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
+ ) {
+ fixupFlags |= FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ }
+ if (navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
+ fixupFlags |= FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ }
+ return fixupFlags;
+ },
+
+ keywordToURI(keyword, isPrivateContext) {
+ if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ // There's no search service in the content process, thus all the calls
+ // from it that care about keywords conversion should go through the
+ // parent process.
+ throw new Components.Exception(
+ "Can't invoke URIFixup in the content process",
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+ let info = new URIFixupInfo(keyword);
+
+ // Strip leading "?" and leading/trailing spaces from aKeyword
+ if (keyword.startsWith("?")) {
+ keyword = keyword.substring(1);
+ }
+ keyword = keyword.trim();
+
+ if (!Services.search.hasSuccessfullyInitialized) {
+ return info;
+ }
+
+ // Try falling back to the search service's default search engine
+ // We must use an appropriate search engine depending on the private
+ // context.
+ let engine = isPrivateContext
+ ? Services.search.defaultPrivateEngine
+ : Services.search.defaultEngine;
+
+ // We allow default search plugins to specify alternate parameters that are
+ // specific to keyword searches.
+ let responseType = null;
+ if (engine.supportsResponseType("application/x-moz-keywordsearch")) {
+ responseType = "application/x-moz-keywordsearch";
+ }
+ let submission = engine.getSubmission(keyword, responseType, "keyword");
+ if (
+ !submission ||
+ // For security reasons (avoid redirecting to file, data, or other unsafe
+ // protocols) we only allow fixup to http/https search engines.
+ !submission.uri.scheme.startsWith("http")
+ ) {
+ throw new Components.Exception(
+ "Invalid search submission uri",
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+ let submissionPostDataStream = submission.postData;
+ if (submissionPostDataStream) {
+ info.postData = submissionPostDataStream;
+ }
+
+ info.keywordProviderName = engine.name;
+ info.keywordAsSent = keyword;
+ info.preferredURI = submission.uri;
+ return info;
+ },
+
+ forceHttpFixup(uriString) {
+ if (!uriString) {
+ throw new Components.Exception(
+ "Should pass a non-null uri",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let info = new URIFixupInfo(uriString);
+ let { scheme, fixedSchemeUriString, fixupChangedProtocol } = extractScheme(
+ uriString,
+ FIXUP_FLAG_FIX_SCHEME_TYPOS
+ );
+
+ if (scheme != "http" && scheme != "https") {
+ throw new Components.Exception(
+ "Scheme should be either http or https",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ info.fixupChangedProtocol = fixupChangedProtocol;
+ info.fixedURI = Services.io.newURI(fixedSchemeUriString);
+
+ let host = info.fixedURI.host;
+ if (host != "http" && host != "https" && host != "localhost") {
+ let modifiedHostname = maybeAddPrefixAndSuffix(host);
+ updateHostAndScheme(info, modifiedHostname);
+ info.preferredURI = info.fixedURI;
+ }
+
+ return info;
+ },
+
+ checkHost(uri, listener, originAttributes) {
+ let { displayHost, asciiHost } = uri;
+ if (!displayHost) {
+ throw new Components.Exception(
+ "URI must have displayHost",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ if (!asciiHost) {
+ throw new Components.Exception(
+ "URI must have asciiHost",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let isIPv4Address = host => {
+ let parts = host.split(".");
+ if (parts.length != 4) {
+ return false;
+ }
+ return parts.every(part => {
+ let n = parseInt(part, 10);
+ return n >= 0 && n <= 255;
+ });
+ };
+
+ // Avoid showing fixup information if we're suggesting an IP. Note that
+ // decimal representations of IPs are normalized to a 'regular'
+ // dot-separated IP address by network code, but that only happens for
+ // numbers that don't overflow. Longer numbers do not get normalized,
+ // but still work to access IP addresses. So for instance,
+ // 1097347366913 (ff7f000001) gets resolved by using the final bytes,
+ // making it the same as 7f000001, which is 127.0.0.1 aka localhost.
+ // While 2130706433 would get normalized by network, 1097347366913
+ // does not, and we have to deal with both cases here:
+ if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) {
+ return;
+ }
+
+ // For dotless hostnames, we want to ensure this ends with a '.' but don't
+ // want the . showing up in the UI if we end up notifying the user, so we
+ // use a separate variable.
+ let lookupName = displayHost;
+ if (lazy.dnsResolveFullyQualifiedNames && !lookupName.includes(".")) {
+ lookupName += ".";
+ }
+
+ Services.obs.notifyObservers(null, "uri-fixup-check-dns");
+ Services.dns.asyncResolve(
+ lookupName,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ Services.tm.mainThread,
+ originAttributes
+ );
+ },
+
+ isDomainKnown,
+
+ _isKnownExternalProtocol(scheme) {
+ if (this._trustExternalProtocolService) {
+ return lazy.externalProtocolService.externalProtocolHandlerExists(scheme);
+ }
+
+ try {
+ // nsIExternalProtocolService.getProtocolHandlerInfo() on Android throws
+ // error due to not implemented.
+ return lazy.handlerService.exists(
+ lazy.externalProtocolService.getProtocolHandlerInfo(scheme)
+ );
+ } catch (e) {
+ return false;
+ }
+ },
+
+ classID: Components.ID("{c6cf88b7-452e-47eb-bdc9-86e3561648ef}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIURIFixup"]),
+};
+
+export function URIFixupInfo(originalInput = "") {
+ this._originalInput = originalInput;
+}
+
+URIFixupInfo.prototype = {
+ set consumer(consumer) {
+ this._consumer = consumer || null;
+ },
+ get consumer() {
+ return this._consumer || null;
+ },
+
+ set preferredURI(uri) {
+ this._preferredURI = uri;
+ },
+ get preferredURI() {
+ return this._preferredURI || null;
+ },
+
+ set fixedURI(uri) {
+ this._fixedURI = uri;
+ },
+ get fixedURI() {
+ return this._fixedURI || null;
+ },
+
+ set keywordProviderName(name) {
+ this._keywordProviderName = name;
+ },
+ get keywordProviderName() {
+ return this._keywordProviderName || "";
+ },
+
+ set keywordAsSent(keyword) {
+ this._keywordAsSent = keyword;
+ },
+ get keywordAsSent() {
+ return this._keywordAsSent || "";
+ },
+
+ set wasSchemelessInput(changed) {
+ this._wasSchemelessInput = changed;
+ },
+ get wasSchemelessInput() {
+ return !!this._wasSchemelessInput;
+ },
+
+ set fixupChangedProtocol(changed) {
+ this._fixupChangedProtocol = changed;
+ },
+ get fixupChangedProtocol() {
+ return !!this._fixupChangedProtocol;
+ },
+
+ set fixupCreatedAlternateURI(changed) {
+ this._fixupCreatedAlternateURI = changed;
+ },
+ get fixupCreatedAlternateURI() {
+ return !!this._fixupCreatedAlternateURI;
+ },
+
+ set originalInput(input) {
+ this._originalInput = input;
+ },
+ get originalInput() {
+ return this._originalInput || "";
+ },
+
+ set postData(postData) {
+ this._postData = postData;
+ },
+ get postData() {
+ return this._postData || null;
+ },
+
+ classID: Components.ID("{33d75835-722f-42c0-89cc-44f328e56a86}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIURIFixupInfo"]),
+};
+
+// Helpers
+
+/**
+ * Implementation of isDomainKnown, so we don't have to go through the
+ * service.
+ * @param {string} asciiHost
+ * @returns {boolean} whether the domain is known
+ */
+function isDomainKnown(asciiHost) {
+ if (lazy.dnsFirstForSingleWords) {
+ return true;
+ }
+ // Check if this domain is known as an actual
+ // domain (which will prevent a keyword query)
+ // Note that any processing of the host here should stay in sync with
+ // code in the front-end(s) that set the pref.
+ let lastDotIndex = asciiHost.lastIndexOf(".");
+ if (lastDotIndex == asciiHost.length - 1) {
+ asciiHost = asciiHost.substring(0, asciiHost.length - 1);
+ lastDotIndex = asciiHost.lastIndexOf(".");
+ }
+ if (lazy.knownDomains.has(asciiHost.toLowerCase())) {
+ return true;
+ }
+ // If there's no dot or only a leading dot we are done, otherwise we'll check
+ // against the known suffixes.
+ if (lastDotIndex <= 0) {
+ return false;
+ }
+ // Don't use getPublicSuffix here, since the suffix is not in the PSL,
+ // thus it couldn't tell if the suffix is made up of one or multiple
+ // dot-separated parts.
+ let lastPart = asciiHost.substr(lastDotIndex + 1);
+ let suffixes = lazy.knownSuffixes.get(lastPart);
+ if (suffixes) {
+ return Array.from(suffixes).some(s => asciiHost.endsWith(s));
+ }
+ return false;
+}
+
+/**
+ * Checks the suffix of info.fixedURI against the Public Suffix List.
+ * If the suffix is unknown due to a typo this will try to fix it up.
+ * @param {URIFixupInfo} info about the uri to check.
+ * @note this may modify the public suffix of info.fixedURI.
+ * @returns {object} result The lookup result.
+ * @returns {string} result.suffix The public suffix if one can be identified.
+ * @returns {boolean} result.hasUnknownSuffix True when the suffix is not in the
+ * Public Suffix List and it's not in knownSuffixes. False in the other cases.
+ */
+function checkAndFixPublicSuffix(info) {
+ let uri = info.fixedURI;
+ let asciiHost = uri?.asciiHost;
+ if (
+ !asciiHost ||
+ !asciiHost.includes(".") ||
+ asciiHost.endsWith(".") ||
+ isDomainKnown(asciiHost)
+ ) {
+ return { suffix: "", hasUnknownSuffix: false };
+ }
+
+ // Quick bailouts for most common cases, according to Alexa Top 1 million.
+ if (
+ /^\w/.test(asciiHost) &&
+ (asciiHost.endsWith(".com") ||
+ asciiHost.endsWith(".net") ||
+ asciiHost.endsWith(".org") ||
+ asciiHost.endsWith(".ru") ||
+ asciiHost.endsWith(".de"))
+ ) {
+ return {
+ suffix: asciiHost.substring(asciiHost.lastIndexOf(".") + 1),
+ hasUnknownSuffix: false,
+ };
+ }
+ try {
+ let suffix = Services.eTLD.getKnownPublicSuffix(uri);
+ if (suffix) {
+ return { suffix, hasUnknownSuffix: false };
+ }
+ } catch (ex) {
+ return { suffix: "", hasUnknownSuffix: false };
+ }
+ // Suffix is unknown, try to fix most common 3 chars TLDs typos.
+ // .com is the most commonly mistyped tld, so it has more cases.
+ let suffix = Services.eTLD.getPublicSuffix(uri);
+ if (!suffix || lazy.numberRegex.test(suffix)) {
+ return { suffix: "", hasUnknownSuffix: false };
+ }
+ for (let [typo, fixed] of [
+ ["ocm", "com"],
+ ["con", "com"],
+ ["cmo", "com"],
+ ["xom", "com"],
+ ["vom", "com"],
+ ["cpm", "com"],
+ ["com'", "com"],
+ ["ent", "net"],
+ ["ner", "net"],
+ ["nte", "net"],
+ ["met", "net"],
+ ["rog", "org"],
+ ["ogr", "org"],
+ ["prg", "org"],
+ ["orh", "org"],
+ ]) {
+ if (suffix == typo) {
+ let host = uri.host.substring(0, uri.host.length - typo.length) + fixed;
+ let updatePreferredURI = info.preferredURI == info.fixedURI;
+ info.fixedURI = uri.mutate().setHost(host).finalize();
+ if (updatePreferredURI) {
+ info.preferredURI = info.fixedURI;
+ }
+ return { suffix: fixed, hasUnknownSuffix: false };
+ }
+ }
+ return { suffix: "", hasUnknownSuffix: true };
+}
+
+function tryKeywordFixupForURIInfo(uriString, fixupInfo, isPrivateContext) {
+ try {
+ let keywordInfo = Services.uriFixup.keywordToURI(
+ uriString,
+ isPrivateContext
+ );
+ fixupInfo.keywordProviderName = keywordInfo.keywordProviderName;
+ fixupInfo.keywordAsSent = keywordInfo.keywordAsSent;
+ fixupInfo.preferredURI = keywordInfo.preferredURI;
+ return true;
+ } catch (ex) {}
+ return false;
+}
+
+/**
+ * This generates an alternate fixedURI, by adding a prefix and a suffix to
+ * the fixedURI host, if and only if the protocol is http. It should _never_
+ * modify URIs with other protocols.
+ * @param {URIFixupInfo} info an URIInfo object
+ * @param {integer} fixupFlags the fixup flags
+ * @returns {boolean} Whether an alternate uri was generated
+ */
+function maybeSetAlternateFixedURI(info, fixupFlags) {
+ let uri = info.fixedURI;
+ if (
+ !(fixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI) ||
+ // Code only works for http. Not for any other protocol including https!
+ !uri.schemeIs("http") ||
+ // Security - URLs with user / password info should NOT be fixed up
+ uri.userPass ||
+ // Don't fix up hosts with ports
+ uri.port != -1
+ ) {
+ return false;
+ }
+
+ let oldHost = uri.host;
+ // Don't create an alternate uri for localhost, because it would be confusing.
+ // Ditto for 'http' and 'https' as these are frequently the result of typos, e.g.
+ // 'https//foo' (note missing : ).
+ if (oldHost == "localhost" || oldHost == "http" || oldHost == "https") {
+ return false;
+ }
+
+ // Get the prefix and suffix to stick onto the new hostname. By default these
+ // are www. & .com but they could be any other value, e.g. www. & .org
+ let newHost = maybeAddPrefixAndSuffix(oldHost);
+
+ if (newHost == oldHost) {
+ return false;
+ }
+
+ return updateHostAndScheme(info, newHost);
+}
+
+/**
+ * Try to fixup a file URI.
+ * @param {string} uriString The file URI to fix.
+ * @returns {nsIURI} a fixed uri or null.
+ * @note FileURIFixup only returns a URI if it has to add the file: protocol.
+ */
+function fileURIFixup(uriString) {
+ let attemptFixup = false;
+ let path = uriString;
+ if (AppConstants.platform == "win") {
+ // Check for "\"" in the url-string, just a drive (e.g. C:),
+ // or 'A:/...' where the "protocol" is also a single letter.
+ attemptFixup =
+ uriString.includes("\\") ||
+ (uriString[1] == ":" && (uriString.length == 2 || uriString[2] == "/"));
+ if (uriString[1] == ":" && uriString[2] == "/") {
+ path = uriString.replace(/\//g, "\\");
+ }
+ } else {
+ // UNIX: Check if it starts with "/" or "~".
+ attemptFixup = /^[~/]/.test(uriString);
+ }
+ if (attemptFixup) {
+ try {
+ // Test if this is a valid path by trying to create a local file
+ // object. The URL of that is returned if successful.
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+ return Services.io.newURI(
+ lazy.fileProtocolHandler.getURLSpecFromActualFile(file)
+ );
+ } catch (ex) {
+ // Not a file uri.
+ }
+ }
+ return null;
+}
+
+/**
+ * Tries to fixup a string to an nsIURI by adding the default protocol.
+ *
+ * Should fix things like:
+ * no-scheme.com
+ * ftp.no-scheme.com
+ * ftp4.no-scheme.com
+ * no-scheme.com/query?foo=http://www.foo.com
+ * user:pass@no-scheme.com
+ *
+ * @param {string} uriString The string to fixup.
+ * @returns {nsIURI} an nsIURI built adding the default protocol to the string,
+ * or null if fixing was not possible.
+ */
+function fixupURIProtocol(uriString) {
+ let schemePos = uriString.indexOf("://");
+ if (schemePos == -1 || schemePos > uriString.search(/[:\/]/)) {
+ uriString = "http://" + uriString;
+ }
+ try {
+ return Services.io.newURI(uriString);
+ } catch (ex) {
+ // We generated an invalid uri.
+ }
+ return null;
+}
+
+/**
+ * Tries to fixup a string to a search url.
+ * @param {string} uriString the string to fixup.
+ * @param {URIFixupInfo} fixupInfo The fixup info object, modified in-place.
+ * @param {boolean} isPrivateContext Whether this happens in a private context.
+ * @param {nsIInputStream} postData optional POST data for the search
+ * @returns {boolean} Whether the keyword fixup was succesful.
+ */
+function keywordURIFixup(uriString, fixupInfo, isPrivateContext) {
+ // Here is a few examples of strings that should be searched:
+ // "what is mozilla"
+ // "what is mozilla?"
+ // "docshell site:mozilla.org" - has a space in the origin part
+ // "?site:mozilla.org - anything that begins with a question mark
+ // "mozilla'.org" - Things that have a quote before the first dot/colon
+ // "mozilla/test" - unknown host
+ // ".mozilla", "mozilla." - starts or ends with a dot ()
+ // "user@nonQualifiedHost"
+
+ // These other strings should not be searched, because they could be URIs:
+ // "www.blah.com" - Domain with a standard or known suffix
+ // "knowndomain" - known domain
+ // "nonQualifiedHost:8888?something" - has a port
+ // "user:pass@nonQualifiedHost"
+ // "blah.com."
+
+ // We do keyword lookups if the input starts with a question mark.
+ if (uriString.startsWith("?")) {
+ return tryKeywordFixupForURIInfo(
+ fixupInfo.originalInput,
+ fixupInfo,
+ isPrivateContext
+ );
+ }
+
+ // Check for IPs.
+ const userPassword = lazy.userPasswordRegex.exec(uriString);
+ const ipString = userPassword
+ ? uriString.replace(userPassword[2], "")
+ : uriString;
+ if (lazy.IPv4LikeRegex.test(ipString) || lazy.IPv6LikeRegex.test(ipString)) {
+ return false;
+ }
+
+ // Avoid keyword lookup if we can identify a host and it's known, or ends
+ // with a dot and has some path.
+ // Note that if dnsFirstForSingleWords is true isDomainKnown will always
+ // return true, so we can avoid checking dnsFirstForSingleWords after this.
+ let asciiHost = fixupInfo.fixedURI?.asciiHost;
+ if (
+ asciiHost &&
+ (isDomainKnown(asciiHost) ||
+ (asciiHost.endsWith(".") &&
+ asciiHost.indexOf(".") != asciiHost.length - 1))
+ ) {
+ return false;
+ }
+
+ // Avoid keyword lookup if the url seems to have password.
+ if (fixupInfo.fixedURI?.password) {
+ return false;
+ }
+
+ // Even if the host is unknown, avoid keyword lookup if the string has
+ // uri-like characteristics, unless it looks like "user@unknownHost".
+ // Note we already excluded passwords at this point.
+ if (
+ !isURILike(uriString, fixupInfo.fixedURI?.displayHost) ||
+ (fixupInfo.fixedURI?.userPass && fixupInfo.fixedURI?.pathQueryRef === "/")
+ ) {
+ return tryKeywordFixupForURIInfo(
+ fixupInfo.originalInput,
+ fixupInfo,
+ isPrivateContext
+ );
+ }
+
+ return false;
+}
+
+/**
+ * Mimics the logic in Services.io.extractScheme, but avoids crossing XPConnect.
+ * This also tries to fixup the scheme if it was clearly mistyped.
+ * @param {string} uriString the string to examine
+ * @param {integer} fixupFlags The original fixup flags
+ * @returns {object}
+ * scheme: a typo fixed scheme or empty string if one could not be identified
+ * fixedSchemeUriString: uri string with a typo fixed scheme
+ * fixupChangedProtocol: true if the scheme is fixed up
+ */
+function extractScheme(uriString, fixupFlags = FIXUP_FLAG_NONE) {
+ const matches = uriString.match(lazy.possibleProtocolRegex);
+ const hasColon = matches?.[2] === ":";
+ const hasSlash2 = matches?.[3] === "//";
+
+ const isFixupSchemeTypos =
+ lazy.fixupSchemeTypos && fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS;
+
+ if (
+ !matches ||
+ (!hasColon && !hasSlash2) ||
+ (!hasColon && !isFixupSchemeTypos)
+ ) {
+ return {
+ scheme: "",
+ fixedSchemeUriString: uriString,
+ fixupChangedProtocol: false,
+ };
+ }
+
+ let scheme = matches[1].replace("\t", "").toLowerCase();
+ let fixedSchemeUriString = uriString;
+
+ if (isFixupSchemeTypos && hasSlash2) {
+ // Fix up typos for string that user would have intented as protocol.
+ const afterProtocol = uriString.substring(matches[0].length);
+ fixedSchemeUriString = `${scheme}://${afterProtocol}`;
+ }
+
+ let fixupChangedProtocol = false;
+
+ if (isFixupSchemeTypos) {
+ // Fix up common scheme typos.
+ // TODO: Use levenshtein distance here?
+ fixupChangedProtocol = [
+ ["ttp", "http"],
+ ["htp", "http"],
+ ["ttps", "https"],
+ ["tps", "https"],
+ ["ps", "https"],
+ ["htps", "https"],
+ ["ile", "file"],
+ ["le", "file"],
+ ].some(([typo, fixed]) => {
+ if (scheme === typo) {
+ scheme = fixed;
+ fixedSchemeUriString =
+ scheme + fixedSchemeUriString.substring(typo.length);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ return {
+ scheme,
+ fixedSchemeUriString,
+ fixupChangedProtocol,
+ };
+}
+
+/**
+ * View-source is a pseudo scheme. We're interested in fixing up the stuff
+ * after it. The easiest way to do that is to call this method again with
+ * the "view-source:" lopped off and then prepend it again afterwards.
+ * @param {string} uriString The original string to fixup
+ * @param {integer} fixupFlags The original fixup flags
+ * @param {nsIInputStream} postData Optional POST data for the search
+ * @returns {object} {preferredURI, postData} The fixed URI and relative postData
+ * @throws if it's not possible to fixup the url
+ */
+function fixupViewSource(uriString, fixupFlags) {
+ // We disable keyword lookup and alternate URIs so that small typos don't
+ // cause us to look at very different domains.
+ let newFixupFlags = fixupFlags & ~FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ let innerURIString = uriString.substring(12).trim();
+
+ // Prevent recursion.
+ const { scheme: innerScheme } = extractScheme(innerURIString);
+ if (innerScheme == "view-source") {
+ throw new Components.Exception(
+ "Prevent view-source recursion",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let info = Services.uriFixup.getFixupURIInfo(innerURIString, newFixupFlags);
+ if (!info.preferredURI) {
+ throw new Components.Exception(
+ "Couldn't build a valid uri",
+ Cr.NS_ERROR_MALFORMED_URI
+ );
+ }
+ return {
+ preferredURI: Services.io.newURI("view-source:" + info.preferredURI.spec),
+ postData: info.postData,
+ };
+}
+
+/**
+ * Fixup the host of fixedURI if it contains consecutive dots.
+ * @param {URIFixupInfo} info an URIInfo object
+ */
+function fixupConsecutiveDotsHost(fixupInfo) {
+ const uri = fixupInfo.fixedURI;
+
+ try {
+ if (!uri?.host.includes("..")) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ try {
+ const isPreferredEqualsToFixed = fixupInfo.preferredURI?.equals(uri);
+
+ fixupInfo.fixedURI = uri
+ .mutate()
+ .setHost(uri.host.replace(/\.+/g, "."))
+ .finalize();
+
+ if (isPreferredEqualsToFixed) {
+ fixupInfo.preferredURI = fixupInfo.fixedURI;
+ }
+ } catch (e) {
+ if (e.result !== Cr.NS_ERROR_MALFORMED_URI) {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Return whether or not given string is uri like.
+ * This function returns true like following strings.
+ * - ":8080"
+ * - "localhost:8080" (if given host is "localhost")
+ * - "/foo?bar"
+ * - "/foo#bar"
+ * @param {string} uriString.
+ * @param {string} host.
+ * @param {boolean} true if uri like.
+ */
+function isURILike(uriString, host) {
+ const indexOfSlash = uriString.indexOf("/");
+ if (
+ indexOfSlash >= 0 &&
+ (indexOfSlash < uriString.indexOf("?", indexOfSlash) ||
+ indexOfSlash < uriString.indexOf("#", indexOfSlash))
+ ) {
+ return true;
+ }
+
+ if (uriString.startsWith(host)) {
+ uriString = uriString.substring(host.length);
+ }
+
+ return lazy.portRegex.test(uriString);
+}
+
+/**
+ * Add prefix and suffix to a hostname if both are missing.
+ *
+ * If the host does not start with the prefix, add the prefix to
+ * the hostname.
+ *
+ * By default the prefix and suffix are www. and .com but they could
+ * be any value e.g. www. and .org as they use the preferences
+ * "browser.fixup.alternate.prefix" and "browser.fixup.alternative.suffix"
+ *
+ * If no changes were made, it returns an empty string.
+ *
+ * @param {string} oldHost.
+ * @return {String} Fixed up hostname or an empty string.
+ */
+function maybeAddPrefixAndSuffix(oldHost) {
+ let prefix = Services.prefs.getCharPref(
+ "browser.fixup.alternate.prefix",
+ "www."
+ );
+ let suffix = Services.prefs.getCharPref(
+ "browser.fixup.alternate.suffix",
+ ".com"
+ );
+ let newHost = "";
+ let numDots = (oldHost.match(/\./g) || []).length;
+ if (numDots == 0) {
+ newHost = prefix + oldHost + suffix;
+ } else if (numDots == 1) {
+ if (prefix && oldHost == prefix) {
+ newHost = oldHost + suffix;
+ } else if (suffix && !oldHost.startsWith(prefix)) {
+ newHost = prefix + oldHost;
+ }
+ }
+ return newHost ? newHost : oldHost;
+}
+
+/**
+ * Given an instance of URIFixupInfo, update its fixedURI.
+ *
+ * First, change the protocol to the one stored in
+ * "browser.fixup.alternate.protocol".
+ *
+ * Then, try to update fixedURI's host to newHost.
+ *
+ * @param {URIFixupInfo} info.
+ * @param {string} newHost.
+ * @return {boolean}
+ * True, if info was updated without any errors.
+ * False, if NS_ERROR_MALFORMED_URI error.
+ * @throws If a non-NS_ERROR_MALFORMED_URI error occurs.
+ */
+function updateHostAndScheme(info, newHost) {
+ let oldHost = info.fixedURI.host;
+ let oldScheme = info.fixedURI.scheme;
+ try {
+ info.fixedURI = info.fixedURI
+ .mutate()
+ .setScheme(lazy.alternateProtocol)
+ .setHost(newHost)
+ .finalize();
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
+ throw ex;
+ }
+ return false;
+ }
+ if (oldScheme != info.fixedURI.scheme) {
+ info.fixupChangedProtocol = true;
+ }
+ if (oldHost != info.fixedURI.host) {
+ info.fixupCreatedAlternateURI = true;
+ }
+ return true;
+}
diff --git a/docshell/base/WindowContext.cpp b/docshell/base/WindowContext.cpp
new file mode 100644
index 0000000000..d2032bc551
--- /dev/null
+++ b/docshell/base/WindowContext.cpp
@@ -0,0 +1,697 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalActorsBinding.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/dom/SyncedContextInlines.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/UserActivationIPCUtils.h"
+#include "mozilla/PermissionDelegateIPCUtils.h"
+#include "mozilla/RFPTargetIPCUtils.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIScriptError.h"
+#include "nsIWebProgressListener.h"
+#include "nsIXULRuntime.h"
+#include "nsRefPtrHashtable.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+// Explicit specialization of the `Transaction` type. Required by the `extern
+// template class` declaration in the header.
+template class syncedcontext::Transaction<WindowContext>;
+
+static LazyLogModule gWindowContextLog("WindowContext");
+static LazyLogModule gWindowContextSyncLog("WindowContextSync");
+
+extern mozilla::LazyLogModule gUserInteractionPRLog;
+
+#define USER_ACTIVATION_LOG(msg, ...) \
+ MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+using WindowContextByIdMap = nsTHashMap<nsUint64HashKey, WindowContext*>;
+static StaticAutoPtr<WindowContextByIdMap> gWindowContexts;
+
+/* static */
+LogModule* WindowContext::GetLog() { return gWindowContextLog; }
+
+/* static */
+LogModule* WindowContext::GetSyncLog() { return gWindowContextSyncLog; }
+
+/* static */
+already_AddRefed<WindowContext> WindowContext::GetById(
+ uint64_t aInnerWindowId) {
+ if (!gWindowContexts) {
+ return nullptr;
+ }
+ return do_AddRef(gWindowContexts->Get(aInnerWindowId));
+}
+
+BrowsingContextGroup* WindowContext::Group() const {
+ return mBrowsingContext->Group();
+}
+
+WindowGlobalParent* WindowContext::Canonical() {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ return static_cast<WindowGlobalParent*>(this);
+}
+
+bool WindowContext::IsCurrent() const {
+ return mBrowsingContext->mCurrentWindowContext == this;
+}
+
+bool WindowContext::IsInBFCache() {
+ if (mozilla::SessionHistoryInParent()) {
+ return mBrowsingContext->IsInBFCache();
+ }
+ return TopWindowContext()->GetWindowStateSaved();
+}
+
+nsGlobalWindowInner* WindowContext::GetInnerWindow() const {
+ return mWindowGlobalChild ? mWindowGlobalChild->GetWindowGlobal() : nullptr;
+}
+
+Document* WindowContext::GetDocument() const {
+ nsGlobalWindowInner* innerWindow = GetInnerWindow();
+ return innerWindow ? innerWindow->GetDocument() : nullptr;
+}
+
+Document* WindowContext::GetExtantDoc() const {
+ nsGlobalWindowInner* innerWindow = GetInnerWindow();
+ return innerWindow ? innerWindow->GetExtantDoc() : nullptr;
+}
+
+WindowGlobalChild* WindowContext::GetWindowGlobalChild() const {
+ return mWindowGlobalChild;
+}
+
+WindowContext* WindowContext::GetParentWindowContext() {
+ return mBrowsingContext->GetParentWindowContext();
+}
+
+WindowContext* WindowContext::TopWindowContext() {
+ WindowContext* current = this;
+ while (current->GetParentWindowContext()) {
+ current = current->GetParentWindowContext();
+ }
+ return current;
+}
+
+bool WindowContext::IsTop() const { return mBrowsingContext->IsTop(); }
+
+bool WindowContext::SameOriginWithTop() const {
+ return mBrowsingContext->SameOriginWithTop();
+}
+
+nsIGlobalObject* WindowContext::GetParentObject() const {
+ return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+}
+
+void WindowContext::AppendChildBrowsingContext(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_DIAGNOSTIC_ASSERT(Group() == aBrowsingContext->Group(),
+ "Mismatched groups?");
+ MOZ_DIAGNOSTIC_ASSERT(!mChildren.Contains(aBrowsingContext));
+
+ mChildren.AppendElement(aBrowsingContext);
+ if (!aBrowsingContext->IsEmbedderTypeObjectOrEmbed()) {
+ mNonSyntheticChildren.AppendElement(aBrowsingContext);
+ }
+
+ // If we're the current WindowContext in our BrowsingContext, make sure to
+ // clear any cached `children` value.
+ if (IsCurrent()) {
+ BrowsingContext_Binding::ClearCachedChildrenValue(mBrowsingContext);
+ }
+}
+
+void WindowContext::RemoveChildBrowsingContext(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_DIAGNOSTIC_ASSERT(Group() == aBrowsingContext->Group(),
+ "Mismatched groups?");
+
+ mChildren.RemoveElement(aBrowsingContext);
+ mNonSyntheticChildren.RemoveElement(aBrowsingContext);
+
+ // If we're the current WindowContext in our BrowsingContext, make sure to
+ // clear any cached `children` value.
+ if (IsCurrent()) {
+ BrowsingContext_Binding::ClearCachedChildrenValue(mBrowsingContext);
+ }
+}
+
+void WindowContext::UpdateChildSynthetic(BrowsingContext* aBrowsingContext,
+ bool aIsSynthetic) {
+ if (aIsSynthetic) {
+ mNonSyntheticChildren.RemoveElement(aBrowsingContext);
+ } else {
+ // The same BrowsingContext will be reused for error pages, so it can be in
+ // the list already.
+ if (!mNonSyntheticChildren.Contains(aBrowsingContext)) {
+ mNonSyntheticChildren.AppendElement(aBrowsingContext);
+ }
+ }
+}
+
+void WindowContext::SendCommitTransaction(ContentParent* aParent,
+ const BaseTransaction& aTxn,
+ uint64_t aEpoch) {
+ Unused << aParent->SendCommitWindowContextTransaction(this, aTxn, aEpoch);
+}
+
+void WindowContext::SendCommitTransaction(ContentChild* aChild,
+ const BaseTransaction& aTxn,
+ uint64_t aEpoch) {
+ aChild->SendCommitWindowContextTransaction(this, aTxn, aEpoch);
+}
+
+bool WindowContext::CheckOnlyOwningProcessCanSet(ContentParent* aSource) {
+ if (IsInProcess()) {
+ return true;
+ }
+
+ if (XRE_IsParentProcess() && aSource) {
+ return Canonical()->GetContentParent() == aSource;
+ }
+
+ return false;
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsSecure>, const bool& aIsSecure,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_AllowMixedContent>,
+ const bool& aAllowMixedContent,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_HasBeforeUnload>,
+ const bool& aHasBeforeUnload,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_CookieBehavior>,
+ const Maybe<uint32_t>& aValue,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsOnContentBlockingAllowList>,
+ const bool& aValue, ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsThirdPartyWindow>,
+ const bool& IsThirdPartyWindow,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsThirdPartyTrackingResourceWindow>,
+ const bool& aIsThirdPartyTrackingResourceWindow,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_UsingStorageAccess>,
+ const bool& aUsingStorageAccess,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_ShouldResistFingerprinting>,
+ const bool& aShouldResistFingerprinting,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_OverriddenFingerprintingSettings>,
+ const Maybe<RFPTarget>& aValue,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsSecureContext>,
+ const bool& aIsSecureContext,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsOriginalFrameSource>,
+ const bool& aIsOriginalFrameSource,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_DocTreeHadMedia>, const bool& aValue,
+ ContentParent* aSource) {
+ return IsTop();
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_AutoplayPermission>,
+ const uint32_t& aValue, ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_ShortcutsPermission>,
+ const uint32_t& aValue, ContentParent* aSource) {
+ return IsTop() && CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_ActiveMediaSessionContextId>,
+ const Maybe<uint64_t>& aValue,
+ ContentParent* aSource) {
+ return IsTop();
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_PopupPermission>, const uint32_t&,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(
+ FieldIndex<IDX_DelegatedPermissions>,
+ const PermissionDelegateHandler::DelegatedPermissionList& aValue,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(
+ FieldIndex<IDX_DelegatedExactHostMatchPermissions>,
+ const PermissionDelegateHandler::DelegatedPermissionList& aValue,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_IsLocalIP>, const bool& aValue,
+ ContentParent* aSource) {
+ return CheckOnlyOwningProcessCanSet(aSource);
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+ ContentParent* aSource) {
+ return (XRE_IsParentProcess() && !aSource) ||
+ CheckOnlyOwningProcessCanSet(aSource);
+}
+
+void WindowContext::DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue) {
+ RecomputeCanExecuteScripts();
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_HasActivePeerConnections>, bool,
+ ContentParent*) {
+ return XRE_IsParentProcess() && IsTop();
+}
+
+void WindowContext::RecomputeCanExecuteScripts(bool aApplyChanges) {
+ const bool old = mCanExecuteScripts;
+ if (!AllowJavascript()) {
+ // Scripting has been explicitly disabled on our WindowContext.
+ mCanExecuteScripts = false;
+ } else {
+ // Otherwise, inherit.
+ mCanExecuteScripts = mBrowsingContext->CanExecuteScripts();
+ }
+
+ if (aApplyChanges && old != mCanExecuteScripts) {
+ // Inform our active DOM window.
+ if (nsGlobalWindowInner* window = GetInnerWindow()) {
+ // Only update scriptability if the window is current. Windows will have
+ // scriptability disabled when entering the bfcache and updated when
+ // coming out.
+ if (window->IsCurrentInnerWindow()) {
+ auto& scriptability =
+ xpc::Scriptability::Get(window->GetGlobalJSObject());
+ scriptability.SetWindowAllowsScript(mCanExecuteScripts);
+ }
+ }
+
+ for (const RefPtr<BrowsingContext>& child : Children()) {
+ child->RecomputeCanExecuteScripts();
+ }
+ }
+}
+
+void WindowContext::DidSet(FieldIndex<IDX_SHEntryHasUserInteraction>,
+ bool aOldValue) {
+ MOZ_ASSERT(
+ TopWindowContext() == this,
+ "SHEntryHasUserInteraction can only be set on the top window context");
+ // This field is set when the child notifies us of new user interaction, so we
+ // also set the currently active shentry in the parent as having interaction.
+ if (XRE_IsParentProcess() && mBrowsingContext) {
+ SessionHistoryEntry* activeEntry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (activeEntry && GetSHEntryHasUserInteraction()) {
+ activeEntry->SetHasUserInteraction(true);
+ }
+ }
+}
+
+void WindowContext::DidSet(FieldIndex<IDX_UserActivationStateAndModifiers>) {
+ MOZ_ASSERT_IF(!IsInProcess(), mUserGestureStart.IsNull());
+ USER_ACTIVATION_LOG("Set user gesture activation 0x%02" PRIu8
+ " for %s browsing context 0x%08" PRIx64,
+ GetUserActivationStateAndModifiers(),
+ XRE_IsParentProcess() ? "Parent" : "Child", Id());
+ if (IsInProcess()) {
+ USER_ACTIVATION_LOG(
+ "Set user gesture start time for %s browsing context 0x%08" PRIx64,
+ XRE_IsParentProcess() ? "Parent" : "Child", Id());
+ if (GetUserActivationState() == UserActivation::State::FullActivated) {
+ mUserGestureStart = TimeStamp::Now();
+ } else if (GetUserActivationState() == UserActivation::State::None) {
+ mUserGestureStart = TimeStamp();
+ }
+ }
+}
+
+void WindowContext::DidSet(FieldIndex<IDX_HasReportedShadowDOMUsage>,
+ bool aOldValue) {
+ if (!aOldValue && GetHasReportedShadowDOMUsage() && IsInProcess()) {
+ MOZ_ASSERT(TopWindowContext() == this);
+ if (mBrowsingContext) {
+ Document* topLevelDoc = mBrowsingContext->GetDocument();
+ if (topLevelDoc) {
+ nsAutoString uri;
+ Unused << topLevelDoc->GetDocumentURI(uri);
+ if (!uri.IsEmpty()) {
+ nsAutoString msg = u"Shadow DOM used in ["_ns + uri +
+ u"] or in some of its subdocuments."_ns;
+ nsContentUtils::ReportToConsoleNonLocalized(
+ msg, nsIScriptError::infoFlag, "DOM"_ns, topLevelDoc);
+ }
+ }
+ }
+ }
+}
+
+bool WindowContext::CanSet(FieldIndex<IDX_WindowStateSaved>, bool aValue,
+ ContentParent* aSource) {
+ return !mozilla::SessionHistoryInParent() && IsTop() &&
+ CheckOnlyOwningProcessCanSet(aSource);
+}
+
+void WindowContext::CreateFromIPC(IPCInitializer&& aInit) {
+ MOZ_RELEASE_ASSERT(XRE_IsContentProcess(),
+ "Should be a WindowGlobalParent in the parent");
+
+ RefPtr<BrowsingContext> bc = BrowsingContext::Get(aInit.mBrowsingContextId);
+ MOZ_RELEASE_ASSERT(bc);
+
+ if (bc->IsDiscarded()) {
+ // If we have already closed our browsing context, the
+ // WindowGlobalChild actor is bound to be destroyed soon and it's
+ // safe to ignore creating the WindowContext.
+ return;
+ }
+
+ RefPtr<WindowContext> context = new WindowContext(
+ bc, aInit.mInnerWindowId, aInit.mOuterWindowId, std::move(aInit.mFields));
+ context->Init();
+}
+
+void WindowContext::Init() {
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("Registering 0x%" PRIx64 " (bc=0x%" PRIx64 ")", mInnerWindowId,
+ mBrowsingContext->Id()));
+
+ // Register the WindowContext in the `WindowContextByIdMap`.
+ if (!gWindowContexts) {
+ gWindowContexts = new WindowContextByIdMap();
+ ClearOnShutdown(&gWindowContexts);
+ }
+ auto& entry = gWindowContexts->LookupOrInsert(mInnerWindowId);
+ MOZ_RELEASE_ASSERT(!entry, "Duplicate WindowContext for ID!");
+ entry = this;
+
+ // Register this to the browsing context.
+ mBrowsingContext->RegisterWindowContext(this);
+ Group()->Register(this);
+}
+
+void WindowContext::Discard() {
+ MOZ_LOG(GetLog(), LogLevel::Debug,
+ ("Discarding 0x%" PRIx64 " (bc=0x%" PRIx64 ")", mInnerWindowId,
+ mBrowsingContext->Id()));
+ if (mIsDiscarded) {
+ return;
+ }
+
+ mIsDiscarded = true;
+ if (gWindowContexts) {
+ gWindowContexts->Remove(InnerWindowId());
+ }
+ mBrowsingContext->UnregisterWindowContext(this);
+ Group()->Unregister(this);
+}
+
+void WindowContext::AddSecurityState(uint32_t aStateFlags) {
+ MOZ_ASSERT(TopWindowContext() == this);
+ MOZ_ASSERT((aStateFlags &
+ (nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT |
+ nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT |
+ nsIWebProgressListener::STATE_BLOCKED_MIXED_DISPLAY_CONTENT |
+ nsIWebProgressListener::STATE_BLOCKED_MIXED_ACTIVE_CONTENT |
+ nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADED |
+ nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED |
+ nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADED_FIRST)) ==
+ aStateFlags,
+ "Invalid flags specified!");
+
+ if (XRE_IsParentProcess()) {
+ Canonical()->AddSecurityState(aStateFlags);
+ } else {
+ ContentChild* child = ContentChild::GetSingleton();
+ child->SendAddSecurityState(this, aStateFlags);
+ }
+}
+
+void WindowContext::NotifyUserGestureActivation(
+ UserActivation::Modifiers
+ aModifiers /* = UserActivation::Modifiers::None() */) {
+ UserActivation::StateAndModifiers stateAndModifiers;
+ stateAndModifiers.SetState(UserActivation::State::FullActivated);
+ stateAndModifiers.SetModifiers(aModifiers);
+ Unused << SetUserActivationStateAndModifiers(stateAndModifiers.GetRawData());
+}
+
+void WindowContext::NotifyResetUserGestureActivation() {
+ UserActivation::StateAndModifiers stateAndModifiers;
+ stateAndModifiers.SetState(UserActivation::State::None);
+ Unused << SetUserActivationStateAndModifiers(stateAndModifiers.GetRawData());
+}
+
+bool WindowContext::HasBeenUserGestureActivated() {
+ return GetUserActivationState() != UserActivation::State::None;
+}
+
+const TimeStamp& WindowContext::GetUserGestureStart() const {
+ MOZ_ASSERT(IsInProcess());
+ return mUserGestureStart;
+}
+
+bool WindowContext::HasValidTransientUserGestureActivation() {
+ MOZ_ASSERT(IsInProcess());
+
+ if (GetUserActivationState() != UserActivation::State::FullActivated) {
+ // mUserGestureStart should be null if the document hasn't ever been
+ // activated by user gesture
+ MOZ_ASSERT_IF(GetUserActivationState() == UserActivation::State::None,
+ mUserGestureStart.IsNull());
+ return false;
+ }
+
+ MOZ_ASSERT(!mUserGestureStart.IsNull(),
+ "mUserGestureStart shouldn't be null if the document has ever "
+ "been activated by user gesture");
+ TimeDuration timeout = TimeDuration::FromMilliseconds(
+ StaticPrefs::dom_user_activation_transient_timeout());
+
+ return timeout <= TimeDuration() ||
+ (TimeStamp::Now() - mUserGestureStart) <= timeout;
+}
+
+bool WindowContext::ConsumeTransientUserGestureActivation() {
+ MOZ_ASSERT(IsInProcess());
+ MOZ_ASSERT(IsCurrent());
+
+ if (!HasValidTransientUserGestureActivation()) {
+ return false;
+ }
+
+ BrowsingContext* top = mBrowsingContext->Top();
+ top->PreOrderWalk([&](BrowsingContext* aBrowsingContext) {
+ WindowContext* windowContext = aBrowsingContext->GetCurrentWindowContext();
+ if (windowContext && windowContext->GetUserActivationState() ==
+ UserActivation::State::FullActivated) {
+ auto stateAndModifiers = UserActivation::StateAndModifiers(
+ GetUserActivationStateAndModifiers());
+ stateAndModifiers.SetState(UserActivation::State::HasBeenActivated);
+ Unused << windowContext->SetUserActivationStateAndModifiers(
+ stateAndModifiers.GetRawData());
+ }
+ });
+
+ return true;
+}
+
+bool WindowContext::GetTransientUserGestureActivationModifiers(
+ UserActivation::Modifiers* aModifiers) {
+ if (!HasValidTransientUserGestureActivation()) {
+ return false;
+ }
+
+ auto stateAndModifiers =
+ UserActivation::StateAndModifiers(GetUserActivationStateAndModifiers());
+ *aModifiers = stateAndModifiers.GetModifiers();
+ return true;
+}
+
+bool WindowContext::CanShowPopup() {
+ uint32_t permit = GetPopupPermission();
+ if (permit == nsIPermissionManager::ALLOW_ACTION) {
+ return true;
+ }
+ if (permit == nsIPermissionManager::DENY_ACTION) {
+ return false;
+ }
+
+ return !StaticPrefs::dom_disable_open_during_load();
+}
+
+void WindowContext::TransientSetHasActivePeerConnections() {
+ if (!IsTop()) {
+ return;
+ }
+
+ mFields.SetWithoutSyncing<IDX_HasActivePeerConnections>(true);
+}
+
+WindowContext::IPCInitializer WindowContext::GetIPCInitializer() {
+ IPCInitializer init;
+ init.mInnerWindowId = mInnerWindowId;
+ init.mOuterWindowId = mOuterWindowId;
+ init.mBrowsingContextId = mBrowsingContext->Id();
+ init.mFields = mFields.RawValues();
+ return init;
+}
+
+WindowContext::WindowContext(BrowsingContext* aBrowsingContext,
+ uint64_t aInnerWindowId, uint64_t aOuterWindowId,
+ FieldValues&& aInit)
+ : mFields(std::move(aInit)),
+ mInnerWindowId(aInnerWindowId),
+ mOuterWindowId(aOuterWindowId),
+ mBrowsingContext(aBrowsingContext) {
+ MOZ_ASSERT(mBrowsingContext);
+ MOZ_ASSERT(mInnerWindowId);
+ MOZ_ASSERT(mOuterWindowId);
+ RecomputeCanExecuteScripts(/* aApplyChanges */ false);
+}
+
+WindowContext::~WindowContext() {
+ if (gWindowContexts) {
+ gWindowContexts->Remove(InnerWindowId());
+ }
+}
+
+JSObject* WindowContext::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return WindowContext_Binding::Wrap(cx, this, aGivenProto);
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WindowContext)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WindowContext)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WindowContext)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WindowContext)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WindowContext)
+ if (gWindowContexts) {
+ gWindowContexts->Remove(tmp->InnerWindowId());
+ }
+
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowsingContext)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildren)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mNonSyntheticChildren)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WindowContext)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowsingContext)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildren)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNonSyntheticChildren)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+} // namespace dom
+
+namespace ipc {
+
+void IPDLParamTraits<dom::MaybeDiscarded<dom::WindowContext>>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::MaybeDiscarded<dom::WindowContext>& aParam) {
+ uint64_t id = aParam.ContextId();
+ WriteIPDLParam(aWriter, aActor, id);
+}
+
+bool IPDLParamTraits<dom::MaybeDiscarded<dom::WindowContext>>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::MaybeDiscarded<dom::WindowContext>* aResult) {
+ uint64_t id = 0;
+ if (!ReadIPDLParam(aReader, aActor, &id)) {
+ return false;
+ }
+
+ if (id == 0) {
+ *aResult = nullptr;
+ } else if (RefPtr<dom::WindowContext> wc = dom::WindowContext::GetById(id)) {
+ *aResult = std::move(wc);
+ } else {
+ aResult->SetDiscarded(id);
+ }
+ return true;
+}
+
+void IPDLParamTraits<dom::WindowContext::IPCInitializer>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::WindowContext::IPCInitializer& aInit) {
+ // Write actor ID parameters.
+ WriteIPDLParam(aWriter, aActor, aInit.mInnerWindowId);
+ WriteIPDLParam(aWriter, aActor, aInit.mOuterWindowId);
+ WriteIPDLParam(aWriter, aActor, aInit.mBrowsingContextId);
+ WriteIPDLParam(aWriter, aActor, aInit.mFields);
+}
+
+bool IPDLParamTraits<dom::WindowContext::IPCInitializer>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::WindowContext::IPCInitializer* aInit) {
+ // Read actor ID parameters.
+ return ReadIPDLParam(aReader, aActor, &aInit->mInnerWindowId) &&
+ ReadIPDLParam(aReader, aActor, &aInit->mOuterWindowId) &&
+ ReadIPDLParam(aReader, aActor, &aInit->mBrowsingContextId) &&
+ ReadIPDLParam(aReader, aActor, &aInit->mFields);
+}
+
+template struct IPDLParamTraits<dom::WindowContext::BaseTransaction>;
+
+} // namespace ipc
+} // namespace mozilla
diff --git a/docshell/base/WindowContext.h b/docshell/base/WindowContext.h
new file mode 100644
index 0000000000..fda4030045
--- /dev/null
+++ b/docshell/base/WindowContext.h
@@ -0,0 +1,429 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_WindowContext_h
+#define mozilla_dom_WindowContext_h
+
+#include "mozilla/PermissionDelegateHandler.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/Span.h"
+#include "mozilla/dom/MaybeDiscarded.h"
+#include "mozilla/dom/SyncedContext.h"
+#include "mozilla/dom/UserActivation.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsILoadInfo.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+class nsGlobalWindowInner;
+
+namespace mozilla {
+class LogModule;
+
+namespace dom {
+
+class WindowGlobalChild;
+class WindowGlobalParent;
+class WindowGlobalInit;
+class BrowsingContext;
+class BrowsingContextGroup;
+
+#define MOZ_EACH_WC_FIELD(FIELD) \
+ /* Whether the SHEntry associated with the current top-level \
+ * window has already seen user interaction. \
+ * As such, this will be reset to false when a new SHEntry is \
+ * created without changing the WC (e.g. when using pushState or \
+ * sub-frame navigation) \
+ * This flag is set for optimization purposes, to avoid \
+ * having to get the top SHEntry and update it on every \
+ * user interaction. \
+ * This is only meaningful on the top-level WC. */ \
+ FIELD(SHEntryHasUserInteraction, bool) \
+ FIELD(CookieBehavior, Maybe<uint32_t>) \
+ FIELD(IsOnContentBlockingAllowList, bool) \
+ /* Whether the given window hierarchy is third party. See \
+ * ThirdPartyUtil::IsThirdPartyWindow for details */ \
+ FIELD(IsThirdPartyWindow, bool) \
+ /* Whether this window's channel has been marked as a third-party \
+ * tracking resource */ \
+ FIELD(IsThirdPartyTrackingResourceWindow, bool) \
+ /* Whether this window is using its unpartitioned cookies due to \
+ * the Storage Access API */ \
+ FIELD(UsingStorageAccess, bool) \
+ FIELD(ShouldResistFingerprinting, bool) \
+ FIELD(OverriddenFingerprintingSettings, Maybe<RFPTarget>) \
+ FIELD(IsSecureContext, bool) \
+ FIELD(IsOriginalFrameSource, bool) \
+ /* Mixed-Content: If the corresponding documentURI is https, \
+ * then this flag is true. */ \
+ FIELD(IsSecure, bool) \
+ /* Whether the user has overriden the mixed content blocker to allow \
+ * mixed content loads to happen */ \
+ FIELD(AllowMixedContent, bool) \
+ /* Whether this window has registered a "beforeunload" event \
+ * handler */ \
+ FIELD(HasBeforeUnload, bool) \
+ /* Controls whether the WindowContext is currently considered to be \
+ * activated by a gesture */ \
+ FIELD(UserActivationStateAndModifiers, \
+ UserActivation::StateAndModifiers::DataT) \
+ FIELD(EmbedderPolicy, nsILoadInfo::CrossOriginEmbedderPolicy) \
+ /* True if this document tree contained at least a HTMLMediaElement. \
+ * This should only be set on top level context. */ \
+ FIELD(DocTreeHadMedia, bool) \
+ FIELD(AutoplayPermission, uint32_t) \
+ FIELD(ShortcutsPermission, uint32_t) \
+ /* Store the Id of the browsing context where active media session \
+ * exists on the top level window context */ \
+ FIELD(ActiveMediaSessionContextId, Maybe<uint64_t>) \
+ /* ALLOW_ACTION if it is allowed to open popups for the sub-tree \
+ * starting and including the current WindowContext */ \
+ FIELD(PopupPermission, uint32_t) \
+ FIELD(DelegatedPermissions, \
+ PermissionDelegateHandler::DelegatedPermissionList) \
+ FIELD(DelegatedExactHostMatchPermissions, \
+ PermissionDelegateHandler::DelegatedPermissionList) \
+ FIELD(HasReportedShadowDOMUsage, bool) \
+ /* Whether the principal of this window is for a local \
+ * IP address */ \
+ FIELD(IsLocalIP, bool) \
+ /* Whether any of the windows in the subtree rooted at this window has \
+ * active peer connections or not (only set on the top window). */ \
+ FIELD(HasActivePeerConnections, bool) \
+ /* Whether we can execute scripts in this WindowContext. Has no effect \
+ * unless scripts are also allowed in the BrowsingContext. */ \
+ FIELD(AllowJavascript, bool) \
+ /* If this field is `true`, it means that this WindowContext's \
+ * WindowState was saved to be stored in the legacy (non-SHIP) BFCache \
+ * implementation. Always false for SHIP */ \
+ FIELD(WindowStateSaved, bool)
+
+class WindowContext : public nsISupports, public nsWrapperCache {
+ MOZ_DECL_SYNCED_CONTEXT(WindowContext, MOZ_EACH_WC_FIELD)
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WindowContext)
+
+ public:
+ static already_AddRefed<WindowContext> GetById(uint64_t aInnerWindowId);
+ static LogModule* GetLog();
+ static LogModule* GetSyncLog();
+
+ BrowsingContext* GetBrowsingContext() const { return mBrowsingContext; }
+ BrowsingContextGroup* Group() const;
+ uint64_t Id() const { return InnerWindowId(); }
+ uint64_t InnerWindowId() const { return mInnerWindowId; }
+ uint64_t OuterWindowId() const { return mOuterWindowId; }
+ bool IsDiscarded() const { return mIsDiscarded; }
+
+ // Returns `true` if this WindowContext is the current WindowContext in its
+ // BrowsingContext.
+ bool IsCurrent() const;
+
+ // Returns `true` if this WindowContext is currently in the BFCache.
+ bool IsInBFCache();
+
+ bool IsInProcess() const { return mIsInProcess; }
+
+ bool HasBeforeUnload() const { return GetHasBeforeUnload(); }
+
+ bool IsLocalIP() const { return GetIsLocalIP(); }
+
+ bool ShouldResistFingerprinting() const {
+ return GetShouldResistFingerprinting();
+ }
+
+ Nullable<uint64_t> GetOverriddenFingerprintingSettingsWebIDL() const {
+ Maybe<RFPTarget> overriddenFingerprintingSettings =
+ GetOverriddenFingerprintingSettings();
+
+ return overriddenFingerprintingSettings.isSome()
+ ? Nullable<uint64_t>(
+ uint64_t(overriddenFingerprintingSettings.ref()))
+ : Nullable<uint64_t>();
+ }
+
+ nsGlobalWindowInner* GetInnerWindow() const;
+ Document* GetDocument() const;
+ Document* GetExtantDoc() const;
+
+ WindowGlobalChild* GetWindowGlobalChild() const;
+
+ // Get the parent WindowContext of this WindowContext, taking the BFCache into
+ // account. This will not cross chrome/content <browser> boundaries.
+ WindowContext* GetParentWindowContext();
+ WindowContext* TopWindowContext();
+
+ bool SameOriginWithTop() const;
+
+ bool IsTop() const;
+
+ Span<RefPtr<BrowsingContext>> Children() { return mChildren; }
+
+ // The filtered version of `Children()`, which contains no browsing contexts
+ // for synthetic documents as created by object loading content.
+ Span<RefPtr<BrowsingContext>> NonSyntheticChildren() {
+ return mNonSyntheticChildren;
+ }
+
+ // Cast this object to it's parent-process canonical form.
+ WindowGlobalParent* Canonical();
+
+ nsIGlobalObject* GetParentObject() const;
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void Discard();
+
+ struct IPCInitializer {
+ uint64_t mInnerWindowId;
+ uint64_t mOuterWindowId;
+ uint64_t mBrowsingContextId;
+
+ FieldValues mFields;
+ };
+ IPCInitializer GetIPCInitializer();
+
+ static void CreateFromIPC(IPCInitializer&& aInit);
+
+ // Add new security state flags.
+ // These should be some of the nsIWebProgressListener 'HTTPS_ONLY_MODE' or
+ // 'MIXED' state flags, and should only be called on the top window context.
+ void AddSecurityState(uint32_t aStateFlags);
+
+ UserActivation::State GetUserActivationState() const {
+ return UserActivation::StateAndModifiers(
+ GetUserActivationStateAndModifiers())
+ .GetState();
+ }
+
+ // This function would be called when its corresponding window is activated
+ // by user gesture.
+ void NotifyUserGestureActivation(
+ UserActivation::Modifiers aModifiers = UserActivation::Modifiers::None());
+
+ // This function would be called when we want to reset the user gesture
+ // activation flag.
+ void NotifyResetUserGestureActivation();
+
+ // Return true if its corresponding window has been activated by user
+ // gesture.
+ bool HasBeenUserGestureActivated();
+
+ // Return true if its corresponding window has transient user gesture
+ // activation and the transient user gesture activation haven't yet timed
+ // out.
+ bool HasValidTransientUserGestureActivation();
+
+ // See `mUserGestureStart`.
+ const TimeStamp& GetUserGestureStart() const;
+
+ // Return true if the corresponding window has valid transient user gesture
+ // activation and the transient user gesture activation had been consumed
+ // successfully.
+ bool ConsumeTransientUserGestureActivation();
+
+ bool GetTransientUserGestureActivationModifiers(
+ UserActivation::Modifiers* aModifiers);
+
+ bool CanShowPopup();
+
+ bool AllowJavascript() const { return GetAllowJavascript(); }
+ bool CanExecuteScripts() const { return mCanExecuteScripts; }
+
+ void TransientSetHasActivePeerConnections();
+
+ protected:
+ WindowContext(BrowsingContext* aBrowsingContext, uint64_t aInnerWindowId,
+ uint64_t aOuterWindowId, FieldValues&& aFields);
+ virtual ~WindowContext();
+
+ virtual void Init();
+
+ private:
+ friend class BrowsingContext;
+ friend class WindowGlobalChild;
+ friend class WindowGlobalActor;
+
+ void AppendChildBrowsingContext(BrowsingContext* aBrowsingContext);
+ void RemoveChildBrowsingContext(BrowsingContext* aBrowsingContext);
+
+ // Update non-synthetic children based on whether `aBrowsingContext`
+ // is synthetic or not. Regardless the synthetic of `aBrowsingContext`, it is
+ // kept in this WindowContext's all children list.
+ void UpdateChildSynthetic(BrowsingContext* aBrowsingContext,
+ bool aIsSynthetic);
+
+ // Send a given `BaseTransaction` object to the correct remote.
+ void SendCommitTransaction(ContentParent* aParent,
+ const BaseTransaction& aTxn, uint64_t aEpoch);
+ void SendCommitTransaction(ContentChild* aChild, const BaseTransaction& aTxn,
+ uint64_t aEpoch);
+
+ bool CheckOnlyOwningProcessCanSet(ContentParent* aSource);
+
+ // Overload `CanSet` to get notifications for a particular field being set.
+ bool CanSet(FieldIndex<IDX_IsSecure>, const bool& aIsSecure,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_AllowMixedContent>, const bool& aAllowMixedContent,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_HasBeforeUnload>, const bool& aHasBeforeUnload,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_CookieBehavior>, const Maybe<uint32_t>& aValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_IsOnContentBlockingAllowList>, const bool& aValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_EmbedderPolicy>, const bool& aValue,
+ ContentParent* aSource) {
+ return true;
+ }
+
+ bool CanSet(FieldIndex<IDX_IsThirdPartyWindow>,
+ const bool& IsThirdPartyWindow, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_IsThirdPartyTrackingResourceWindow>,
+ const bool& aIsThirdPartyTrackingResourceWindow,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_UsingStorageAccess>,
+ const bool& aUsingStorageAccess, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_ShouldResistFingerprinting>,
+ const bool& aShouldResistFingerprinting, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_OverriddenFingerprintingSettings>,
+ const Maybe<RFPTarget>& aValue, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_IsSecureContext>, const bool& aIsSecureContext,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_IsOriginalFrameSource>,
+ const bool& aIsOriginalFrameSource, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_DocTreeHadMedia>, const bool& aValue,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_AutoplayPermission>, const uint32_t& aValue,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_ShortcutsPermission>, const uint32_t& aValue,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_ActiveMediaSessionContextId>,
+ const Maybe<uint64_t>& aValue, ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_PopupPermission>, const uint32_t&,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_SHEntryHasUserInteraction>,
+ const bool& aSHEntryHasUserInteraction, ContentParent* aSource) {
+ return true;
+ }
+ bool CanSet(FieldIndex<IDX_DelegatedPermissions>,
+ const PermissionDelegateHandler::DelegatedPermissionList& aValue,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_DelegatedExactHostMatchPermissions>,
+ const PermissionDelegateHandler::DelegatedPermissionList& aValue,
+ ContentParent* aSource);
+ bool CanSet(FieldIndex<IDX_UserActivationStateAndModifiers>,
+ const UserActivation::StateAndModifiers::DataT&
+ aUserActivationStateAndModifiers,
+ ContentParent* aSource) {
+ return true;
+ }
+
+ bool CanSet(FieldIndex<IDX_HasReportedShadowDOMUsage>, const bool& aValue,
+ ContentParent* aSource) {
+ return true;
+ }
+
+ bool CanSet(FieldIndex<IDX_IsLocalIP>, const bool& aValue,
+ ContentParent* aSource);
+
+ bool CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+ ContentParent* aSource);
+ void DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue);
+
+ bool CanSet(FieldIndex<IDX_HasActivePeerConnections>, bool, ContentParent*);
+
+ void DidSet(FieldIndex<IDX_HasReportedShadowDOMUsage>, bool aOldValue);
+
+ void DidSet(FieldIndex<IDX_SHEntryHasUserInteraction>, bool aOldValue);
+
+ bool CanSet(FieldIndex<IDX_WindowStateSaved>, bool aValue,
+ ContentParent* aSource);
+
+ // Overload `DidSet` to get notifications for a particular field being set.
+ //
+ // You can also overload the variant that gets the old value if you need it.
+ template <size_t I>
+ void DidSet(FieldIndex<I>) {}
+ template <size_t I, typename T>
+ void DidSet(FieldIndex<I>, T&& aOldValue) {}
+ void DidSet(FieldIndex<IDX_UserActivationStateAndModifiers>);
+
+ // Recomputes whether we can execute scripts in this WindowContext based on
+ // the value of AllowJavascript() and whether scripts are allowed in the
+ // BrowsingContext.
+ void RecomputeCanExecuteScripts(bool aApplyChanges = true);
+
+ const uint64_t mInnerWindowId;
+ const uint64_t mOuterWindowId;
+ RefPtr<BrowsingContext> mBrowsingContext;
+ WeakPtr<WindowGlobalChild> mWindowGlobalChild;
+
+ // --- NEVER CHANGE `mChildren` DIRECTLY! ---
+ // Changes to this list need to be synchronized to the list within our
+ // `mBrowsingContext`, and should only be performed through the
+ // `AppendChildBrowsingContext` and `RemoveChildBrowsingContext` methods.
+ nsTArray<RefPtr<BrowsingContext>> mChildren;
+
+ // --- NEVER CHANGE `mNonSyntheticChildren` DIRECTLY! ---
+ // Same reason as for mChildren.
+ // mNonSyntheticChildren contains the same browsing contexts except browsing
+ // contexts created by the synthetic document for object loading contents
+ // loading images. This is used to discern browsing contexts created when
+ // loading images in <object> or <embed> elements, so that they can be hidden
+ // from named targeting, `Window.frames` etc.
+ nsTArray<RefPtr<BrowsingContext>> mNonSyntheticChildren;
+
+ bool mIsDiscarded = false;
+ bool mIsInProcess = false;
+
+ // Determines if we can execute scripts in this WindowContext. True if
+ // AllowJavascript() is true and script execution is allowed in the
+ // BrowsingContext.
+ bool mCanExecuteScripts = true;
+
+ // The start time of user gesture, this is only available if the window
+ // context is in process.
+ TimeStamp mUserGestureStart;
+};
+
+using WindowContextTransaction = WindowContext::BaseTransaction;
+using WindowContextInitializer = WindowContext::IPCInitializer;
+using MaybeDiscardedWindowContext = MaybeDiscarded<WindowContext>;
+
+// Don't specialize the `Transaction` object for every translation unit it's
+// used in. This should help keep code size down.
+extern template class syncedcontext::Transaction<WindowContext>;
+
+} // namespace dom
+
+namespace ipc {
+template <>
+struct IPDLParamTraits<dom::MaybeDiscarded<dom::WindowContext>> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::MaybeDiscarded<dom::WindowContext>& aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::MaybeDiscarded<dom::WindowContext>* aResult);
+};
+
+template <>
+struct IPDLParamTraits<dom::WindowContext::IPCInitializer> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::WindowContext::IPCInitializer& aInitializer);
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::WindowContext::IPCInitializer* aInitializer);
+};
+} // namespace ipc
+} // namespace mozilla
+
+#endif // !defined(mozilla_dom_WindowContext_h)
diff --git a/docshell/base/crashtests/1257730-1.html b/docshell/base/crashtests/1257730-1.html
new file mode 100644
index 0000000000..028a1adb88
--- /dev/null
+++ b/docshell/base/crashtests/1257730-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<!--
+user_pref("browser.send_pings", true);
+-->
+<script>
+
+function boom() {
+ var aLink = document.createElement('a');
+ document.body.appendChild(aLink);
+ aLink.ping = "ping";
+ aLink.href = "href";
+ aLink.click();
+
+ var baseElement = document.createElement('base');
+ baseElement.setAttribute("href", "javascript:void 0");
+ document.head.appendChild(baseElement);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/docshell/base/crashtests/1331295.html b/docshell/base/crashtests/1331295.html
new file mode 100644
index 0000000000..cdcb29e7fe
--- /dev/null
+++ b/docshell/base/crashtests/1331295.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+function boom() {
+ setTimeout(function(){
+ var o=document.getElementById('b');
+ document.getElementById('a').appendChild(o.parentNode.removeChild(o));
+ },0);
+ var o=document.getElementById('c');
+ var p=document.getElementById('b');
+ p.id=[o.id, o.id=p.id][0];
+ o=document.getElementById('b');
+ o.setAttribute('sandbox', 'disc');
+ window.location.reload(true);
+}
+</script>
+</head>
+<body onload="boom();">
+<header id='a'></header>
+<output id='b'></output>
+<iframe id='c' sandbox='allow-same-origin' src='http://a'></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/1341657.html b/docshell/base/crashtests/1341657.html
new file mode 100644
index 0000000000..d68fa1eb03
--- /dev/null
+++ b/docshell/base/crashtests/1341657.html
@@ -0,0 +1,18 @@
+<html class="reftest-wait">
+ <head>
+ <script>
+ function boom() {
+ o1 = document.createElement("script");
+ o2 = document.implementation.createDocument('', '', null);
+ o3 = document.createElement("iframe");
+ document.documentElement.appendChild(o3);
+ o4 = o3.contentWindow;
+ o5 = document.createTextNode('o2.adoptNode(o3); try { o4.location = "" } catch(e) {}');
+ o1.appendChild(o5);
+ document.documentElement.appendChild(o1);
+ document.documentElement.classList.remove("reftest-wait");
+ }
+ </script>
+ </head>
+ <body onload="boom();"></body>
+</html>
diff --git a/docshell/base/crashtests/1584467.html b/docshell/base/crashtests/1584467.html
new file mode 100644
index 0000000000..5509808bcc
--- /dev/null
+++ b/docshell/base/crashtests/1584467.html
@@ -0,0 +1,12 @@
+<script>
+window.onload = () => {
+ a.addEventListener("DOMSubtreeModified", () => {
+ document.body.appendChild(b)
+ document.body.removeChild(b)
+ window[1]
+ })
+ a.type = ""
+}
+</script>
+<embed id="a">
+<iframe id="b"></iframe>
diff --git a/docshell/base/crashtests/1614211-1.html b/docshell/base/crashtests/1614211-1.html
new file mode 100644
index 0000000000..1d683e0714
--- /dev/null
+++ b/docshell/base/crashtests/1614211-1.html
@@ -0,0 +1,15 @@
+<script>
+window.onload = () => {
+ b.addEventListener('DOMSubtreeModified', () => {
+ var o = document.getElementById('a')
+ var a = o.attributes
+ for (let j = 0; j < a.length; j++) {
+ o.setAttribute(a[j].name, 'i')
+ o.parentNode.appendChild(o)
+ }
+ })
+ b.setAttribute('a', b)
+}
+</script>
+<iframe id='a' sandbox='' allowfullscreen=''></iframe>
+<dfn id='b'>
diff --git a/docshell/base/crashtests/1617315-1.html b/docshell/base/crashtests/1617315-1.html
new file mode 100644
index 0000000000..05d9a704dc
--- /dev/null
+++ b/docshell/base/crashtests/1617315-1.html
@@ -0,0 +1,8 @@
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ let o = document.getElementById('a')
+ o.setAttribute('id', '')
+ o.setAttribute('sandbox', '')
+})
+</script>
+<iframe id='a' sandbox='s' src='http://%CF'></iframe>
diff --git a/docshell/base/crashtests/1667491.html b/docshell/base/crashtests/1667491.html
new file mode 100644
index 0000000000..ecc77a5e9b
--- /dev/null
+++ b/docshell/base/crashtests/1667491.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <meta charset="UTF-8">
+<script>
+ function go() {
+ let win = window.open("1667491_1.html");
+ win.finish = function() {
+ document.documentElement.removeAttribute("class");
+ };
+ }
+</script>
+</head>
+<body onload="go()">
+</body>
+</html>
diff --git a/docshell/base/crashtests/1667491_1.html b/docshell/base/crashtests/1667491_1.html
new file mode 100644
index 0000000000..3df3353f72
--- /dev/null
+++ b/docshell/base/crashtests/1667491_1.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <script>
+ function go() {
+ document.body.appendChild(a)
+ window.frames[0].onbeforeunload = document.createElement("body").onload;
+ window.requestIdleCallback(() => {
+ window.close();
+ finish();
+ });
+ }
+ </script>
+</head>
+<body onload="go()">
+<iframe id="a"></iframe>
+<iframe></iframe>
+</body>
+</html>
+
diff --git a/docshell/base/crashtests/1672873.html b/docshell/base/crashtests/1672873.html
new file mode 100644
index 0000000000..33aa92f0ef
--- /dev/null
+++ b/docshell/base/crashtests/1672873.html
@@ -0,0 +1,6 @@
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ var x = new Blob([undefined, ''], { })
+ self.history.pushState(x, 'x', 'missing.file')
+})
+</script>
diff --git a/docshell/base/crashtests/1690169-1.html b/docshell/base/crashtests/1690169-1.html
new file mode 100644
index 0000000000..6c9be20be3
--- /dev/null
+++ b/docshell/base/crashtests/1690169-1.html
@@ -0,0 +1,11 @@
+<script>
+var woff = "data:font/woff2;base64,";
+for (let i = 0; i < 20000; i++) {
+ woff += "d09GMgABAAAA";
+}
+window.onload = () => {
+ try { window.open('data:text/html,<spacer>', 'pu9', 'width=911').close() } catch (e) {}
+ document.getElementById('a').setAttribute('src', woff)
+}
+</script>
+<iframe id='a' hidden src='http://a'></iframe>
diff --git a/docshell/base/crashtests/1753136.html b/docshell/base/crashtests/1753136.html
new file mode 100644
index 0000000000..22f679309e
--- /dev/null
+++ b/docshell/base/crashtests/1753136.html
@@ -0,0 +1,2 @@
+<meta charset="UTF-8">
+<iframe sandbox='' src='http://🙅🎂งิ'></iframe>
diff --git a/docshell/base/crashtests/1804803.html b/docshell/base/crashtests/1804803.html
new file mode 100644
index 0000000000..5103c00416
--- /dev/null
+++ b/docshell/base/crashtests/1804803.html
@@ -0,0 +1,13 @@
+<html class="reftest-wait">
+<script>
+function test() {
+ // If the reload in the iframe succeeds we might crash, so wait for it.
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 500);
+}
+</script>
+<body onload="test()">
+ <iframe src="1804803.sjs"></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/1804803.sjs b/docshell/base/crashtests/1804803.sjs
new file mode 100644
index 0000000000..0486e32048
--- /dev/null
+++ b/docshell/base/crashtests/1804803.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+ let counter = Number(getState("load"));
+ const reload = counter == 0 ? "self.history.go(0);" : "";
+ setState("load", String(++counter));
+ const document = `
+<script>
+ document.addEventListener('DOMContentLoaded', () => {
+ ${reload}
+ let url = window.location.href;
+ for (let i = 0; i < 50; i++) {
+ self.history.pushState({x: i}, '', url + "#" + i);
+ }
+ });
+</script>
+`;
+
+ response.write(document);
+}
diff --git a/docshell/base/crashtests/369126-1.html b/docshell/base/crashtests/369126-1.html
new file mode 100644
index 0000000000..e9dacec301
--- /dev/null
+++ b/docshell/base/crashtests/369126-1.html
@@ -0,0 +1,16 @@
+<html class="reftest-wait">
+<head>
+<script>
+function boom()
+{
+ document.getElementById("frameset").removeChild(document.getElementById("frame"));
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+
+<frameset id="frameset" onload="setTimeout(boom, 100)">
+ <frame id="frame" src="data:text/html,<body onUnload=&quot;location = 'http://www.mozilla.org/'&quot;>This frame's onunload tries to load another page.">
+</frameset>
+
+</html>
diff --git a/docshell/base/crashtests/40929-1-inner.html b/docshell/base/crashtests/40929-1-inner.html
new file mode 100644
index 0000000000..313046a348
--- /dev/null
+++ b/docshell/base/crashtests/40929-1-inner.html
@@ -0,0 +1,14 @@
+<html><head><title>Infinite Loop</title></head>
+<body onLoad="initNav(); initNav();">
+
+<script language="JavaScript">
+
+function initNav() {
+ ++parent.i;
+ if (parent.i < 10)
+ window.location.href=window.location.href;
+}
+
+</script>
+
+</body></html>
diff --git a/docshell/base/crashtests/40929-1.html b/docshell/base/crashtests/40929-1.html
new file mode 100644
index 0000000000..90685d9f1f
--- /dev/null
+++ b/docshell/base/crashtests/40929-1.html
@@ -0,0 +1,6 @@
+<html>
+<head><title>Infinite Loop</title><script>var i=0;</script></head>
+<body>
+<iframe src="40929-1-inner.html"></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/430124-1.html b/docshell/base/crashtests/430124-1.html
new file mode 100644
index 0000000000..8cdbc1d077
--- /dev/null
+++ b/docshell/base/crashtests/430124-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body onpagehide="document.getElementById('a').focus();"><div id="a"></div></body>
+</html>
diff --git a/docshell/base/crashtests/430628-1.html b/docshell/base/crashtests/430628-1.html
new file mode 100644
index 0000000000..4a68a5a015
--- /dev/null
+++ b/docshell/base/crashtests/430628-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body onpagehide="document.body.removeChild(document.getElementById('s'));">
+<span id="s" contenteditable="true"></span>
+</body>
+</html>
diff --git a/docshell/base/crashtests/432114-1.html b/docshell/base/crashtests/432114-1.html
new file mode 100644
index 0000000000..8878d6605a
--- /dev/null
+++ b/docshell/base/crashtests/432114-1.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Bug - Crash [@ PL_DHashTableOperate] with DOMNodeInserted event listener removing window and frameset contenteditable</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Cscript%3E%0Awindow.addEventListener%28%27DOMNodeInserted%27%2C%20function%28%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3Cframeset%20contenteditable%3D%22true%22%3E"></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/432114-2.html b/docshell/base/crashtests/432114-2.html
new file mode 100644
index 0000000000..da77287b61
--- /dev/null
+++ b/docshell/base/crashtests/432114-2.html
@@ -0,0 +1,21 @@
+<html class="reftest-wait">
+<head>
+<title>testcase2 Bug 432114 � Crash [@ PL_DHashTableOperate] with DOMNodeInserted event listener removing window and frameset contenteditable</title>
+</head>
+<body>
+<script>
+ window.addEventListener("DOMNodeRemoved", function() {
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ });
+ var iframe = document.getElementById("content");
+ iframe.onload=function() {
+ dump("iframe onload\n");
+ console.log("iframe onload");
+ };
+</script>
+<iframe id="content" src="file_432114-2.xhtml" style="width:1000px;height: 200px;"></iframe>
+
+</body>
+</html>
diff --git a/docshell/base/crashtests/436900-1-inner.html b/docshell/base/crashtests/436900-1-inner.html
new file mode 100644
index 0000000000..6fe35ccb1a
--- /dev/null
+++ b/docshell/base/crashtests/436900-1-inner.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+<meta http-equiv="refresh" content="0">
+
+<script language="javascript">
+
+location.hash += "+++";
+
+function done()
+{
+ parent.document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="setTimeout(done, 10)">
+
+</body>
+</html>
diff --git a/docshell/base/crashtests/436900-1.html b/docshell/base/crashtests/436900-1.html
new file mode 100644
index 0000000000..582d1919d1
--- /dev/null
+++ b/docshell/base/crashtests/436900-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<iframe src="436900-1-inner.html#foo"></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/436900-2-inner.html b/docshell/base/crashtests/436900-2-inner.html
new file mode 100644
index 0000000000..ea79f75e88
--- /dev/null
+++ b/docshell/base/crashtests/436900-2-inner.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+<meta http-equiv="refresh" content="0">
+
+<script language="javascript" id="foo+++">
+
+location.hash += "+++";
+
+function done()
+{
+ parent.document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="setTimeout(done, 10)">
+
+</body>
+</html>
diff --git a/docshell/base/crashtests/436900-2.html b/docshell/base/crashtests/436900-2.html
new file mode 100644
index 0000000000..2e1f0c1def
--- /dev/null
+++ b/docshell/base/crashtests/436900-2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<iframe src="436900-2-inner.html#foo"></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/443655.html b/docshell/base/crashtests/443655.html
new file mode 100644
index 0000000000..ce0a8c18b8
--- /dev/null
+++ b/docshell/base/crashtests/443655.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+
+<body onload="document.removeChild(document.documentElement)">
+
+<!-- The order of the two iframes matters! -->
+
+<iframe src='data:text/html,<body onload="s = parent.document.getElementById(&apos;s&apos;).contentWindow;" onunload="s.location = s.location;">'></iframe>
+
+<iframe id="s"></iframe>
+
+</body>
+</html>
diff --git a/docshell/base/crashtests/500328-1.html b/docshell/base/crashtests/500328-1.html
new file mode 100644
index 0000000000..fd97f84ae1
--- /dev/null
+++ b/docshell/base/crashtests/500328-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<body onload="test();">
+<script>
+ function test() {
+ // Test that calling pushState() with a state object which calls
+ // history.back() doesn't crash. We need to make sure that there's at least
+ // one entry in the history before we do anything else.
+ history.pushState(null, "");
+
+ x = {};
+ x.toJSON = { history.back(); return "{a:1}"; };
+ history.pushState(x, "");
+ }
+</script>
+</body>
+</html>
diff --git a/docshell/base/crashtests/514779-1.xhtml b/docshell/base/crashtests/514779-1.xhtml
new file mode 100644
index 0000000000..16ac3d9d66
--- /dev/null
+++ b/docshell/base/crashtests/514779-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head></head>
+
+<body onunload="document.getElementById('tbody').appendChild(document.createElementNS('http://www.w3.org/1999/xhtml', 'span'))">
+ <iframe/>
+ <tbody contenteditable="true" id="tbody">xy</tbody>
+</body>
+
+</html>
diff --git a/docshell/base/crashtests/614499-1.html b/docshell/base/crashtests/614499-1.html
new file mode 100644
index 0000000000..7053a3f52f
--- /dev/null
+++ b/docshell/base/crashtests/614499-1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var f = document.getElementById("f");
+
+ for (var i = 0; i < 50; ++i) {
+ f.contentWindow.history.pushState({}, "");
+ }
+
+ document.body.removeChild(f);
+}
+
+</script>
+</head>
+<body onload="boom();"><iframe id="f" src="data:text/html,1"></iframe></body>
+</html> \ No newline at end of file
diff --git a/docshell/base/crashtests/678872-1.html b/docshell/base/crashtests/678872-1.html
new file mode 100644
index 0000000000..294b3e689b
--- /dev/null
+++ b/docshell/base/crashtests/678872-1.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+var f1, f2;
+
+function b1()
+{
+ f1 = document.getElementById("f1");
+ f2 = document.getElementById("f2");
+ f1.contentWindow.document.write("11");
+ f1.contentWindow.history.back();
+ setTimeout(b2, 0);
+}
+
+function b2()
+{
+ f2.contentWindow.history.forward();
+ f2.contentWindow.location.reload();
+ f1.remove();
+}
+
+</script>
+
+
+</script>
+</head>
+
+<body onload="setTimeout(b1, 0);">
+
+<iframe id="f1" src="data:text/html,1"></iframe>
+<iframe id="f2" src="data:text/html,2"></iframe>
+
+</body>
+</html>
diff --git a/docshell/base/crashtests/914521.html b/docshell/base/crashtests/914521.html
new file mode 100644
index 0000000000..8196e43016
--- /dev/null
+++ b/docshell/base/crashtests/914521.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+
+function f()
+{
+ function spin() {
+ for (var i = 0; i < 8; ++i) {
+ var x = new XMLHttpRequest();
+ x.open('GET', 'data:text/html,' + i, false);
+ x.send();
+ }
+ }
+
+ window.addEventListener("popstate", spin);
+ window.close();
+ window.location = "#c";
+ setTimeout(finish,0);
+}
+
+var win;
+function finish() {
+ win.close();
+ document.documentElement.removeAttribute("class");
+}
+
+function start()
+{
+ win = window.open("javascript:'<html><body>dummy</body></html>';", null, "width=300,height=300");
+ win.onload = f;
+}
+
+</script>
+</head>
+<body onload="start();"></body>
+</html>
diff --git a/docshell/base/crashtests/crashtests.list b/docshell/base/crashtests/crashtests.list
new file mode 100644
index 0000000000..f9b214bfa2
--- /dev/null
+++ b/docshell/base/crashtests/crashtests.list
@@ -0,0 +1,25 @@
+load 40929-1.html
+load 369126-1.html
+load 430124-1.html
+load 430628-1.html
+load 432114-1.html
+load 432114-2.html
+load 436900-1.html
+asserts(0-1) load 436900-2.html # bug 566159
+load 443655.html
+load 500328-1.html
+load 514779-1.xhtml
+load 614499-1.html
+load 678872-1.html
+skip-if(Android) pref(dom.disable_open_during_load,false) load 914521.html # Android bug 1584562
+pref(browser.send_pings,true) asserts(0-2) load 1257730-1.html # bug 566159
+load 1331295.html
+load 1341657.html
+load 1584467.html
+load 1614211-1.html
+load 1617315-1.html
+skip-if(Android) pref(dom.disable_open_during_load,false) load 1667491.html
+pref(dom.disable_open_during_load,false) load 1690169-1.html
+load 1672873.html
+load 1753136.html
+HTTP load 1804803.html
diff --git a/docshell/base/crashtests/file_432114-2.xhtml b/docshell/base/crashtests/file_432114-2.xhtml
new file mode 100644
index 0000000000..40bf886b8e
--- /dev/null
+++ b/docshell/base/crashtests/file_432114-2.xhtml
@@ -0,0 +1 @@
+<html xmlns='http://www.w3.org/1999/xhtml'><frameset contenteditable='true'/><script>function doExecCommand(){dump("doExecCommand\n");document.execCommand('formatBlock', false, 'p');}setTimeout(doExecCommand,100); window.addEventListener('DOMNodeRemoved', function() {window.frameElement.parentNode.removeChild(window.frameElement);}, true);</script></html>
diff --git a/docshell/base/metrics.yaml b/docshell/base/metrics.yaml
new file mode 100644
index 0000000000..ddb3457945
--- /dev/null
+++ b/docshell/base/metrics.yaml
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: DOM: Navigation'
+
+performance.page:
+ total_content_page_load:
+ type: timing_distribution
+ time_unit: millisecond
+ telemetry_mirror: TOTAL_CONTENT_PAGE_LOAD_TIME
+ description: >
+ Time to load all of a page's resources and render.
+ (Migrated from the geckoview metric of the same name.)
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1877842
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10
+ notification_emails:
+ - perf-telemetry-alerts@mozilla.com
+ - bdekoz@mozilla.com
+ expires: never
diff --git a/docshell/base/moz.build b/docshell/base/moz.build
new file mode 100644
index 0000000000..3520e9d75a
--- /dev/null
+++ b/docshell/base/moz.build
@@ -0,0 +1,126 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Navigation")
+
+with Files("crashtests/430628*"):
+ BUG_COMPONENT = ("Core", "DOM: Editor")
+
+with Files("crashtests/432114*"):
+ BUG_COMPONENT = ("Core", "DOM: Editor")
+
+with Files("crashtests/500328*"):
+ BUG_COMPONENT = ("Firefox", "Bookmarks & History")
+
+with Files("IHistory.h"):
+ BUG_COMPONENT = ("Toolkit", "Places")
+
+with Files("*LoadContext.*"):
+ BUG_COMPONENT = ("Core", "Networking")
+
+with Files("nsAboutRedirector.*"):
+ BUG_COMPONENT = ("Core", "General")
+
+with Files("nsIScrollObserver.*"):
+ BUG_COMPONENT = ("Core", "Panning and Zooming")
+
+XPIDL_SOURCES += [
+ "nsIDocShell.idl",
+ "nsIDocShellTreeItem.idl",
+ "nsIDocShellTreeOwner.idl",
+ "nsIDocumentLoaderFactory.idl",
+ "nsIDocumentViewer.idl",
+ "nsIDocumentViewerEdit.idl",
+ "nsILoadContext.idl",
+ "nsILoadURIDelegate.idl",
+ "nsIPrivacyTransitionObserver.idl",
+ "nsIReflowObserver.idl",
+ "nsIRefreshURI.idl",
+ "nsITooltipListener.idl",
+ "nsITooltipTextProvider.idl",
+ "nsIURIFixup.idl",
+ "nsIWebNavigation.idl",
+ "nsIWebNavigationInfo.idl",
+ "nsIWebPageDescriptor.idl",
+]
+
+XPIDL_MODULE = "docshell"
+
+EXPORTS += [
+ "nsCTooltipTextProvider.h",
+ "nsDocShell.h",
+ "nsDocShellLoadState.h",
+ "nsDocShellLoadTypes.h",
+ "nsDocShellTreeOwner.h",
+ "nsDSURIContentListener.h",
+ "nsIScrollObserver.h",
+ "nsWebNavigationInfo.h",
+ "SerializedLoadContext.h",
+]
+
+EXPORTS.mozilla += [
+ "BaseHistory.h",
+ "IHistory.h",
+ "LoadContext.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "BrowsingContext.h",
+ "BrowsingContextGroup.h",
+ "BrowsingContextWebProgress.h",
+ "CanonicalBrowsingContext.h",
+ "ChildProcessChannelListener.h",
+ "SyncedContext.h",
+ "SyncedContextInlines.h",
+ "WindowContext.h",
+]
+
+UNIFIED_SOURCES += [
+ "BaseHistory.cpp",
+ "BrowsingContext.cpp",
+ "BrowsingContextGroup.cpp",
+ "BrowsingContextWebProgress.cpp",
+ "CanonicalBrowsingContext.cpp",
+ "ChildProcessChannelListener.cpp",
+ "LoadContext.cpp",
+ "nsAboutRedirector.cpp",
+ "nsDocShell.cpp",
+ "nsDocShellEditorData.cpp",
+ "nsDocShellEnumerator.cpp",
+ "nsDocShellLoadState.cpp",
+ "nsDocShellTelemetryUtils.cpp",
+ "nsDocShellTreeOwner.cpp",
+ "nsDSURIContentListener.cpp",
+ "nsPingListener.cpp",
+ "nsRefreshTimer.cpp",
+ "nsWebNavigationInfo.cpp",
+ "SerializedLoadContext.cpp",
+ "WindowContext.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/docshell/shistory",
+ "/dom/base",
+ "/dom/bindings",
+ "/js/xpconnect/src",
+ "/layout/base",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/xul",
+ "/netwerk/base",
+ "/netwerk/protocol/viewsource",
+ "/toolkit/components/browser",
+ "/toolkit/components/find",
+ "/tools/profiler",
+]
+
+EXTRA_JS_MODULES += ["URIFixup.sys.mjs"]
+
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp
new file mode 100644
index 0000000000..fdae228b90
--- /dev/null
+++ b/docshell/base/nsAboutRedirector.cpp
@@ -0,0 +1,318 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsAboutRedirector.h"
+#include "nsNetUtil.h"
+#include "nsAboutProtocolUtils.h"
+#include "nsBaseChannel.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsIProtocolHandler.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/RemoteType.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+
+#define ABOUT_CONFIG_ENABLED_PREF "general.aboutConfig.enable"
+
+NS_IMPL_ISUPPORTS(nsAboutRedirector, nsIAboutModule)
+
+struct RedirEntry {
+ const char* id;
+ const char* url;
+ uint32_t flags;
+};
+
+class CrashChannel final : public nsBaseChannel {
+ public:
+ explicit CrashChannel(nsIURI* aURI) { SetURI(aURI); }
+
+ nsresult OpenContentStream(bool async, nsIInputStream** stream,
+ nsIChannel** channel) override {
+ nsAutoCString spec;
+ mURI->GetSpec(spec);
+
+ if (spec.EqualsASCII("about:crashparent") && XRE_IsParentProcess()) {
+ MOZ_CRASH("Crash via about:crashparent");
+ }
+
+ if (spec.EqualsASCII("about:crashgpu") && XRE_IsParentProcess()) {
+ if (auto* gpu = mozilla::gfx::GPUProcessManager::Get()) {
+ gpu->CrashProcess();
+ }
+ }
+
+ if (spec.EqualsASCII("about:crashcontent") && XRE_IsContentProcess()) {
+ MOZ_CRASH("Crash via about:crashcontent");
+ }
+
+ if (spec.EqualsASCII("about:crashextensions") && XRE_IsParentProcess()) {
+ using ContentParent = mozilla::dom::ContentParent;
+ nsTArray<RefPtr<ContentParent>> toKill;
+ for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) {
+ if (cp->GetRemoteType() == EXTENSION_REMOTE_TYPE) {
+ toKill.AppendElement(cp);
+ }
+ }
+ for (auto& cp : toKill) {
+ cp->KillHard("Killed via about:crashextensions");
+ }
+ }
+
+ NS_WARNING("Unhandled about:crash* URI or wrong process");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ protected:
+ virtual ~CrashChannel() = default;
+};
+
+/*
+ Entries which do not have URI_SAFE_FOR_UNTRUSTED_CONTENT will run with chrome
+ privileges. This is potentially dangerous. Please use
+ URI_SAFE_FOR_UNTRUSTED_CONTENT in the third argument to each map item below
+ unless your about: page really needs chrome privileges. Security review is
+ required before adding new map entries without
+ URI_SAFE_FOR_UNTRUSTED_CONTENT.
+
+ URI_SAFE_FOR_UNTRUSTED_CONTENT is not enough to let web pages load that page,
+ for that you need MAKE_LINKABLE.
+
+ NOTE: changes to this redir map need to be accompanied with changes to
+ docshell/build/components.conf
+ */
+static const RedirEntry kRedirMap[] = {
+ {"about", "chrome://global/content/aboutAbout.html", 0},
+ {"addons", "chrome://mozapps/content/extensions/aboutaddons.html",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI},
+ {"buildconfig", "chrome://global/content/buildconfig.html",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::IS_SECURE_CHROME_UI},
+ {"checkerboard", "chrome://global/content/aboutCheckerboard.html",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::ALLOW_SCRIPT},
+#ifndef MOZ_WIDGET_ANDROID
+ {"config", "chrome://global/content/aboutconfig/aboutconfig.html",
+ nsIAboutModule::IS_SECURE_CHROME_UI},
+#else
+ {"config", "chrome://geckoview/content/config.xhtml",
+ nsIAboutModule::IS_SECURE_CHROME_UI},
+#endif
+#ifdef MOZ_CRASHREPORTER
+ {"crashes", "chrome://global/content/crashes.html",
+ nsIAboutModule::IS_SECURE_CHROME_UI},
+#endif
+ {"credits", "https://www.mozilla.org/credits/",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::URI_MUST_LOAD_IN_CHILD},
+ {"httpsonlyerror", "chrome://global/content/httpsonlyerror/errorpage.html",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+ {"license", "chrome://global/content/license.html",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::IS_SECURE_CHROME_UI},
+ {"logging", "chrome://global/content/aboutLogging.html",
+ nsIAboutModule::ALLOW_SCRIPT},
+ {"logo", "chrome://branding/content/about.png",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ // Linkable for testing reasons.
+ nsIAboutModule::MAKE_LINKABLE},
+ {"memory", "chrome://global/content/aboutMemory.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT},
+ {"certificate", "chrome://global/content/certviewer/certviewer.html",
+ nsIAboutModule::ALLOW_SCRIPT |
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
+ nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS |
+ nsIAboutModule::IS_SECURE_CHROME_UI},
+ {"mozilla", "chrome://global/content/mozilla.html",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT},
+#if !defined(ANDROID) && !defined(XP_WIN)
+ {"webauthn", "chrome://global/content/aboutWebauthn.html",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI},
+#endif
+ {"neterror", "chrome://global/content/aboutNetError.html",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+ {"networking", "chrome://global/content/aboutNetworking.html",
+ nsIAboutModule::ALLOW_SCRIPT},
+ {"performance", "about:processes",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+ {"processes", "chrome://global/content/aboutProcesses.html",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI},
+ // about:serviceworkers always wants to load in the parent process because
+ // the only place nsIServiceWorkerManager has any data is in the parent
+ // process.
+ //
+ // There is overlap without about:debugging, but about:debugging is not
+ // available on mobile at this time, and it's useful to be able to know if
+ // a ServiceWorker is registered directly from the mobile browser without
+ // having to connect the device to a desktop machine and all that entails.
+ {"serviceworkers", "chrome://global/content/aboutServiceWorkers.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT},
+#ifndef ANDROID
+ {"profiles", "chrome://global/content/aboutProfiles.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI},
+#endif
+ // about:srcdoc is unresolvable by specification. It is included here
+ // because the security manager would disallow srcdoc iframes otherwise.
+ {"srcdoc", "about:blank",
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT |
+ // Needs to be linkable so content can touch its own srcdoc frames
+ nsIAboutModule::MAKE_LINKABLE | nsIAboutModule::URI_CAN_LOAD_IN_CHILD},
+ {"support", "chrome://global/content/aboutSupport.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI},
+#ifdef XP_WIN
+ {"third-party", "chrome://global/content/aboutThirdParty.html",
+ nsIAboutModule::ALLOW_SCRIPT},
+ {"windows-messages", "chrome://global/content/aboutWindowsMessages.html",
+ nsIAboutModule::ALLOW_SCRIPT},
+#endif
+#ifndef MOZ_GLEAN_ANDROID
+ {"glean", "chrome://global/content/aboutGlean.html",
+# if !defined(NIGHTLY_BUILD) && defined(MOZILLA_OFFICIAL)
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT |
+# endif
+ nsIAboutModule::ALLOW_SCRIPT},
+#endif
+ {"telemetry", "chrome://global/content/aboutTelemetry.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI},
+ {"translations", "chrome://global/content/translations/translations.html",
+ nsIAboutModule::ALLOW_SCRIPT |
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
+ nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+ {"url-classifier", "chrome://global/content/aboutUrlClassifier.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT},
+ {"webrtc", "chrome://global/content/aboutwebrtc/aboutWebrtc.html",
+ nsIAboutModule::ALLOW_SCRIPT},
+ {"crashparent", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+ {"crashcontent", "about:blank",
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT |
+ nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ nsIAboutModule::URI_CAN_LOAD_IN_CHILD |
+ nsIAboutModule::URI_MUST_LOAD_IN_CHILD},
+ {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+ {"crashextensions", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}};
+static const int kRedirTotal = mozilla::ArrayLength(kRedirMap);
+
+NS_IMETHODIMP
+nsAboutRedirector::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel** aResult) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aLoadInfo);
+ NS_ASSERTION(aResult, "must not be null");
+
+ nsAutoCString path;
+ nsresult rv = NS_GetAboutModuleName(aURI, path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (path.EqualsASCII("crashparent") || path.EqualsASCII("crashcontent") ||
+ path.EqualsASCII("crashgpu") || path.EqualsASCII("crashextensions")) {
+ bool isExternal;
+ aLoadInfo->GetLoadTriggeredFromExternal(&isExternal);
+ if (isExternal || !aLoadInfo->TriggeringPrincipal() ||
+ !aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIChannel> channel = new CrashChannel(aURI);
+ channel->SetLoadInfo(aLoadInfo);
+ channel.forget(aResult);
+ return NS_OK;
+ }
+
+ if (path.EqualsASCII("config") &&
+ !mozilla::Preferences::GetBool(ABOUT_CONFIG_ENABLED_PREF, true)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ for (int i = 0; i < kRedirTotal; i++) {
+ if (!strcmp(path.get(), kRedirMap[i].id)) {
+ nsCOMPtr<nsIChannel> tempChannel;
+ nsCOMPtr<nsIURI> tempURI;
+ rv = NS_NewURI(getter_AddRefs(tempURI), kRedirMap[i].url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_NewChannelInternal(getter_AddRefs(tempChannel), tempURI,
+ aLoadInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If tempURI links to an external URI (i.e. something other than
+ // chrome:// or resource://) then set result principal URI on the
+ // load info which forces the channel principal to reflect the displayed
+ // URL rather then being the systemPrincipal.
+ bool isUIResource = false;
+ rv = NS_URIChainHasFlags(tempURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,
+ &isUIResource);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isAboutBlank = NS_IsAboutBlank(tempURI);
+
+ if (!isUIResource && !isAboutBlank) {
+ aLoadInfo->SetResultPrincipalURI(tempURI);
+ }
+
+ tempChannel->SetOriginalURI(aURI);
+
+ tempChannel.forget(aResult);
+ return rv;
+ }
+ }
+
+ NS_ERROR("nsAboutRedirector called for unknown case");
+ return NS_ERROR_ILLEGAL_VALUE;
+}
+
+NS_IMETHODIMP
+nsAboutRedirector::GetURIFlags(nsIURI* aURI, uint32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsAutoCString name;
+ nsresult rv = NS_GetAboutModuleName(aURI, name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int i = 0; i < kRedirTotal; i++) {
+ if (name.EqualsASCII(kRedirMap[i].id)) {
+ *aResult = kRedirMap[i].flags;
+ return NS_OK;
+ }
+ }
+
+ NS_ERROR("nsAboutRedirector called for unknown case");
+ return NS_ERROR_ILLEGAL_VALUE;
+}
+
+NS_IMETHODIMP
+nsAboutRedirector::GetChromeURI(nsIURI* aURI, nsIURI** chromeURI) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsAutoCString name;
+ nsresult rv = NS_GetAboutModuleName(aURI, name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (const auto& redir : kRedirMap) {
+ if (name.EqualsASCII(redir.id)) {
+ return NS_NewURI(chromeURI, redir.url);
+ }
+ }
+
+ NS_ERROR("nsAboutRedirector called for unknown case");
+ return NS_ERROR_ILLEGAL_VALUE;
+}
+
+nsresult nsAboutRedirector::Create(REFNSIID aIID, void** aResult) {
+ RefPtr<nsAboutRedirector> about = new nsAboutRedirector();
+ return about->QueryInterface(aIID, aResult);
+}
diff --git a/docshell/base/nsAboutRedirector.h b/docshell/base/nsAboutRedirector.h
new file mode 100644
index 0000000000..0bf021cc31
--- /dev/null
+++ b/docshell/base/nsAboutRedirector.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsAboutRedirector_h__
+#define nsAboutRedirector_h__
+
+#include "nsIAboutModule.h"
+
+class nsAboutRedirector : public nsIAboutModule {
+ public:
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIABOUTMODULE
+
+ nsAboutRedirector() {}
+
+ static nsresult Create(REFNSIID aIID, void** aResult);
+
+ protected:
+ virtual ~nsAboutRedirector() {}
+};
+
+#endif // nsAboutRedirector_h__
diff --git a/docshell/base/nsCTooltipTextProvider.h b/docshell/base/nsCTooltipTextProvider.h
new file mode 100644
index 0000000000..731edf1170
--- /dev/null
+++ b/docshell/base/nsCTooltipTextProvider.h
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NSCTOOLTIPTEXTPROVIDER_H
+#define NSCTOOLTIPTEXTPROVIDER_H
+
+#define NS_TOOLTIPTEXTPROVIDER_CONTRACTID \
+ "@mozilla.org/embedcomp/tooltiptextprovider;1"
+#define NS_DEFAULTTOOLTIPTEXTPROVIDER_CONTRACTID \
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1"
+
+#endif
diff --git a/docshell/base/nsDSURIContentListener.cpp b/docshell/base/nsDSURIContentListener.cpp
new file mode 100644
index 0000000000..2eccb74da9
--- /dev/null
+++ b/docshell/base/nsDSURIContentListener.cpp
@@ -0,0 +1,297 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDocShell.h"
+#include "nsDSURIContentListener.h"
+#include "nsIChannel.h"
+#include "nsServiceManagerUtils.h"
+#include "nsDocShellCID.h"
+#include "nsIWebNavigationInfo.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/Unused.h"
+#include "nsError.h"
+#include "nsContentSecurityManager.h"
+#include "nsDocShellLoadTypes.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIMultiPartChannel.h"
+#include "nsWebNavigationInfo.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_ADDREF(MaybeCloseWindowHelper)
+NS_IMPL_RELEASE(MaybeCloseWindowHelper)
+
+NS_INTERFACE_MAP_BEGIN(MaybeCloseWindowHelper)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+MaybeCloseWindowHelper::MaybeCloseWindowHelper(BrowsingContext* aContentContext)
+ : mBrowsingContext(aContentContext),
+ mTimer(nullptr),
+ mShouldCloseWindow(false) {}
+
+MaybeCloseWindowHelper::~MaybeCloseWindowHelper() {}
+
+void MaybeCloseWindowHelper::SetShouldCloseWindow(bool aShouldCloseWindow) {
+ mShouldCloseWindow = aShouldCloseWindow;
+}
+
+BrowsingContext* MaybeCloseWindowHelper::MaybeCloseWindow() {
+ if (!mShouldCloseWindow) {
+ return mBrowsingContext;
+ }
+
+ // This method should not be called more than once, but it's better to avoid
+ // closing the current window again.
+ mShouldCloseWindow = false;
+
+ // Reset the window context to the opener window so that the dependent
+ // dialogs have a parent
+ RefPtr<BrowsingContext> newBC = ChooseNewBrowsingContext(mBrowsingContext);
+
+ if (newBC != mBrowsingContext && newBC && !newBC->IsDiscarded()) {
+ mBCToClose = mBrowsingContext;
+ mBrowsingContext = newBC;
+
+ // Now close the old window. Do it on a timer so that we don't run
+ // into issues trying to close the window before it has fully opened.
+ NS_ASSERTION(!mTimer, "mTimer was already initialized once!");
+ NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, 0,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+
+ return mBrowsingContext;
+}
+
+already_AddRefed<BrowsingContext>
+MaybeCloseWindowHelper::ChooseNewBrowsingContext(BrowsingContext* aBC) {
+ RefPtr<BrowsingContext> opener = aBC->GetOpener();
+ if (opener && !opener->IsDiscarded()) {
+ return opener.forget();
+ }
+
+ if (!XRE_IsParentProcess()) {
+ return nullptr;
+ }
+
+ opener = BrowsingContext::Get(aBC->Canonical()->GetCrossGroupOpenerId());
+ if (!opener || opener->IsDiscarded()) {
+ return nullptr;
+ }
+ return opener.forget();
+}
+
+NS_IMETHODIMP
+MaybeCloseWindowHelper::Notify(nsITimer* timer) {
+ NS_ASSERTION(mBCToClose, "No window to close after timer fired");
+
+ mBCToClose->Close(CallerType::System, IgnoreErrors());
+ mBCToClose = nullptr;
+ mTimer = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MaybeCloseWindowHelper::GetName(nsACString& aName) {
+ aName.AssignLiteral("MaybeCloseWindowHelper");
+ return NS_OK;
+}
+
+nsDSURIContentListener::nsDSURIContentListener(nsDocShell* aDocShell)
+ : mDocShell(aDocShell),
+ mExistingJPEGRequest(nullptr),
+ mParentContentListener(nullptr) {}
+
+nsDSURIContentListener::~nsDSURIContentListener() {}
+
+NS_IMPL_ADDREF(nsDSURIContentListener)
+NS_IMPL_RELEASE(nsDSURIContentListener)
+
+NS_INTERFACE_MAP_BEGIN(nsDSURIContentListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURIContentListener)
+ NS_INTERFACE_MAP_ENTRY(nsIURIContentListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+NS_IMETHODIMP
+nsDSURIContentListener::DoContent(const nsACString& aContentType,
+ bool aIsContentPreferred,
+ nsIRequest* aRequest,
+ nsIStreamListener** aContentHandler,
+ bool* aAbortProcess) {
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(aContentHandler);
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE);
+ RefPtr<nsDocShell> docShell = mDocShell;
+
+ *aAbortProcess = false;
+
+ // determine if the channel has just been retargeted to us...
+ nsLoadFlags loadFlags = 0;
+ if (nsCOMPtr<nsIChannel> openedChannel = do_QueryInterface(aRequest)) {
+ openedChannel->GetLoadFlags(&loadFlags);
+ }
+
+ if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) {
+ // XXX: Why does this not stop the content too?
+ docShell->Stop(nsIWebNavigation::STOP_NETWORK);
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE);
+ docShell->SetLoadType(aIsContentPreferred ? LOAD_LINK : LOAD_NORMAL);
+ }
+
+ // In case of multipart jpeg request (mjpeg) we don't really want to
+ // create new viewer since the one we already have is capable of
+ // rendering multipart jpeg correctly (see bug 625012)
+ nsCOMPtr<nsIChannel> baseChannel;
+ if (nsCOMPtr<nsIMultiPartChannel> mpchan = do_QueryInterface(aRequest)) {
+ mpchan->GetBaseChannel(getter_AddRefs(baseChannel));
+ }
+
+ bool reuseCV = baseChannel && baseChannel == mExistingJPEGRequest &&
+ aContentType.EqualsLiteral("image/jpeg");
+
+ if (mExistingJPEGStreamListener && reuseCV) {
+ RefPtr<nsIStreamListener> copy(mExistingJPEGStreamListener);
+ copy.forget(aContentHandler);
+ rv = NS_OK;
+ } else {
+ rv =
+ docShell->CreateDocumentViewer(aContentType, aRequest, aContentHandler);
+ if (NS_SUCCEEDED(rv) && reuseCV) {
+ mExistingJPEGStreamListener = *aContentHandler;
+ } else {
+ mExistingJPEGStreamListener = nullptr;
+ }
+ mExistingJPEGRequest = baseChannel;
+ }
+
+ if (rv == NS_ERROR_DOCSHELL_DYING) {
+ aRequest->Cancel(rv);
+ *aAbortProcess = true;
+ return NS_OK;
+ }
+
+ if (NS_FAILED(rv)) {
+ // we don't know how to handle the content
+ nsCOMPtr<nsIStreamListener> forget = dont_AddRef(*aContentHandler);
+ *aContentHandler = nullptr;
+ return rv;
+ }
+
+ if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) {
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow =
+ mDocShell ? mDocShell->GetWindow() : nullptr;
+ NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE);
+ domWindow->Focus(mozilla::dom::CallerType::System);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDSURIContentListener::IsPreferred(const char* aContentType,
+ char** aDesiredContentType,
+ bool* aCanHandle) {
+ NS_ENSURE_ARG_POINTER(aCanHandle);
+ NS_ENSURE_ARG_POINTER(aDesiredContentType);
+
+ // the docshell has no idea if it is the preferred content provider or not.
+ // It needs to ask its parent if it is the preferred content handler or not...
+
+ nsCOMPtr<nsIURIContentListener> parentListener;
+ GetParentContentListener(getter_AddRefs(parentListener));
+ if (parentListener) {
+ return parentListener->IsPreferred(aContentType, aDesiredContentType,
+ aCanHandle);
+ }
+ // we used to return false here if we didn't have a parent properly registered
+ // at the top of the docshell hierarchy to dictate what content types this
+ // docshell should be a preferred handler for. But this really makes it hard
+ // for developers using iframe or browser tags because then they need to make
+ // sure they implement nsIURIContentListener otherwise all link clicks would
+ // get sent to another window because we said we weren't the preferred handler
+ // type. I'm going to change the default now... if we can handle the content,
+ // and someone didn't EXPLICITLY set a nsIURIContentListener at the top of our
+ // docshell chain, then we'll now always attempt to process the content
+ // ourselves...
+ return CanHandleContent(aContentType, true, aDesiredContentType, aCanHandle);
+}
+
+NS_IMETHODIMP
+nsDSURIContentListener::CanHandleContent(const char* aContentType,
+ bool aIsContentPreferred,
+ char** aDesiredContentType,
+ bool* aCanHandleContent) {
+ MOZ_ASSERT(aCanHandleContent, "Null out param?");
+ NS_ENSURE_ARG_POINTER(aDesiredContentType);
+
+ *aCanHandleContent = false;
+ *aDesiredContentType = nullptr;
+
+ if (aContentType) {
+ uint32_t canHandle =
+ nsWebNavigationInfo::IsTypeSupported(nsDependentCString(aContentType));
+ *aCanHandleContent = (canHandle != nsIWebNavigationInfo::UNSUPPORTED);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDSURIContentListener::GetLoadCookie(nsISupports** aLoadCookie) {
+ NS_IF_ADDREF(*aLoadCookie = nsDocShell::GetAsSupports(mDocShell));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDSURIContentListener::SetLoadCookie(nsISupports* aLoadCookie) {
+#ifdef DEBUG
+ RefPtr<nsDocLoader> cookieAsDocLoader =
+ nsDocLoader::GetAsDocLoader(aLoadCookie);
+ NS_ASSERTION(cookieAsDocLoader && cookieAsDocLoader == mDocShell,
+ "Invalid load cookie being set!");
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDSURIContentListener::GetParentContentListener(
+ nsIURIContentListener** aParentListener) {
+ if (mWeakParentContentListener) {
+ nsCOMPtr<nsIURIContentListener> tempListener =
+ do_QueryReferent(mWeakParentContentListener);
+ *aParentListener = tempListener;
+ NS_IF_ADDREF(*aParentListener);
+ } else {
+ *aParentListener = mParentContentListener;
+ NS_IF_ADDREF(*aParentListener);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDSURIContentListener::SetParentContentListener(
+ nsIURIContentListener* aParentListener) {
+ if (aParentListener) {
+ // Store the parent listener as a weak ref. Parents not supporting
+ // nsISupportsWeakReference assert but may still be used.
+ mParentContentListener = nullptr;
+ mWeakParentContentListener = do_GetWeakReference(aParentListener);
+ if (!mWeakParentContentListener) {
+ mParentContentListener = aParentListener;
+ }
+ } else {
+ mWeakParentContentListener = nullptr;
+ mParentContentListener = nullptr;
+ }
+ return NS_OK;
+}
diff --git a/docshell/base/nsDSURIContentListener.h b/docshell/base/nsDSURIContentListener.h
new file mode 100644
index 0000000000..61ed36456f
--- /dev/null
+++ b/docshell/base/nsDSURIContentListener.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDSURIContentListener_h__
+#define nsDSURIContentListener_h__
+
+#include "nsCOMPtr.h"
+#include "nsIURIContentListener.h"
+#include "nsWeakReference.h"
+#include "nsITimer.h"
+
+class nsDocShell;
+class nsIInterfaceRequestor;
+class nsIWebNavigationInfo;
+class nsPIDOMWindowOuter;
+
+// Helper Class to eventually close an already opened window
+class MaybeCloseWindowHelper final : public nsITimerCallback, public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ explicit MaybeCloseWindowHelper(
+ mozilla::dom::BrowsingContext* aContentContext);
+
+ /**
+ * Closes the provided window async (if mShouldCloseWindow is true) and
+ * returns a valid browsingContext to be used instead as parent for dialogs or
+ * similar things.
+ * In case mShouldCloseWindow is true, the returned BrowsingContext will be
+ * the window's opener (or original cross-group opener in the case of a
+ * `noopener` popup).
+ */
+ mozilla::dom::BrowsingContext* MaybeCloseWindow();
+
+ void SetShouldCloseWindow(bool aShouldCloseWindow);
+
+ protected:
+ ~MaybeCloseWindowHelper();
+
+ private:
+ already_AddRefed<mozilla::dom::BrowsingContext> ChooseNewBrowsingContext(
+ mozilla::dom::BrowsingContext* aBC);
+
+ /**
+ * The dom window associated to handle content.
+ */
+ RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext;
+
+ /**
+ * Used to close the window on a timer, to avoid any exceptions that are
+ * thrown if we try to close the window before it's fully loaded.
+ */
+ RefPtr<mozilla::dom::BrowsingContext> mBCToClose;
+ nsCOMPtr<nsITimer> mTimer;
+
+ /**
+ * This is set based on whether the channel indicates that a new window
+ * was opened, e.g. for a download, or was blocked. If so, then we
+ * close it.
+ */
+ bool mShouldCloseWindow;
+};
+
+class nsDSURIContentListener final : public nsIURIContentListener,
+ public nsSupportsWeakReference {
+ friend class nsDocShell;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURICONTENTLISTENER
+
+ protected:
+ explicit nsDSURIContentListener(nsDocShell* aDocShell);
+ virtual ~nsDSURIContentListener();
+
+ void DropDocShellReference() {
+ mDocShell = nullptr;
+ mExistingJPEGRequest = nullptr;
+ mExistingJPEGStreamListener = nullptr;
+ }
+
+ protected:
+ nsDocShell* mDocShell;
+ // Hack to handle multipart images without creating a new viewer
+ nsCOMPtr<nsIStreamListener> mExistingJPEGStreamListener;
+ nsCOMPtr<nsIChannel> mExistingJPEGRequest;
+
+ // Store the parent listener in either of these depending on
+ // if supports weak references or not. Proper weak refs are
+ // preferred and encouraged!
+ nsWeakPtr mWeakParentContentListener;
+ nsIURIContentListener* mParentContentListener;
+};
+
+#endif /* nsDSURIContentListener_h__ */
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
new file mode 100644
index 0000000000..f2a9e0fa59
--- /dev/null
+++ b/docshell/base/nsDocShell.cpp
@@ -0,0 +1,13763 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDocShell.h"
+
+#include <algorithm>
+
+#ifdef XP_WIN
+# include <process.h>
+# define getpid _getpid
+#else
+# include <unistd.h> // for getpid()
+#endif
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Casting.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/Components.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/InputTaskManager.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MediaFeatureChange.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/SimpleEnumerator.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_docshell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_extensions.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/StorageAccess.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/Telemetry.h"
+
+#include "mozilla/Unused.h"
+#include "mozilla/WidgetUtils.h"
+
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/dom/ChildProcessChannelListener.h"
+#include "mozilla/dom/ClientChannelHelper.h"
+#include "mozilla/dom/ClientHandle.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/dom/ClientManager.h"
+#include "mozilla/dom/ClientSource.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentFrameMessageManager.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/dom/HTMLIFrameElement.h"
+#include "mozilla/dom/PerformanceNavigation.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/dom/PopupBlocker.h"
+#include "mozilla/dom/ScreenOrientation.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ServiceWorkerInterceptController.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+#include "mozilla/dom/SessionHistoryEntry.h"
+#include "mozilla/dom/SessionStorageManager.h"
+#include "mozilla/dom/SessionStoreChangeListener.h"
+#include "mozilla/dom/SessionStoreChild.h"
+#include "mozilla/dom/SessionStoreUtils.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/ChildSHistory.h"
+#include "mozilla/dom/nsCSPContext.h"
+#include "mozilla/dom/nsHTTPSOnlyUtils.h"
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+#include "mozilla/dom/JSWindowActorChild.h"
+#include "mozilla/dom/DocumentBinding.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/net/DocumentChannel.h"
+#include "mozilla/net/DocumentChannelChild.h"
+#include "mozilla/net/ParentChannelWrapper.h"
+#include "mozilla/net/UrlClassifierFeatureFactory.h"
+#include "ReferrerInfo.h"
+
+#include "nsIAuthPrompt.h"
+#include "nsIAuthPrompt2.h"
+#include "nsICachingChannel.h"
+#include "nsICaptivePortalService.h"
+#include "nsIChannel.h"
+#include "nsIChannelEventSink.h"
+#include "nsIClassOfService.h"
+#include "nsIConsoleReportCollector.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsIController.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIDocumentViewer.h"
+#include "mozilla/dom/Document.h"
+#include "nsHTMLDocument.h"
+#include "nsIDocumentLoaderFactory.h"
+#include "nsIDOMWindow.h"
+#include "nsIEditingSession.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsIFormPOSTActionChannel.h"
+#include "nsIFrame.h"
+#include "nsIGlobalObject.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIIDNService.h"
+#include "nsIInputStreamChannel.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsILayoutHistoryState.h"
+#include "nsILoadInfo.h"
+#include "nsILoadURIDelegate.h"
+#include "nsIMultiPartChannel.h"
+#include "nsINestedURI.h"
+#include "nsINetworkPredictor.h"
+#include "nsINode.h"
+#include "nsINSSErrorsService.h"
+#include "nsIObserverService.h"
+#include "nsIOService.h"
+#include "nsIPrincipal.h"
+#include "nsIPrivacyTransitionObserver.h"
+#include "nsIPrompt.h"
+#include "nsIPromptCollection.h"
+#include "nsIPromptFactory.h"
+#include "nsIPublicKeyPinningService.h"
+#include "nsIReflowObserver.h"
+#include "nsIScriptChannel.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollObserver.h"
+#include "nsISupportsPrimitives.h"
+#include "nsISecureBrowserUI.h"
+#include "nsISeekableStream.h"
+#include "nsISelectionDisplay.h"
+#include "nsISHEntry.h"
+#include "nsISiteSecurityService.h"
+#include "nsISocketProvider.h"
+#include "nsIStringBundle.h"
+#include "nsIStructuredCloneContainer.h"
+#include "nsIBrowserChild.h"
+#include "nsITextToSubURI.h"
+#include "nsITimedChannel.h"
+#include "nsITimer.h"
+#include "nsITransportSecurityInfo.h"
+#include "nsIUploadChannel.h"
+#include "nsIURIFixup.h"
+#include "nsIURIMutator.h"
+#include "nsIURILoader.h"
+#include "nsIViewSourceChannel.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsIWebBrowserChromeFocus.h"
+#include "nsIWebBrowserFind.h"
+#include "nsIWebProgress.h"
+#include "nsIWidget.h"
+#include "nsIWindowWatcher.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsIX509Cert.h"
+#include "nsIXULRuntime.h"
+
+#include "nsCommandManager.h"
+#include "nsPIDOMWindow.h"
+#include "nsPIWindowRoot.h"
+
+#include "IHistory.h"
+#include "IUrlClassifierUITelemetry.h"
+
+#include "nsArray.h"
+#include "nsArrayUtils.h"
+#include "nsCExternalHandlerService.h"
+#include "nsContentDLF.h"
+#include "nsContentPolicyUtils.h" // NS_CheckContentLoadPolicy(...)
+#include "nsContentSecurityManager.h"
+#include "nsContentSecurityUtils.h"
+#include "nsContentUtils.h"
+#include "nsCURILoader.h"
+#include "nsDocShellCID.h"
+#include "nsDocShellEditorData.h"
+#include "nsDocShellEnumerator.h"
+#include "nsDocShellLoadState.h"
+#include "nsDocShellLoadTypes.h"
+#include "nsDOMCID.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsDSURIContentListener.h"
+#include "nsEditingSession.h"
+#include "nsError.h"
+#include "nsEscape.h"
+#include "nsFocusManager.h"
+#include "nsGlobalWindowInner.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsJSEnvironment.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsObjectLoadingContent.h"
+#include "nsPingListener.h"
+#include "nsPoint.h"
+#include "nsQueryObject.h"
+#include "nsQueryActor.h"
+#include "nsRect.h"
+#include "nsRefreshTimer.h"
+#include "nsSandboxFlags.h"
+#include "nsSHEntry.h"
+#include "nsSHistory.h"
+#include "nsSHEntry.h"
+#include "nsStructuredCloneContainer.h"
+#include "nsSubDocumentFrame.h"
+#include "nsURILoader.h"
+#include "nsURLHelper.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+#include "nsViewSourceHandler.h"
+#include "nsWebBrowserFind.h"
+#include "nsWhitespaceTokenizer.h"
+#include "nsWidgetsCID.h"
+#include "nsXULAppAPI.h"
+
+#include "ThirdPartyUtil.h"
+#include "GeckoProfiler.h"
+#include "mozilla/NullPrincipal.h"
+#include "Navigator.h"
+#include "prenv.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "sslerr.h"
+#include "mozpkix/pkix.h"
+#include "NSSErrorsService.h"
+
+#include "nsDocShellTelemetryUtils.h"
+
+#ifdef MOZ_PLACES
+# include "nsIFaviconService.h"
+# include "mozIPlacesPendingOperation.h"
+#endif
+
+#if NS_PRINT_PREVIEW
+# include "nsIDocumentViewerPrint.h"
+# include "nsIWebBrowserPrint.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::net;
+
+using mozilla::ipc::Endpoint;
+
+// Threshold value in ms for META refresh based redirects
+#define REFRESH_REDIRECT_TIMER 15000
+
+static mozilla::LazyLogModule gCharsetMenuLog("CharsetMenu");
+
+#define LOGCHARSETMENU(args) \
+ MOZ_LOG(gCharsetMenuLog, mozilla::LogLevel::Debug, args)
+
+#ifdef DEBUG
+unsigned long nsDocShell::gNumberOfDocShells = 0;
+static uint64_t gDocshellIDCounter = 0;
+
+static mozilla::LazyLogModule gDocShellLog("nsDocShell");
+static mozilla::LazyLogModule gDocShellAndDOMWindowLeakLogging(
+ "DocShellAndDOMWindowLeak");
+#endif
+static mozilla::LazyLogModule gDocShellLeakLog("nsDocShellLeak");
+extern mozilla::LazyLogModule gPageCacheLog;
+mozilla::LazyLogModule gSHLog("SessionHistory");
+extern mozilla::LazyLogModule gSHIPBFCacheLog;
+
+const char kAppstringsBundleURL[] =
+ "chrome://global/locale/appstrings.properties";
+
+static bool IsTopLevelDoc(BrowsingContext* aBrowsingContext,
+ nsILoadInfo* aLoadInfo) {
+ MOZ_ASSERT(aBrowsingContext);
+ MOZ_ASSERT(aLoadInfo);
+
+ if (aLoadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ return false;
+ }
+
+ return aBrowsingContext->IsTopContent();
+}
+
+// True if loading for top level document loading in active tab.
+static bool IsUrgentStart(BrowsingContext* aBrowsingContext,
+ nsILoadInfo* aLoadInfo, uint32_t aLoadType) {
+ MOZ_ASSERT(aBrowsingContext);
+ MOZ_ASSERT(aLoadInfo);
+
+ if (!IsTopLevelDoc(aBrowsingContext, aLoadInfo)) {
+ return false;
+ }
+
+ if (aLoadType &
+ (nsIDocShell::LOAD_CMD_NORMAL | nsIDocShell::LOAD_CMD_HISTORY)) {
+ return true;
+ }
+
+ return aBrowsingContext->IsActive();
+}
+
+nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext,
+ uint64_t aContentWindowID)
+ : nsDocLoader(true),
+ mContentWindowID(aContentWindowID),
+ mBrowsingContext(aBrowsingContext),
+ mParentCharset(nullptr),
+ mTreeOwner(nullptr),
+ mScrollbarPref(ScrollbarPreference::Auto),
+ mCharsetReloadState(eCharsetReloadInit),
+ mParentCharsetSource(0),
+ mFrameMargins(-1, -1),
+ mItemType(aBrowsingContext->IsContent() ? typeContent : typeChrome),
+ mPreviousEntryIndex(-1),
+ mLoadedEntryIndex(-1),
+ mBusyFlags(BUSY_FLAGS_NONE),
+ mAppType(nsIDocShell::APP_TYPE_UNKNOWN),
+ mLoadType(0),
+ mFailedLoadType(0),
+ mMetaViewportOverride(nsIDocShell::META_VIEWPORT_OVERRIDE_NONE),
+ mChannelToDisconnectOnPageHide(0),
+ mCreatingDocument(false),
+#ifdef DEBUG
+ mInEnsureScriptEnv(false),
+#endif
+ mInitialized(false),
+ mAllowSubframes(true),
+ mAllowMetaRedirects(true),
+ mAllowImages(true),
+ mAllowMedia(true),
+ mAllowDNSPrefetch(true),
+ mAllowWindowControl(true),
+ mCSSErrorReportingEnabled(false),
+ mAllowAuth(mItemType == typeContent),
+ mAllowKeywordFixup(false),
+ mDisableMetaRefreshWhenInactive(false),
+ mWindowDraggingAllowed(false),
+ mInFrameSwap(false),
+ mFiredUnloadEvent(false),
+ mEODForCurrentDocument(false),
+ mURIResultedInDocument(false),
+ mIsBeingDestroyed(false),
+ mIsExecutingOnLoadHandler(false),
+ mSavingOldViewer(false),
+ mInvisible(false),
+ mHasLoadedNonBlankURI(false),
+ mBlankTiming(false),
+ mTitleValidForCurrentURI(false),
+ mWillChangeProcess(false),
+ mIsNavigating(false),
+ mForcedAutodetection(false),
+ mCheckingSessionHistory(false),
+ mNeedToReportActiveAfterLoadingBecomesActive(false) {
+ // If no outer window ID was provided, generate a new one.
+ if (aContentWindowID == 0) {
+ mContentWindowID = nsContentUtils::GenerateWindowId();
+ }
+
+ MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, ("DOCSHELL %p created\n", this));
+
+#ifdef DEBUG
+ mDocShellID = gDocshellIDCounter++;
+ // We're counting the number of |nsDocShells| to help find leaks
+ ++gNumberOfDocShells;
+ MOZ_LOG(gDocShellAndDOMWindowLeakLogging, LogLevel::Info,
+ ("++DOCSHELL %p == %ld [pid = %d] [id = %" PRIu64 "]\n", (void*)this,
+ gNumberOfDocShells, getpid(), mDocShellID));
+#endif
+}
+
+nsDocShell::~nsDocShell() {
+ // Avoid notifying observers while we're in the dtor.
+ mIsBeingDestroyed = true;
+
+ Destroy();
+
+ if (mDocumentViewer) {
+ mDocumentViewer->Close(nullptr);
+ mDocumentViewer->Destroy();
+ mDocumentViewer = nullptr;
+ }
+
+ MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, ("DOCSHELL %p destroyed\n", this));
+
+#ifdef DEBUG
+ if (MOZ_LOG_TEST(gDocShellAndDOMWindowLeakLogging, LogLevel::Info)) {
+ nsAutoCString url;
+ if (mLastOpenedURI) {
+ url = mLastOpenedURI->GetSpecOrDefault();
+
+ // Data URLs can be very long, so truncate to avoid flooding the log.
+ const uint32_t maxURLLength = 1000;
+ if (url.Length() > maxURLLength) {
+ url.Truncate(maxURLLength);
+ }
+ }
+
+ // We're counting the number of |nsDocShells| to help find leaks
+ --gNumberOfDocShells;
+ MOZ_LOG(
+ gDocShellAndDOMWindowLeakLogging, LogLevel::Info,
+ ("--DOCSHELL %p == %ld [pid = %d] [id = %" PRIu64 "] [url = %s]\n",
+ (void*)this, gNumberOfDocShells, getpid(), mDocShellID, url.get()));
+ }
+#endif
+}
+
+bool nsDocShell::Initialize() {
+ if (mInitialized) {
+ // We've already been initialized.
+ return true;
+ }
+
+ NS_ASSERTION(mItemType == typeContent || mItemType == typeChrome,
+ "Unexpected item type in docshell");
+
+ NS_ENSURE_TRUE(Preferences::GetRootBranch(), false);
+ mInitialized = true;
+
+ mDisableMetaRefreshWhenInactive =
+ Preferences::GetBool("browser.meta_refresh_when_inactive.disabled",
+ mDisableMetaRefreshWhenInactive);
+
+ if (nsCOMPtr<nsIObserverService> serv = services::GetObserverService()) {
+ const char* msg = mItemType == typeContent ? NS_WEBNAVIGATION_CREATE
+ : NS_CHROME_WEBNAVIGATION_CREATE;
+ serv->NotifyWhenScriptSafe(GetAsSupports(this), msg, nullptr);
+ }
+
+ return true;
+}
+
+/* static */
+already_AddRefed<nsDocShell> nsDocShell::Create(
+ BrowsingContext* aBrowsingContext, uint64_t aContentWindowID) {
+ MOZ_ASSERT(aBrowsingContext, "DocShell without a BrowsingContext!");
+
+ nsresult rv;
+ RefPtr<nsDocShell> ds = new nsDocShell(aBrowsingContext, aContentWindowID);
+
+ // Initialize the underlying nsDocLoader.
+ rv = ds->nsDocLoader::InitWithBrowsingContext(aBrowsingContext);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // Create our ContentListener
+ ds->mContentListener = new nsDSURIContentListener(ds);
+
+ // We enable if we're in the parent process in order to support non-e10s
+ // configurations.
+ // Note: This check is duplicated in SharedWorkerInterfaceRequestor's
+ // constructor.
+ if (XRE_IsParentProcess()) {
+ ds->mInterceptController = new ServiceWorkerInterceptController();
+ }
+
+ // We want to hold a strong ref to the loadgroup, so it better hold a weak
+ // ref to us... use an InterfaceRequestorProxy to do this.
+ nsCOMPtr<nsIInterfaceRequestor> proxy = new InterfaceRequestorProxy(ds);
+ ds->mLoadGroup->SetNotificationCallbacks(proxy);
+
+ // XXX(nika): We have our BrowsingContext, so we might be able to skip this.
+ // It could be nice to directly set up our DocLoader tree?
+ rv = nsDocLoader::AddDocLoaderAsChildOfRoot(ds);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // Add |ds| as a progress listener to itself. A little weird, but simpler
+ // than reproducing all the listener-notification logic in overrides of the
+ // various methods via which nsDocLoader can be notified. Note that this
+ // holds an nsWeakPtr to |ds|, so it's ok.
+ rv = ds->AddProgressListener(ds, nsIWebProgress::NOTIFY_STATE_DOCUMENT |
+ nsIWebProgress::NOTIFY_STATE_NETWORK |
+ nsIWebProgress::NOTIFY_LOCATION);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // If our BrowsingContext has private browsing enabled, update the number of
+ // private browsing docshells.
+ if (aBrowsingContext->UsePrivateBrowsing()) {
+ ds->NotifyPrivateBrowsingChanged();
+ }
+
+ // If our parent window is present in this process, set up our parent now.
+ RefPtr<WindowContext> parentWC = aBrowsingContext->GetParentWindowContext();
+ if (parentWC && parentWC->IsInProcess()) {
+ // If we don't have a parent element anymore, we can't finish this load!
+ // How'd we get here?
+ RefPtr<Element> parentElement = aBrowsingContext->GetEmbedderElement();
+ if (!parentElement) {
+ MOZ_ASSERT_UNREACHABLE("nsDocShell::Create() - !parentElement");
+ return nullptr;
+ }
+
+ // We have an in-process parent window, but don't have a parent nsDocShell?
+ // How'd we get here!
+ nsCOMPtr<nsIDocShell> parentShell =
+ parentElement->OwnerDoc()->GetDocShell();
+ if (!parentShell) {
+ MOZ_ASSERT_UNREACHABLE("nsDocShell::Create() - !parentShell");
+ return nullptr;
+ }
+ parentShell->AddChild(ds);
+ }
+
+ // Make |ds| the primary DocShell for the given context.
+ aBrowsingContext->SetDocShell(ds);
+
+ // Set |ds| default load flags on load group.
+ ds->SetLoadGroupDefaultLoadFlags(aBrowsingContext->GetDefaultLoadFlags());
+
+ if (XRE_IsParentProcess()) {
+ aBrowsingContext->Canonical()->MaybeAddAsProgressListener(ds);
+ }
+
+ return ds.forget();
+}
+
+void nsDocShell::DestroyChildren() {
+ for (auto* child : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShellTreeItem> shell = do_QueryObject(child);
+ NS_ASSERTION(shell, "docshell has null child");
+
+ if (shell) {
+ shell->SetTreeOwner(nullptr);
+ }
+ }
+
+ nsDocLoader::DestroyChildren();
+}
+
+NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(nsDocShell, nsDocLoader,
+ mScriptGlobal, mInitialClientSource,
+ mBrowsingContext,
+ mChromeEventHandler)
+
+NS_IMPL_ADDREF_INHERITED(nsDocShell, nsDocLoader)
+NS_IMPL_RELEASE_INHERITED(nsDocShell, nsDocLoader)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDocShell)
+ NS_INTERFACE_MAP_ENTRY(nsIDocShell)
+ NS_INTERFACE_MAP_ENTRY(nsIDocShellTreeItem)
+ NS_INTERFACE_MAP_ENTRY(nsIWebNavigation)
+ NS_INTERFACE_MAP_ENTRY(nsIBaseWindow)
+ NS_INTERFACE_MAP_ENTRY(nsIRefreshURI)
+ NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIWebPageDescriptor)
+ NS_INTERFACE_MAP_ENTRY(nsIAuthPromptProvider)
+ NS_INTERFACE_MAP_ENTRY(nsILoadContext)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsINetworkInterceptController,
+ mInterceptController)
+NS_INTERFACE_MAP_END_INHERITING(nsDocLoader)
+
+NS_IMETHODIMP
+nsDocShell::GetInterface(const nsIID& aIID, void** aSink) {
+ MOZ_ASSERT(aSink, "null out param");
+
+ *aSink = nullptr;
+
+ if (aIID.Equals(NS_GET_IID(nsICommandManager))) {
+ NS_ENSURE_SUCCESS(EnsureCommandHandler(), NS_ERROR_FAILURE);
+ *aSink = static_cast<nsICommandManager*>(mCommandManager.get());
+ } else if (aIID.Equals(NS_GET_IID(nsIURIContentListener))) {
+ *aSink = mContentListener;
+ } else if ((aIID.Equals(NS_GET_IID(nsIScriptGlobalObject)) ||
+ aIID.Equals(NS_GET_IID(nsIGlobalObject)) ||
+ aIID.Equals(NS_GET_IID(nsPIDOMWindowOuter)) ||
+ aIID.Equals(NS_GET_IID(mozIDOMWindowProxy)) ||
+ aIID.Equals(NS_GET_IID(nsIDOMWindow))) &&
+ NS_SUCCEEDED(EnsureScriptEnvironment())) {
+ return mScriptGlobal->QueryInterface(aIID, aSink);
+ } else if (aIID.Equals(NS_GET_IID(Document)) &&
+ NS_SUCCEEDED(EnsureDocumentViewer())) {
+ RefPtr<Document> doc = mDocumentViewer->GetDocument();
+ doc.forget(aSink);
+ return *aSink ? NS_OK : NS_NOINTERFACE;
+ } else if (aIID.Equals(NS_GET_IID(nsIPrompt)) &&
+ NS_SUCCEEDED(EnsureScriptEnvironment())) {
+ nsresult rv;
+ nsCOMPtr<nsIWindowWatcher> wwatch =
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the an auth prompter for our window so that the parenting
+ // of the dialogs works as it should when using tabs.
+ nsIPrompt* prompt;
+ rv = wwatch->GetNewPrompter(mScriptGlobal, &prompt);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aSink = prompt;
+ return NS_OK;
+ } else if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) ||
+ aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) {
+ return NS_SUCCEEDED(GetAuthPrompt(PROMPT_NORMAL, aIID, aSink))
+ ? NS_OK
+ : NS_NOINTERFACE;
+ } else if (aIID.Equals(NS_GET_IID(nsISHistory))) {
+ // This is deprecated, you should instead directly get
+ // ChildSHistory from the browsing context.
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "Do not try to get a nsISHistory interface from nsIDocShell");
+ return NS_NOINTERFACE;
+ } else if (aIID.Equals(NS_GET_IID(nsIWebBrowserFind))) {
+ nsresult rv = EnsureFind();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ *aSink = mFind;
+ NS_ADDREF((nsISupports*)*aSink);
+ return NS_OK;
+ } else if (aIID.Equals(NS_GET_IID(nsISelectionDisplay))) {
+ if (PresShell* presShell = GetPresShell()) {
+ return presShell->QueryInterface(aIID, aSink);
+ }
+ } else if (aIID.Equals(NS_GET_IID(nsIDocShellTreeOwner))) {
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ nsresult rv = GetTreeOwner(getter_AddRefs(treeOwner));
+ if (NS_SUCCEEDED(rv) && treeOwner) {
+ return treeOwner->QueryInterface(aIID, aSink);
+ }
+ } else if (aIID.Equals(NS_GET_IID(nsIBrowserChild))) {
+ *aSink = GetBrowserChild().take();
+ return *aSink ? NS_OK : NS_ERROR_FAILURE;
+ } else {
+ return nsDocLoader::GetInterface(aIID, aSink);
+ }
+
+ NS_IF_ADDREF(((nsISupports*)*aSink));
+ return *aSink ? NS_OK : NS_NOINTERFACE;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetCancelContentJSEpoch(int32_t aEpoch) {
+ // Note: this gets called fairly early (before a pageload actually starts).
+ // We could probably defer this even longer.
+ nsCOMPtr<nsIBrowserChild> browserChild = GetBrowserChild();
+ static_cast<BrowserChild*>(browserChild.get())
+ ->SetCancelContentJSEpoch(aEpoch);
+ return NS_OK;
+}
+
+nsresult nsDocShell::CheckDisallowedJavascriptLoad(
+ nsDocShellLoadState* aLoadState) {
+ if (!net::SchemeIsJavascript(aLoadState->URI())) {
+ return NS_OK;
+ }
+
+ if (nsCOMPtr<nsIPrincipal> targetPrincipal =
+ GetInheritedPrincipal(/* aConsiderCurrentDocument */ true)) {
+ if (!aLoadState->TriggeringPrincipal()->Subsumes(targetPrincipal)) {
+ return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
+}
+
+NS_IMETHODIMP
+nsDocShell::LoadURI(nsDocShellLoadState* aLoadState, bool aSetNavigating) {
+ return LoadURI(aLoadState, aSetNavigating, false);
+}
+
+nsresult nsDocShell::LoadURI(nsDocShellLoadState* aLoadState,
+ bool aSetNavigating,
+ bool aContinueHandlingSubframeHistory) {
+ MOZ_ASSERT(aLoadState, "Must have a valid load state!");
+ // NOTE: This comparison between what appears to be internal/external load
+ // flags is intentional, as it's ensuring that the caller isn't using any of
+ // the flags reserved for implementations by the `nsIWebNavigation` interface.
+ // In the future, this check may be dropped.
+ MOZ_ASSERT(
+ (aLoadState->LoadFlags() & INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS) == 0,
+ "Should not have these flags set");
+ MOZ_ASSERT(aLoadState->TargetBrowsingContext().IsNull(),
+ "Targeting doesn't occur until InternalLoad");
+
+ if (!aLoadState->TriggeringPrincipal()) {
+ MOZ_ASSERT(false, "LoadURI must have a triggering principal");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_TRY(CheckDisallowedJavascriptLoad(aLoadState));
+
+ bool oldIsNavigating = mIsNavigating;
+ auto cleanupIsNavigating =
+ MakeScopeExit([&]() { mIsNavigating = oldIsNavigating; });
+ if (aSetNavigating) {
+ mIsNavigating = true;
+ }
+
+ PopupBlocker::PopupControlState popupState = PopupBlocker::openOverridden;
+ if (aLoadState->HasLoadFlags(LOAD_FLAGS_ALLOW_POPUPS)) {
+ popupState = PopupBlocker::openAllowed;
+ // If we allow popups as part of the navigation, ensure we fake a user
+ // interaction, so that popups can, in fact, be allowed to open.
+ if (WindowContext* wc = mBrowsingContext->GetCurrentWindowContext()) {
+ wc->NotifyUserGestureActivation();
+ }
+ }
+
+ AutoPopupStatePusher statePusher(popupState);
+
+ if (aLoadState->GetCancelContentJSEpoch().isSome()) {
+ SetCancelContentJSEpoch(*aLoadState->GetCancelContentJSEpoch());
+ }
+
+ // Note: we allow loads to get through here even if mFiredUnloadEvent is
+ // true; that case will get handled in LoadInternal or LoadHistoryEntry,
+ // so we pass false as the second parameter to IsNavigationAllowed.
+ // However, we don't allow the page to change location *in the middle of*
+ // firing beforeunload, so we do need to check if *beforeunload* is currently
+ // firing, so we call IsNavigationAllowed rather than just IsPrintingOrPP.
+ if (!IsNavigationAllowed(true, false)) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+
+ nsLoadFlags defaultLoadFlags = mBrowsingContext->GetDefaultLoadFlags();
+ if (aLoadState->HasLoadFlags(LOAD_FLAGS_FORCE_TRR)) {
+ defaultLoadFlags |= nsIRequest::LOAD_TRR_ONLY_MODE;
+ } else if (aLoadState->HasLoadFlags(LOAD_FLAGS_DISABLE_TRR)) {
+ defaultLoadFlags |= nsIRequest::LOAD_TRR_DISABLED_MODE;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(mBrowsingContext->SetDefaultLoadFlags(defaultLoadFlags));
+
+ if (!StartupTimeline::HasRecord(StartupTimeline::FIRST_LOAD_URI) &&
+ mItemType == typeContent && !NS_IsAboutBlank(aLoadState->URI())) {
+ StartupTimeline::RecordOnce(StartupTimeline::FIRST_LOAD_URI);
+ }
+
+ // LoadType used to be set to a default value here, if no LoadInfo/LoadState
+ // object was passed in. That functionality has been removed as of bug
+ // 1492648. LoadType should now be set up by the caller at the time they
+ // create their nsDocShellLoadState object to pass into LoadURI.
+
+ MOZ_LOG(
+ gDocShellLeakLog, LogLevel::Debug,
+ ("nsDocShell[%p]: loading %s with flags 0x%08x", this,
+ aLoadState->URI()->GetSpecOrDefault().get(), aLoadState->LoadFlags()));
+
+ if ((!aLoadState->LoadIsFromSessionHistory() &&
+ !LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(),
+ LOAD_FLAGS_REPLACE_HISTORY)) ||
+ aContinueHandlingSubframeHistory) {
+ // This is possibly a subframe, so handle it accordingly.
+ //
+ // If history exists, it will be loaded into the aLoadState object, and the
+ // LoadType will be changed.
+ if (MaybeHandleSubframeHistory(aLoadState,
+ aContinueHandlingSubframeHistory)) {
+ // MaybeHandleSubframeHistory returns true if we need to continue loading
+ // asynchronously.
+ return NS_OK;
+ }
+ }
+
+ if (aLoadState->LoadIsFromSessionHistory()) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell[%p]: loading from session history", this));
+
+ if (!mozilla::SessionHistoryInParent()) {
+ nsCOMPtr<nsISHEntry> entry = aLoadState->SHEntry();
+ return LoadHistoryEntry(entry, aLoadState->LoadType(),
+ aLoadState->HasValidUserGestureActivation());
+ }
+
+ // FIXME Null check aLoadState->GetLoadingSessionHistoryInfo()?
+ return LoadHistoryEntry(*aLoadState->GetLoadingSessionHistoryInfo(),
+ aLoadState->LoadType(),
+ aLoadState->HasValidUserGestureActivation());
+ }
+
+ // On history navigation via Back/Forward buttons, don't execute
+ // automatic JavaScript redirection such as |location.href = ...| or
+ // |window.open()|
+ //
+ // LOAD_NORMAL: window.open(...) etc.
+ // LOAD_STOP_CONTENT: location.href = ..., location.assign(...)
+ if ((aLoadState->LoadType() == LOAD_NORMAL ||
+ aLoadState->LoadType() == LOAD_STOP_CONTENT) &&
+ ShouldBlockLoadingForBackButton()) {
+ return NS_OK;
+ }
+
+ BrowsingContext::Type bcType = mBrowsingContext->GetType();
+
+ // Set up the inheriting principal in LoadState.
+ nsresult rv = aLoadState->SetupInheritingPrincipal(
+ bcType, mBrowsingContext->OriginAttributesRef());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aLoadState->SetupTriggeringPrincipal(
+ mBrowsingContext->OriginAttributesRef());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aLoadState->CalculateLoadURIFlags();
+
+ MOZ_ASSERT(aLoadState->TypeHint().IsVoid(),
+ "Typehint should be null when calling InternalLoad from LoadURI");
+ MOZ_ASSERT(aLoadState->FileName().IsVoid(),
+ "FileName should be null when calling InternalLoad from LoadURI");
+ MOZ_ASSERT(!aLoadState->LoadIsFromSessionHistory(),
+ "Shouldn't be loading from an entry when calling InternalLoad "
+ "from LoadURI");
+
+ // If we have a system triggering principal, we can assume that this load was
+ // triggered by some UI in the browser chrome, such as the URL bar or
+ // bookmark bar. This should count as a user interaction for the current sh
+ // entry, so that the user may navigate back to the current entry, from the
+ // entry that is going to be added as part of this load.
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal =
+ aLoadState->TriggeringPrincipal();
+ if (triggeringPrincipal && triggeringPrincipal->IsSystemPrincipal()) {
+ if (mozilla::SessionHistoryInParent()) {
+ WindowContext* topWc = mBrowsingContext->GetTopWindowContext();
+ if (topWc && !topWc->IsDiscarded()) {
+ MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(true));
+ }
+ } else {
+ bool oshe = false;
+ nsCOMPtr<nsISHEntry> currentSHEntry;
+ GetCurrentSHEntry(getter_AddRefs(currentSHEntry), &oshe);
+ if (currentSHEntry) {
+ currentSHEntry->SetHasUserInteraction(true);
+ }
+ }
+ }
+
+ rv = InternalLoad(aLoadState);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aLoadState->GetOriginalURIString().isSome()) {
+ // Save URI string in case it's needed later when
+ // sending to search engine service in EndPageLoad()
+ mOriginalUriString = *aLoadState->GetOriginalURIString();
+ }
+
+ return NS_OK;
+}
+
+bool nsDocShell::IsLoadingFromSessionHistory() {
+ return mActiveEntryIsLoadingFromSessionHistory;
+}
+
+// StopDetector is modeled similarly to OnloadBlocker; it is a rather
+// dummy nsIRequest implementation which can be added to an nsILoadGroup to
+// detect Cancel calls.
+class StopDetector final : public nsIRequest {
+ public:
+ StopDetector() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUEST
+
+ bool Canceled() { return mCanceled; }
+
+ private:
+ ~StopDetector() = default;
+
+ bool mCanceled = false;
+};
+
+NS_IMPL_ISUPPORTS(StopDetector, nsIRequest)
+
+NS_IMETHODIMP
+StopDetector::GetName(nsACString& aResult) {
+ aResult.AssignLiteral("about:stop-detector");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+StopDetector::IsPending(bool* aRetVal) {
+ *aRetVal = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+StopDetector::GetStatus(nsresult* aStatus) {
+ *aStatus = NS_OK;
+ return NS_OK;
+}
+
+NS_IMETHODIMP StopDetector::SetCanceledReason(const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP StopDetector::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP StopDetector::CancelWithReason(nsresult aStatus,
+ const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+NS_IMETHODIMP
+StopDetector::Cancel(nsresult aStatus) {
+ mCanceled = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+StopDetector::Suspend(void) { return NS_OK; }
+NS_IMETHODIMP
+StopDetector::Resume(void) { return NS_OK; }
+
+NS_IMETHODIMP
+StopDetector::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ *aLoadGroup = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+StopDetector::SetLoadGroup(nsILoadGroup* aLoadGroup) { return NS_OK; }
+
+NS_IMETHODIMP
+StopDetector::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = nsIRequest::LOAD_NORMAL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+StopDetector::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+StopDetector::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+StopDetector::SetLoadFlags(nsLoadFlags aLoadFlags) { return NS_OK; }
+
+bool nsDocShell::MaybeHandleSubframeHistory(
+ nsDocShellLoadState* aLoadState, bool aContinueHandlingSubframeHistory) {
+ // First, verify if this is a subframe.
+ // Note, it is ok to rely on docshell here and not browsing context since when
+ // an iframe is created, it has first in-process docshell.
+ nsCOMPtr<nsIDocShellTreeItem> parentAsItem;
+ GetInProcessSameTypeParent(getter_AddRefs(parentAsItem));
+ nsCOMPtr<nsIDocShell> parentDS(do_QueryInterface(parentAsItem));
+
+ if (!parentDS || parentDS == static_cast<nsIDocShell*>(this)) {
+ if (mBrowsingContext && mBrowsingContext->IsTop()) {
+ // This is the root docshell. If we got here while
+ // executing an onLoad Handler,this load will not go
+ // into session history.
+ // XXX Why is this code in a method which deals with iframes!
+ if (aLoadState->IsFormSubmission()) {
+#ifdef DEBUG
+ if (!mEODForCurrentDocument) {
+ const MaybeDiscarded<BrowsingContext>& targetBC =
+ aLoadState->TargetBrowsingContext();
+ MOZ_ASSERT_IF(GetBrowsingContext() == targetBC.get(),
+ aLoadState->LoadType() == LOAD_NORMAL_REPLACE);
+ }
+#endif
+ } else {
+ bool inOnLoadHandler = false;
+ GetIsExecutingOnLoadHandler(&inOnLoadHandler);
+ if (inOnLoadHandler) {
+ aLoadState->SetLoadType(LOAD_NORMAL_REPLACE);
+ }
+ }
+ }
+ return false;
+ }
+
+ /* OK. It is a subframe. Checkout the parent's loadtype. If the parent was
+ * loaded through a history mechanism, then get the SH entry for the child
+ * from the parent. This is done to restore frameset navigation while going
+ * back/forward. If the parent was loaded through any other loadType, set the
+ * child's loadType too accordingly, so that session history does not get
+ * confused.
+ */
+
+ // Get the parent's load type
+ uint32_t parentLoadType;
+ parentDS->GetLoadType(&parentLoadType);
+
+ if (!aContinueHandlingSubframeHistory) {
+ if (mozilla::SessionHistoryInParent()) {
+ if (nsDocShell::Cast(parentDS.get())->IsLoadingFromSessionHistory() &&
+ !GetCreatedDynamically()) {
+ if (XRE_IsContentProcess()) {
+ dom::ContentChild* contentChild = dom::ContentChild::GetSingleton();
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ GetLoadGroup(getter_AddRefs(loadGroup));
+ if (contentChild && loadGroup && !mCheckingSessionHistory) {
+ RefPtr<Document> parentDoc = parentDS->GetDocument();
+ parentDoc->BlockOnload();
+ RefPtr<BrowsingContext> browsingContext = mBrowsingContext;
+ Maybe<uint64_t> currentLoadIdentifier =
+ mBrowsingContext->GetCurrentLoadIdentifier();
+ RefPtr<nsDocShellLoadState> loadState = aLoadState;
+ bool isNavigating = mIsNavigating;
+ RefPtr<StopDetector> stopDetector = new StopDetector();
+ loadGroup->AddRequest(stopDetector, nullptr);
+ // Need to set mCheckingSessionHistory so that
+ // GetIsAttemptingToNavigate() returns true.
+ mCheckingSessionHistory = true;
+
+ auto resolve =
+ [currentLoadIdentifier, browsingContext, parentDoc, loadState,
+ isNavigating, loadGroup, stopDetector](
+ mozilla::Maybe<LoadingSessionHistoryInfo>&& aResult) {
+ RefPtr<nsDocShell> docShell =
+ static_cast<nsDocShell*>(browsingContext->GetDocShell());
+ auto unblockParent = MakeScopeExit(
+ [loadGroup, stopDetector, parentDoc, docShell]() {
+ if (docShell) {
+ docShell->mCheckingSessionHistory = false;
+ }
+ loadGroup->RemoveRequest(stopDetector, nullptr, NS_OK);
+ parentDoc->UnblockOnload(false);
+ });
+
+ if (!docShell || !docShell->mCheckingSessionHistory) {
+ return;
+ }
+
+ if (stopDetector->Canceled()) {
+ return;
+ }
+ if (currentLoadIdentifier ==
+ browsingContext->GetCurrentLoadIdentifier() &&
+ aResult.isSome()) {
+ loadState->SetLoadingSessionHistoryInfo(aResult.value());
+ // This is an initial subframe load from the session
+ // history, index doesn't need to be updated.
+ loadState->SetLoadIsFromSessionHistory(0, false);
+ }
+
+ // We got the results back from the parent process, call
+ // LoadURI again with the possibly updated data.
+ docShell->LoadURI(loadState, isNavigating, true);
+ };
+ auto reject = [loadGroup, stopDetector, browsingContext,
+ parentDoc](mozilla::ipc::ResponseRejectReason) {
+ RefPtr<nsDocShell> docShell =
+ static_cast<nsDocShell*>(browsingContext->GetDocShell());
+ if (docShell) {
+ docShell->mCheckingSessionHistory = false;
+ }
+ // In practise reject shouldn't be called ever.
+ loadGroup->RemoveRequest(stopDetector, nullptr, NS_OK);
+ parentDoc->UnblockOnload(false);
+ };
+ contentChild->SendGetLoadingSessionHistoryInfoFromParent(
+ mBrowsingContext, std::move(resolve), std::move(reject));
+ return true;
+ }
+ } else {
+ Maybe<LoadingSessionHistoryInfo> info;
+ mBrowsingContext->Canonical()->GetLoadingSessionHistoryInfoFromParent(
+ info);
+ if (info.isSome()) {
+ aLoadState->SetLoadingSessionHistoryInfo(info.value());
+ // This is an initial subframe load from the session
+ // history, index doesn't need to be updated.
+ aLoadState->SetLoadIsFromSessionHistory(0, false);
+ }
+ }
+ }
+ } else {
+ // Get the ShEntry for the child from the parent
+ nsCOMPtr<nsISHEntry> currentSH;
+ bool oshe = false;
+ parentDS->GetCurrentSHEntry(getter_AddRefs(currentSH), &oshe);
+ bool dynamicallyAddedChild = GetCreatedDynamically();
+
+ if (!dynamicallyAddedChild && !oshe && currentSH) {
+ // Only use the old SHEntry, if we're sure enough that
+ // it wasn't originally for some other frame.
+ nsCOMPtr<nsISHEntry> shEntry;
+ currentSH->GetChildSHEntryIfHasNoDynamicallyAddedChild(
+ mBrowsingContext->ChildOffset(), getter_AddRefs(shEntry));
+ if (shEntry) {
+ aLoadState->SetSHEntry(shEntry);
+ }
+ }
+ }
+ }
+
+ // Make some decisions on the child frame's loadType based on the
+ // parent's loadType, if the subframe hasn't loaded anything into it.
+ //
+ // In some cases privileged scripts may try to get the DOMWindow
+ // reference of this docshell before the loading starts, causing the
+ // initial about:blank content viewer being created and mCurrentURI being
+ // set. To handle this case we check if mCurrentURI is about:blank and
+ // currentSHEntry is null.
+ bool oshe = false;
+ nsCOMPtr<nsISHEntry> currentChildEntry;
+ GetCurrentSHEntry(getter_AddRefs(currentChildEntry), &oshe);
+
+ if (mCurrentURI && (!NS_IsAboutBlank(mCurrentURI) || currentChildEntry ||
+ mLoadingEntry || mActiveEntry)) {
+ // This is a pre-existing subframe. If
+ // 1. The load of this frame was not originally initiated by session
+ // history directly (i.e. (!shEntry) condition succeeded, but it can
+ // still be a history load on parent which causes this frame being
+ // loaded), which we checked with the above assert, and
+ // 2. mCurrentURI is not null, nor the initial about:blank,
+ // it is possible that a parent's onLoadHandler or even self's
+ // onLoadHandler is loading a new page in this child. Check parent's and
+ // self's busy flag and if it is set, we don't want this onLoadHandler
+ // load to get in to session history.
+ BusyFlags parentBusy = parentDS->GetBusyFlags();
+ BusyFlags selfBusy = GetBusyFlags();
+
+ if (parentBusy & BUSY_FLAGS_BUSY || selfBusy & BUSY_FLAGS_BUSY) {
+ aLoadState->SetLoadType(LOAD_NORMAL_REPLACE);
+ aLoadState->ClearLoadIsFromSessionHistory();
+ }
+ return false;
+ }
+
+ // This is a newly created frame. Check for exception cases first.
+ // By default the subframe will inherit the parent's loadType.
+ if (aLoadState->LoadIsFromSessionHistory() &&
+ (parentLoadType == LOAD_NORMAL || parentLoadType == LOAD_LINK)) {
+ // The parent was loaded normally. In this case, this *brand new*
+ // child really shouldn't have a SHEntry. If it does, it could be
+ // because the parent is replacing an existing frame with a new frame,
+ // in the onLoadHandler. We don't want this url to get into session
+ // history. Clear off shEntry, and set load type to
+ // LOAD_BYPASS_HISTORY.
+ bool inOnLoadHandler = false;
+ parentDS->GetIsExecutingOnLoadHandler(&inOnLoadHandler);
+ if (inOnLoadHandler) {
+ aLoadState->SetLoadType(LOAD_NORMAL_REPLACE);
+ aLoadState->ClearLoadIsFromSessionHistory();
+ }
+ } else if (parentLoadType == LOAD_REFRESH) {
+ // Clear shEntry. For refresh loads, we have to load
+ // what comes through the pipe, not what's in history.
+ aLoadState->ClearLoadIsFromSessionHistory();
+ } else if ((parentLoadType == LOAD_BYPASS_HISTORY) ||
+ (aLoadState->LoadIsFromSessionHistory() &&
+ ((parentLoadType & LOAD_CMD_HISTORY) ||
+ (parentLoadType == LOAD_RELOAD_NORMAL) ||
+ (parentLoadType == LOAD_RELOAD_CHARSET_CHANGE) ||
+ (parentLoadType == LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE) ||
+ (parentLoadType ==
+ LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE)))) {
+ // If the parent url, bypassed history or was loaded from
+ // history, pass on the parent's loadType to the new child
+ // frame too, so that the child frame will also
+ // avoid getting into history.
+ aLoadState->SetLoadType(parentLoadType);
+ } else if (parentLoadType == LOAD_ERROR_PAGE) {
+ // If the parent document is an error page, we don't
+ // want to update global/session history. However,
+ // this child frame is not an error page.
+ aLoadState->SetLoadType(LOAD_BYPASS_HISTORY);
+ } else if ((parentLoadType == LOAD_RELOAD_BYPASS_CACHE) ||
+ (parentLoadType == LOAD_RELOAD_BYPASS_PROXY) ||
+ (parentLoadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE)) {
+ // the new frame should inherit the parent's load type so that it also
+ // bypasses the cache and/or proxy
+ aLoadState->SetLoadType(parentLoadType);
+ }
+
+ return false;
+}
+
+/*
+ * Reset state to a new content model within the current document and the
+ * document viewer. Called by the document before initiating an out of band
+ * document.write().
+ */
+NS_IMETHODIMP
+nsDocShell::PrepareForNewContentModel() {
+ // Clear out our form control state, because the state of controls
+ // in the pre-open() document should not affect the state of
+ // controls that are now going to be written.
+ SetLayoutHistoryState(nullptr);
+ mEODForCurrentDocument = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::FirePageHideNotification(bool aIsUnload) {
+ FirePageHideNotificationInternal(aIsUnload, false);
+ return NS_OK;
+}
+
+void nsDocShell::FirePageHideNotificationInternal(
+ bool aIsUnload, bool aSkipCheckingDynEntries) {
+ if (mDocumentViewer && !mFiredUnloadEvent) {
+ // Keep an explicit reference since calling PageHide could release
+ // mDocumentViewer
+ nsCOMPtr<nsIDocumentViewer> viewer(mDocumentViewer);
+ mFiredUnloadEvent = true;
+
+ if (mTiming) {
+ mTiming->NotifyUnloadEventStart();
+ }
+
+ viewer->PageHide(aIsUnload);
+
+ if (mTiming) {
+ mTiming->NotifyUnloadEventEnd();
+ }
+
+ AutoTArray<nsCOMPtr<nsIDocShell>, 8> kids;
+ uint32_t n = mChildList.Length();
+ kids.SetCapacity(n);
+ for (uint32_t i = 0; i < n; i++) {
+ kids.AppendElement(do_QueryInterface(ChildAt(i)));
+ }
+
+ n = kids.Length();
+ for (uint32_t i = 0; i < n; ++i) {
+ RefPtr<nsDocShell> child = static_cast<nsDocShell*>(kids[i].get());
+ if (child) {
+ // Skip checking dynamic subframe entries in our children.
+ child->FirePageHideNotificationInternal(aIsUnload, true);
+ }
+ }
+
+ // If the document is unloading, remove all dynamic subframe entries.
+ if (aIsUnload && !aSkipCheckingDynEntries) {
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (rootSH) {
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ ("nsDocShell %p unloading, remove dynamic subframe entries", this));
+ if (mozilla::SessionHistoryInParent()) {
+ if (mActiveEntry) {
+ mBrowsingContext->RemoveDynEntriesFromActiveSessionHistoryEntry();
+ }
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p unloading, no active entries", this));
+ } else if (mOSHE) {
+ int32_t index = rootSH->Index();
+ rootSH->LegacySHistory()->RemoveDynEntries(index, mOSHE);
+ }
+ }
+ }
+
+ // Now make sure our editor, if any, is detached before we go
+ // any farther.
+ DetachEditorFromWindow();
+ }
+}
+
+void nsDocShell::ThawFreezeNonRecursive(bool aThaw) {
+ MOZ_ASSERT(mozilla::BFCacheInParent());
+
+ if (!mScriptGlobal) {
+ return;
+ }
+
+ if (RefPtr<nsGlobalWindowInner> inner =
+ nsGlobalWindowInner::Cast(mScriptGlobal->GetCurrentInnerWindow())) {
+ if (aThaw) {
+ inner->Thaw(false);
+ } else {
+ inner->Freeze(false);
+ }
+ }
+}
+
+void nsDocShell::FirePageHideShowNonRecursive(bool aShow) {
+ MOZ_ASSERT(mozilla::BFCacheInParent());
+
+ if (!mDocumentViewer) {
+ return;
+ }
+
+ // Emulate what non-SHIP BFCache does too. In pageshow case
+ // add and remove a request and before that call SetCurrentURI to get
+ // the location change notification.
+ // For pagehide, set mFiredUnloadEvent to true, so that unload doesn't fire.
+ nsCOMPtr<nsIDocumentViewer> viewer(mDocumentViewer);
+ if (aShow) {
+ viewer->SetIsHidden(false);
+ mRefreshURIList = std::move(mBFCachedRefreshURIList);
+ RefreshURIFromQueue();
+ mFiredUnloadEvent = false;
+ RefPtr<Document> doc = viewer->GetDocument();
+ if (doc) {
+ doc->NotifyActivityChanged();
+ nsCOMPtr<nsPIDOMWindowInner> inner =
+ mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : nullptr;
+ if (mBrowsingContext->IsTop()) {
+ doc->NotifyPossibleTitleChange(false);
+ doc->SetLoadingOrRestoredFromBFCacheTimeStampToNow();
+ if (inner) {
+ // Now that we have found the inner window of the page restored
+ // from the history, we have to make sure that
+ // performance.navigation.type is 2.
+ // Traditionally this type change has been done to the top level page
+ // only.
+ Performance* performance = inner->GetPerformance();
+ if (performance) {
+ performance->GetDOMTiming()->NotifyRestoreStart();
+ }
+ }
+ }
+
+ nsCOMPtr<nsIChannel> channel = doc->GetChannel();
+ if (channel) {
+ SetLoadType(LOAD_HISTORY);
+ mEODForCurrentDocument = false;
+ mIsRestoringDocument = true;
+ mLoadGroup->AddRequest(channel, nullptr);
+ SetCurrentURI(doc->GetDocumentURI(), channel,
+ /* aFireOnLocationChange */ true,
+ /* aIsInitialAboutBlank */ false,
+ /* aLocationFlags */ 0);
+ mLoadGroup->RemoveRequest(channel, nullptr, NS_OK);
+ mIsRestoringDocument = false;
+ }
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (presShell) {
+ presShell->Thaw(false);
+ }
+
+ if (inner) {
+ inner->FireDelayedDOMEvents(false);
+ }
+ }
+ } else if (!mFiredUnloadEvent) {
+ // XXXBFCache check again that the page can enter bfcache.
+ // XXXBFCache should mTiming->NotifyUnloadEventStart()/End() be called here?
+
+ if (mRefreshURIList) {
+ RefreshURIToQueue();
+ mBFCachedRefreshURIList = std::move(mRefreshURIList);
+ } else {
+ // If Stop was called, the list was moved to mSavedRefreshURIList after
+ // calling SuspendRefreshURIs, which calls RefreshURIToQueue.
+ mBFCachedRefreshURIList = std::move(mSavedRefreshURIList);
+ }
+
+ mFiredUnloadEvent = true;
+ viewer->PageHide(false);
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (presShell) {
+ presShell->Freeze(false);
+ }
+ }
+}
+
+nsresult nsDocShell::Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) {
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+ if (NS_WARN_IF(!GetWindow())) {
+ // Window should only be unavailable after destroyed.
+ MOZ_ASSERT(mIsBeingDestroyed);
+ return NS_ERROR_FAILURE;
+ }
+ return SchedulerGroup::Dispatch(runnable.forget());
+}
+
+NS_IMETHODIMP
+nsDocShell::DispatchLocationChangeEvent() {
+ return Dispatch(NewRunnableMethod("nsDocShell::FireDummyOnLocationChange",
+ this,
+ &nsDocShell::FireDummyOnLocationChange));
+}
+
+NS_IMETHODIMP
+nsDocShell::StartDelayedAutoplayMediaComponents() {
+ RefPtr<nsPIDOMWindowOuter> outerWindow = GetWindow();
+ if (outerWindow) {
+ outerWindow->ActivateMediaComponents();
+ }
+ return NS_OK;
+}
+
+bool nsDocShell::MaybeInitTiming() {
+ if (mTiming && !mBlankTiming) {
+ return false;
+ }
+
+ bool canBeReset = false;
+
+ if (mScriptGlobal && mBlankTiming) {
+ nsPIDOMWindowInner* innerWin = mScriptGlobal->GetCurrentInnerWindow();
+ if (innerWin && innerWin->GetPerformance()) {
+ mTiming = innerWin->GetPerformance()->GetDOMTiming();
+ mBlankTiming = false;
+ }
+ }
+
+ if (!mTiming) {
+ mTiming = new nsDOMNavigationTiming(this);
+ canBeReset = true;
+ }
+
+ mTiming->NotifyNavigationStart(
+ mBrowsingContext->IsActive()
+ ? nsDOMNavigationTiming::DocShellState::eActive
+ : nsDOMNavigationTiming::DocShellState::eInactive);
+
+ return canBeReset;
+}
+
+void nsDocShell::MaybeResetInitTiming(bool aReset) {
+ if (aReset) {
+ mTiming = nullptr;
+ }
+}
+
+nsDOMNavigationTiming* nsDocShell::GetNavigationTiming() const {
+ return mTiming;
+}
+
+nsPresContext* nsDocShell::GetEldestPresContext() {
+ nsIDocumentViewer* viewer = mDocumentViewer;
+ while (viewer) {
+ nsIDocumentViewer* prevViewer = viewer->GetPreviousViewer();
+ if (!prevViewer) {
+ return viewer->GetPresContext();
+ }
+ viewer = prevViewer;
+ }
+
+ return nullptr;
+}
+
+nsPresContext* nsDocShell::GetPresContext() {
+ if (!mDocumentViewer) {
+ return nullptr;
+ }
+
+ return mDocumentViewer->GetPresContext();
+}
+
+PresShell* nsDocShell::GetPresShell() {
+ nsPresContext* presContext = GetPresContext();
+ return presContext ? presContext->GetPresShell() : nullptr;
+}
+
+PresShell* nsDocShell::GetEldestPresShell() {
+ nsPresContext* presContext = GetEldestPresContext();
+
+ if (presContext) {
+ return presContext->GetPresShell();
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDocViewer(nsIDocumentViewer** aDocumentViewer) {
+ NS_ENSURE_ARG_POINTER(aDocumentViewer);
+
+ *aDocumentViewer = mDocumentViewer;
+ NS_IF_ADDREF(*aDocumentViewer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetOuterWindowID(uint64_t* aWindowID) {
+ *aWindowID = mContentWindowID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetChromeEventHandler(EventTarget* aChromeEventHandler) {
+ mChromeEventHandler = aChromeEventHandler;
+
+ if (mScriptGlobal) {
+ mScriptGlobal->SetChromeEventHandler(mChromeEventHandler);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetChromeEventHandler(EventTarget** aChromeEventHandler) {
+ NS_ENSURE_ARG_POINTER(aChromeEventHandler);
+ RefPtr<EventTarget> handler = mChromeEventHandler;
+ handler.forget(aChromeEventHandler);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetCurrentURIForSessionStore(nsIURI* aURI) {
+ // Note that securityUI will set STATE_IS_INSECURE, even if
+ // the scheme of |aURI| is "https".
+ SetCurrentURI(aURI, nullptr,
+ /* aFireOnLocationChange */
+ true,
+ /* aIsInitialAboutBlank */
+ false,
+ /* aLocationFlags */
+ nsIWebProgressListener::LOCATION_CHANGE_SESSION_STORE);
+ return NS_OK;
+}
+
+bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
+ bool aFireOnLocationChange,
+ bool aIsInitialAboutBlank,
+ uint32_t aLocationFlags) {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ MOZ_LOG(gDocShellLeakLog, LogLevel::Debug,
+ ("DOCSHELL %p SetCurrentURI %s\n", this,
+ aURI ? aURI->GetSpecOrDefault().get() : ""));
+
+ // We don't want to send a location change when we're displaying an error
+ // page, and we don't want to change our idea of "current URI" either
+ if (mLoadType == LOAD_ERROR_PAGE) {
+ return false;
+ }
+
+ bool uriIsEqual = false;
+ if (!mCurrentURI || !aURI ||
+ NS_FAILED(mCurrentURI->Equals(aURI, &uriIsEqual)) || !uriIsEqual) {
+ mTitleValidForCurrentURI = false;
+ }
+
+ SetCurrentURIInternal(aURI);
+
+#ifdef DEBUG
+ mLastOpenedURI = aURI;
+#endif
+
+ if (!NS_IsAboutBlank(mCurrentURI)) {
+ mHasLoadedNonBlankURI = true;
+ }
+
+ // Don't fire onLocationChange when creating a subframe's initial about:blank
+ // document, as this can happen when it's not safe for us to run script.
+ if (aIsInitialAboutBlank && !mHasLoadedNonBlankURI &&
+ !mBrowsingContext->IsTop()) {
+ MOZ_ASSERT(!aRequest && aLocationFlags == 0);
+ return false;
+ }
+
+ MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+
+ if (aFireOnLocationChange) {
+ FireOnLocationChange(this, aRequest, aURI, aLocationFlags);
+ }
+ return !aFireOnLocationChange;
+}
+
+void nsDocShell::SetCurrentURIInternal(nsIURI* aURI) {
+ mCurrentURI = aURI;
+ if (mBrowsingContext) {
+ mBrowsingContext->ClearCachedValuesOfLocations();
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCharset(nsACString& aCharset) {
+ aCharset.Truncate();
+
+ PresShell* presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+ Document* doc = presShell->GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+ doc->GetDocumentCharacterSet()->Name(aCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::ForceEncodingDetection() {
+ nsCOMPtr<nsIDocumentViewer> viewer;
+ GetDocViewer(getter_AddRefs(viewer));
+ if (!viewer) {
+ return NS_OK;
+ }
+
+ Document* doc = viewer->GetDocument();
+ if (!doc || doc->WillIgnoreCharsetOverride()) {
+ return NS_OK;
+ }
+
+ mForcedAutodetection = true;
+
+ nsIURI* url = doc->GetOriginalURI();
+ bool isFileURL = url && SchemeIsFile(url);
+
+ int32_t charsetSource = doc->GetDocumentCharacterSetSource();
+ auto encoding = doc->GetDocumentCharacterSet();
+ // AsHTMLDocument is valid, because we called
+ // WillIgnoreCharsetOverride() above.
+ if (doc->AsHTMLDocument()->IsPlainText()) {
+ switch (charsetSource) {
+ case kCharsetFromInitialAutoDetectionASCII:
+ // Deliberately no final version
+ LOGCHARSETMENU(("TEXT:UnlabeledAscii"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::UnlabeledAscii);
+ break;
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Generic:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Generic:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8GenericInitialWasASCII:
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Content:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Content:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8ContentInitialWasASCII:
+ LOGCHARSETMENU(("TEXT:UnlabeledNonUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::
+ UnlabeledNonUtf8);
+ break;
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLDInitialWasASCII:
+ LOGCHARSETMENU(("TEXT:UnlabeledNonUtf8TLD"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::
+ UnlabeledNonUtf8TLD);
+ break;
+ case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8:
+ case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII:
+ LOGCHARSETMENU(("TEXT:UnlabeledUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::UnlabeledUtf8);
+ break;
+ case kCharsetFromChannel:
+ if (encoding == UTF_8_ENCODING) {
+ LOGCHARSETMENU(("TEXT:ChannelUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::ChannelUtf8);
+ } else {
+ LOGCHARSETMENU(("TEXT:ChannelNonUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::
+ ChannelNonUtf8);
+ }
+ break;
+ default:
+ LOGCHARSETMENU(("TEXT:Bug"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::Bug);
+ break;
+ }
+ } else {
+ switch (charsetSource) {
+ case kCharsetFromInitialAutoDetectionASCII:
+ // Deliberately no final version
+ LOGCHARSETMENU(("HTML:UnlabeledAscii"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::UnlabeledAscii);
+ break;
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Generic:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Generic:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8GenericInitialWasASCII:
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Content:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Content:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8ContentInitialWasASCII:
+ LOGCHARSETMENU(("HTML:UnlabeledNonUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::
+ UnlabeledNonUtf8);
+ break;
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLDInitialWasASCII:
+ LOGCHARSETMENU(("HTML:UnlabeledNonUtf8TLD"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::
+ UnlabeledNonUtf8TLD);
+ break;
+ case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8:
+ case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII:
+ LOGCHARSETMENU(("HTML:UnlabeledUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::UnlabeledUtf8);
+ break;
+ case kCharsetFromChannel:
+ if (encoding == UTF_8_ENCODING) {
+ LOGCHARSETMENU(("HTML:ChannelUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::ChannelUtf8);
+ } else {
+ LOGCHARSETMENU(("HTML:ChannelNonUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::
+ ChannelNonUtf8);
+ }
+ break;
+ case kCharsetFromXmlDeclaration:
+ case kCharsetFromMetaTag:
+ if (isFileURL) {
+ LOGCHARSETMENU(("HTML:LocalLabeled"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::LocalLabeled);
+ } else if (encoding == UTF_8_ENCODING) {
+ LOGCHARSETMENU(("HTML:MetaUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::InternalUtf8);
+ } else {
+ LOGCHARSETMENU(("HTML:MetaNonUtf8"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::
+ InternalNonUtf8);
+ }
+ break;
+ default:
+ LOGCHARSETMENU(("HTML:Bug"));
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::Bug);
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+void nsDocShell::SetParentCharset(const Encoding*& aCharset,
+ int32_t aCharsetSource,
+ nsIPrincipal* aPrincipal) {
+ mParentCharset = aCharset;
+ mParentCharsetSource = aCharsetSource;
+ mParentCharsetPrincipal = aPrincipal;
+}
+
+void nsDocShell::GetParentCharset(const Encoding*& aCharset,
+ int32_t* aCharsetSource,
+ nsIPrincipal** aPrincipal) {
+ aCharset = mParentCharset;
+ *aCharsetSource = mParentCharsetSource;
+ NS_IF_ADDREF(*aPrincipal = mParentCharsetPrincipal);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetHasTrackingContentBlocked(Promise** aPromise) {
+ MOZ_ASSERT(aPromise);
+
+ ErrorResult rv;
+ RefPtr<Document> doc(GetDocument());
+ RefPtr<Promise> retPromise = Promise::Create(doc->GetOwnerGlobal(), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ // Retrieve the document's content blocking events from the parent process.
+ RefPtr<Document::GetContentBlockingEventsPromise> promise =
+ doc->GetContentBlockingEvents();
+ if (promise) {
+ promise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [retPromise](const Document::GetContentBlockingEventsPromise::
+ ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ bool has = aValue.ResolveValue() &
+ nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT;
+ retPromise->MaybeResolve(has);
+ } else {
+ retPromise->MaybeResolve(false);
+ }
+ });
+ } else {
+ retPromise->MaybeResolve(false);
+ }
+
+ retPromise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCssErrorReportingEnabled(bool* aEnabled) {
+ MOZ_ASSERT(aEnabled);
+ *aEnabled = mCSSErrorReportingEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetCssErrorReportingEnabled(bool aEnabled) {
+ mCSSErrorReportingEnabled = aEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) {
+ NS_ENSURE_ARG_POINTER(aUsePrivateBrowsing);
+ return mBrowsingContext->GetUsePrivateBrowsing(aUsePrivateBrowsing);
+}
+
+void nsDocShell::NotifyPrivateBrowsingChanged() {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mPrivacyObservers);
+ while (iter.HasMore()) {
+ nsWeakPtr ref = iter.GetNext();
+ nsCOMPtr<nsIPrivacyTransitionObserver> obs = do_QueryReferent(ref);
+ if (!obs) {
+ iter.Remove();
+ } else {
+ obs->PrivateModeChanged(UsePrivateBrowsing());
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) {
+ return mBrowsingContext->SetUsePrivateBrowsing(aUsePrivateBrowsing);
+}
+
+NS_IMETHODIMP
+nsDocShell::SetPrivateBrowsing(bool aUsePrivateBrowsing) {
+ return mBrowsingContext->SetPrivateBrowsing(aUsePrivateBrowsing);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetHasLoadedNonBlankURI(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = mHasLoadedNonBlankURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetUseRemoteTabs(bool* aUseRemoteTabs) {
+ NS_ENSURE_ARG_POINTER(aUseRemoteTabs);
+ return mBrowsingContext->GetUseRemoteTabs(aUseRemoteTabs);
+}
+
+NS_IMETHODIMP
+nsDocShell::SetRemoteTabs(bool aUseRemoteTabs) {
+ return mBrowsingContext->SetRemoteTabs(aUseRemoteTabs);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetUseRemoteSubframes(bool* aUseRemoteSubframes) {
+ NS_ENSURE_ARG_POINTER(aUseRemoteSubframes);
+ return mBrowsingContext->GetUseRemoteSubframes(aUseRemoteSubframes);
+}
+
+NS_IMETHODIMP
+nsDocShell::SetRemoteSubframes(bool aUseRemoteSubframes) {
+ return mBrowsingContext->SetRemoteSubframes(aUseRemoteSubframes);
+}
+
+NS_IMETHODIMP
+nsDocShell::AddWeakPrivacyTransitionObserver(
+ nsIPrivacyTransitionObserver* aObserver) {
+ nsWeakPtr weakObs = do_GetWeakReference(aObserver);
+ if (!weakObs) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mPrivacyObservers.AppendElement(weakObs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::AddWeakReflowObserver(nsIReflowObserver* aObserver) {
+ nsWeakPtr weakObs = do_GetWeakReference(aObserver);
+ if (!weakObs) {
+ return NS_ERROR_FAILURE;
+ }
+ mReflowObservers.AppendElement(weakObs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::RemoveWeakReflowObserver(nsIReflowObserver* aObserver) {
+ nsWeakPtr obs = do_GetWeakReference(aObserver);
+ return mReflowObservers.RemoveElement(obs) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDocShell::NotifyReflowObservers(bool aInterruptible,
+ DOMHighResTimeStamp aStart,
+ DOMHighResTimeStamp aEnd) {
+ nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mReflowObservers);
+ while (iter.HasMore()) {
+ nsWeakPtr ref = iter.GetNext();
+ nsCOMPtr<nsIReflowObserver> obs = do_QueryReferent(ref);
+ if (!obs) {
+ iter.Remove();
+ } else if (aInterruptible) {
+ obs->ReflowInterruptible(aStart, aEnd);
+ } else {
+ obs->Reflow(aStart, aEnd);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowMetaRedirects(bool* aReturn) {
+ NS_ENSURE_ARG_POINTER(aReturn);
+
+ *aReturn = mAllowMetaRedirects;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowMetaRedirects(bool aValue) {
+ mAllowMetaRedirects = aValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowSubframes(bool* aAllowSubframes) {
+ NS_ENSURE_ARG_POINTER(aAllowSubframes);
+
+ *aAllowSubframes = mAllowSubframes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowSubframes(bool aAllowSubframes) {
+ mAllowSubframes = aAllowSubframes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowImages(bool* aAllowImages) {
+ NS_ENSURE_ARG_POINTER(aAllowImages);
+
+ *aAllowImages = mAllowImages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowImages(bool aAllowImages) {
+ mAllowImages = aAllowImages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowMedia(bool* aAllowMedia) {
+ *aAllowMedia = mAllowMedia;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowMedia(bool aAllowMedia) {
+ mAllowMedia = aAllowMedia;
+
+ // Mute or unmute audio contexts attached to the inner window.
+ if (mScriptGlobal) {
+ if (nsPIDOMWindowInner* innerWin = mScriptGlobal->GetCurrentInnerWindow()) {
+ if (aAllowMedia) {
+ innerWin->UnmuteAudioContexts();
+ } else {
+ innerWin->MuteAudioContexts();
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowDNSPrefetch(bool* aAllowDNSPrefetch) {
+ *aAllowDNSPrefetch = mAllowDNSPrefetch;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowDNSPrefetch(bool aAllowDNSPrefetch) {
+ mAllowDNSPrefetch = aAllowDNSPrefetch;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowWindowControl(bool* aAllowWindowControl) {
+ *aAllowWindowControl = mAllowWindowControl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowWindowControl(bool aAllowWindowControl) {
+ mAllowWindowControl = aAllowWindowControl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowContentRetargeting(bool* aAllowContentRetargeting) {
+ *aAllowContentRetargeting = mBrowsingContext->GetAllowContentRetargeting();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowContentRetargeting(bool aAllowContentRetargeting) {
+ BrowsingContext::Transaction txn;
+ txn.SetAllowContentRetargeting(aAllowContentRetargeting);
+ txn.SetAllowContentRetargetingOnChildren(aAllowContentRetargeting);
+ return txn.Commit(mBrowsingContext);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowContentRetargetingOnChildren(
+ bool* aAllowContentRetargetingOnChildren) {
+ *aAllowContentRetargetingOnChildren =
+ mBrowsingContext->GetAllowContentRetargetingOnChildren();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowContentRetargetingOnChildren(
+ bool aAllowContentRetargetingOnChildren) {
+ return mBrowsingContext->SetAllowContentRetargetingOnChildren(
+ aAllowContentRetargetingOnChildren);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetMayEnableCharacterEncodingMenu(
+ bool* aMayEnableCharacterEncodingMenu) {
+ *aMayEnableCharacterEncodingMenu = false;
+ if (!mDocumentViewer) {
+ return NS_OK;
+ }
+ Document* doc = mDocumentViewer->GetDocument();
+ if (!doc) {
+ return NS_OK;
+ }
+ if (doc->WillIgnoreCharsetOverride()) {
+ return NS_OK;
+ }
+
+ *aMayEnableCharacterEncodingMenu = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllDocShellsInSubtree(int32_t aItemType,
+ DocShellEnumeratorDirection aDirection,
+ nsTArray<RefPtr<nsIDocShell>>& aResult) {
+ aResult.Clear();
+
+ nsDocShellEnumerator docShellEnum(
+ (aDirection == ENUMERATE_FORWARDS)
+ ? nsDocShellEnumerator::EnumerationDirection::Forwards
+ : nsDocShellEnumerator::EnumerationDirection::Backwards,
+ aItemType, *this);
+
+ nsresult rv = docShellEnum.BuildDocShellArray(aResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAppType(AppType* aAppType) {
+ *aAppType = mAppType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAppType(AppType aAppType) {
+ mAppType = aAppType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAllowAuth(bool* aAllowAuth) {
+ *aAllowAuth = mAllowAuth;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetAllowAuth(bool aAllowAuth) {
+ mAllowAuth = aAllowAuth;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetZoom(float* aZoom) {
+ NS_ENSURE_ARG_POINTER(aZoom);
+ *aZoom = 1.0f;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetZoom(float aZoom) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+nsDocShell::GetBusyFlags(BusyFlags* aBusyFlags) {
+ NS_ENSURE_ARG_POINTER(aBusyFlags);
+
+ *aBusyFlags = mBusyFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::TabToTreeOwner(bool aForward, bool aForDocumentNavigation,
+ bool* aTookFocus) {
+ NS_ENSURE_ARG_POINTER(aTookFocus);
+
+ nsCOMPtr<nsIWebBrowserChromeFocus> chromeFocus = do_GetInterface(mTreeOwner);
+ if (chromeFocus) {
+ if (aForward) {
+ *aTookFocus =
+ NS_SUCCEEDED(chromeFocus->FocusNextElement(aForDocumentNavigation));
+ } else {
+ *aTookFocus =
+ NS_SUCCEEDED(chromeFocus->FocusPrevElement(aForDocumentNavigation));
+ }
+ } else {
+ *aTookFocus = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetLoadURIDelegate(nsILoadURIDelegate** aLoadURIDelegate) {
+ nsCOMPtr<nsILoadURIDelegate> delegate = GetLoadURIDelegate();
+ delegate.forget(aLoadURIDelegate);
+ return NS_OK;
+}
+
+already_AddRefed<nsILoadURIDelegate> nsDocShell::GetLoadURIDelegate() {
+ if (nsCOMPtr<nsILoadURIDelegate> result =
+ do_QueryActor("LoadURIDelegate", GetDocument())) {
+ return result.forget();
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetUseErrorPages(bool* aUseErrorPages) {
+ *aUseErrorPages = mBrowsingContext->GetUseErrorPages();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetUseErrorPages(bool aUseErrorPages) {
+ return mBrowsingContext->SetUseErrorPages(aUseErrorPages);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetPreviousEntryIndex(int32_t* aPreviousEntryIndex) {
+ *aPreviousEntryIndex = mPreviousEntryIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetLoadedEntryIndex(int32_t* aLoadedEntryIndex) {
+ *aLoadedEntryIndex = mLoadedEntryIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::HistoryPurged(int32_t aNumEntries) {
+ // These indices are used for fastback cache eviction, to determine
+ // which session history entries are candidates for content viewer
+ // eviction. We need to adjust by the number of entries that we
+ // just purged from history, so that we look at the right session history
+ // entries during eviction.
+ mPreviousEntryIndex = std::max(-1, mPreviousEntryIndex - aNumEntries);
+ mLoadedEntryIndex = std::max(0, mLoadedEntryIndex - aNumEntries);
+
+ for (auto* child : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> shell = do_QueryObject(child);
+ if (shell) {
+ shell->HistoryPurged(aNumEntries);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::HistoryEntryRemoved(int32_t aIndex) {
+ // These indices are used for fastback cache eviction, to determine
+ // which session history entries are candidates for content viewer
+ // eviction. We need to adjust by the number of entries that we
+ // just purged from history, so that we look at the right session history
+ // entries during eviction.
+ if (aIndex == mPreviousEntryIndex) {
+ mPreviousEntryIndex = -1;
+ } else if (aIndex < mPreviousEntryIndex) {
+ --mPreviousEntryIndex;
+ }
+ if (mLoadedEntryIndex == aIndex) {
+ mLoadedEntryIndex = 0;
+ } else if (aIndex < mLoadedEntryIndex) {
+ --mLoadedEntryIndex;
+ }
+
+ for (auto* child : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> shell = do_QueryObject(child);
+ if (shell) {
+ static_cast<nsDocShell*>(shell.get())->HistoryEntryRemoved(aIndex);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::Now(DOMHighResTimeStamp* aWhen) {
+ *aWhen = (TimeStamp::Now() - TimeStamp::ProcessCreation()).ToMilliseconds();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetWindowDraggingAllowed(bool aValue) {
+ RefPtr<nsDocShell> parent = GetInProcessParentDocshell();
+ if (!aValue && mItemType == typeChrome && !parent) {
+ // Window dragging is always allowed for top level
+ // chrome docshells.
+ return NS_ERROR_FAILURE;
+ }
+ mWindowDraggingAllowed = aValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetWindowDraggingAllowed(bool* aValue) {
+ // window dragging regions in CSS (-moz-window-drag:drag)
+ // can be slow. Default behavior is to only allow it for
+ // chrome top level windows.
+ RefPtr<nsDocShell> parent = GetInProcessParentDocshell();
+ if (mItemType == typeChrome && !parent) {
+ // Top level chrome window
+ *aValue = true;
+ } else {
+ *aValue = mWindowDraggingAllowed;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCurrentDocumentChannel(nsIChannel** aResult) {
+ NS_IF_ADDREF(*aResult = GetCurrentDocChannel());
+ return NS_OK;
+}
+
+nsIChannel* nsDocShell::GetCurrentDocChannel() {
+ if (mDocumentViewer) {
+ Document* doc = mDocumentViewer->GetDocument();
+ if (doc) {
+ return doc->GetChannel();
+ }
+ }
+ return nullptr;
+}
+
+NS_IMETHODIMP
+nsDocShell::AddWeakScrollObserver(nsIScrollObserver* aObserver) {
+ nsWeakPtr weakObs = do_GetWeakReference(aObserver);
+ if (!weakObs) {
+ return NS_ERROR_FAILURE;
+ }
+ mScrollObservers.AppendElement(weakObs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::RemoveWeakScrollObserver(nsIScrollObserver* aObserver) {
+ nsWeakPtr obs = do_GetWeakReference(aObserver);
+ return mScrollObservers.RemoveElement(obs) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+void nsDocShell::NotifyAsyncPanZoomStarted() {
+ nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mScrollObservers);
+ while (iter.HasMore()) {
+ nsWeakPtr ref = iter.GetNext();
+ nsCOMPtr<nsIScrollObserver> obs = do_QueryReferent(ref);
+ if (obs) {
+ obs->AsyncPanZoomStarted();
+ } else {
+ iter.Remove();
+ }
+ }
+}
+
+void nsDocShell::NotifyAsyncPanZoomStopped() {
+ nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mScrollObservers);
+ while (iter.HasMore()) {
+ nsWeakPtr ref = iter.GetNext();
+ nsCOMPtr<nsIScrollObserver> obs = do_QueryReferent(ref);
+ if (obs) {
+ obs->AsyncPanZoomStopped();
+ } else {
+ iter.Remove();
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::NotifyScrollObservers() {
+ nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mScrollObservers);
+ while (iter.HasMore()) {
+ nsWeakPtr ref = iter.GetNext();
+ nsCOMPtr<nsIScrollObserver> obs = do_QueryReferent(ref);
+ if (obs) {
+ obs->ScrollPositionChanged();
+ } else {
+ iter.Remove();
+ }
+ }
+ return NS_OK;
+}
+
+//*****************************************************************************
+// nsDocShell::nsIDocShellTreeItem
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::GetName(nsAString& aName) {
+ aName = mBrowsingContext->Name();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetName(const nsAString& aName) {
+ return mBrowsingContext->SetName(aName);
+}
+
+NS_IMETHODIMP
+nsDocShell::NameEquals(const nsAString& aName, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mBrowsingContext->NameEquals(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCustomUserAgent(nsAString& aCustomUserAgent) {
+ mBrowsingContext->GetCustomUserAgent(aCustomUserAgent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetCustomUserAgent(const nsAString& aCustomUserAgent) {
+ if (mWillChangeProcess) {
+ NS_WARNING("SetCustomUserAgent: Process is changing. Ignoring set");
+ return NS_ERROR_FAILURE;
+ }
+
+ return mBrowsingContext->SetCustomUserAgent(aCustomUserAgent);
+}
+
+NS_IMETHODIMP
+nsDocShell::ClearCachedPlatform() {
+ nsCOMPtr<nsPIDOMWindowInner> win =
+ mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : nullptr;
+ if (win) {
+ Navigator* navigator = win->Navigator();
+ if (navigator) {
+ navigator->ClearPlatformCache();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::ClearCachedUserAgent() {
+ nsCOMPtr<nsPIDOMWindowInner> win =
+ mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : nullptr;
+ if (win) {
+ Navigator* navigator = win->Navigator();
+ if (navigator) {
+ navigator->ClearUserAgentCache();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetMetaViewportOverride(
+ MetaViewportOverride* aMetaViewportOverride) {
+ NS_ENSURE_ARG_POINTER(aMetaViewportOverride);
+
+ *aMetaViewportOverride = mMetaViewportOverride;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetMetaViewportOverride(
+ MetaViewportOverride aMetaViewportOverride) {
+ // We don't have a way to verify this coming from Javascript, so this check is
+ // still needed.
+ if (!(aMetaViewportOverride == META_VIEWPORT_OVERRIDE_NONE ||
+ aMetaViewportOverride == META_VIEWPORT_OVERRIDE_ENABLED ||
+ aMetaViewportOverride == META_VIEWPORT_OVERRIDE_DISABLED)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mMetaViewportOverride = aMetaViewportOverride;
+
+ // Inform our presShell that it needs to re-check its need for a viewport
+ // override.
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->MaybeRecreateMobileViewportManager(true);
+ }
+
+ return NS_OK;
+}
+
+/* virtual */
+int32_t nsDocShell::ItemType() { return mItemType; }
+
+NS_IMETHODIMP
+nsDocShell::GetItemType(int32_t* aItemType) {
+ NS_ENSURE_ARG_POINTER(aItemType);
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ (mBrowsingContext->IsContent() ? typeContent : typeChrome) == mItemType);
+ *aItemType = mItemType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetInProcessParent(nsIDocShellTreeItem** aParent) {
+ if (!mParent) {
+ *aParent = nullptr;
+ } else {
+ CallQueryInterface(mParent, aParent);
+ }
+ // Note that in the case when the parent is not an nsIDocShellTreeItem we
+ // don't want to throw; we just want to return null.
+ return NS_OK;
+}
+
+// With Fission, related nsDocShell objects may exist in a different process. In
+// that case, this method will return `nullptr`, despite a parent nsDocShell
+// object existing.
+//
+// Prefer using `BrowsingContext::Parent()`, which will succeed even if the
+// parent entry is not in the current process, and handle the case where the
+// parent nsDocShell is inaccessible.
+already_AddRefed<nsDocShell> nsDocShell::GetInProcessParentDocshell() {
+ nsCOMPtr<nsIDocShell> docshell = do_QueryInterface(GetAsSupports(mParent));
+ return docshell.forget().downcast<nsDocShell>();
+}
+
+void nsDocShell::MaybeCreateInitialClientSource(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ // If there is an existing document then there is no need to create
+ // a client for a future initial about:blank document.
+ if (mScriptGlobal && mScriptGlobal->GetCurrentInnerWindow() &&
+ mScriptGlobal->GetCurrentInnerWindow()->GetExtantDoc()) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mScriptGlobal->GetCurrentInnerWindow()->GetClientInfo().isSome());
+ MOZ_DIAGNOSTIC_ASSERT(!mInitialClientSource);
+ return;
+ }
+
+ // Don't recreate the initial client source. We call this multiple times
+ // when DoChannelLoad() is called before CreateAboutBlankDocumentViewer.
+ if (mInitialClientSource) {
+ return;
+ }
+
+ // Don't pre-allocate the client when we are sandboxed. The inherited
+ // principal does not take sandboxing into account.
+ // TODO: Refactor sandboxing principal code out so we can use it here.
+ if (!aPrincipal && mBrowsingContext->GetSandboxFlags()) {
+ return;
+ }
+
+ // We cannot get inherited foreign partitioned principal here. Instead, we
+ // directly check which principal we want to inherit for the service worker.
+ nsIPrincipal* principal =
+ aPrincipal
+ ? aPrincipal
+ : GetInheritedPrincipal(
+ false, StoragePrincipalHelper::
+ ShouldUsePartitionPrincipalForServiceWorker(this));
+
+ // Sometimes there is no principal available when we are called from
+ // CreateAboutBlankDocumentViewer. For example, sometimes the principal
+ // is only extracted from the load context after the document is created
+ // in Document::ResetToURI(). Ideally we would do something similar
+ // here, but for now lets just avoid the issue by not preallocating the
+ // client.
+ if (!principal) {
+ return;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow();
+ if (!win) {
+ return;
+ }
+
+ mInitialClientSource = ClientManager::CreateSource(
+ ClientType::Window, GetMainThreadSerialEventTarget(), principal);
+ MOZ_DIAGNOSTIC_ASSERT(mInitialClientSource);
+
+ // Mark the initial client as execution ready, but owned by the docshell.
+ // If the client is actually used this will cause ClientSource to force
+ // the creation of the initial about:blank by calling
+ // nsDocShell::GetDocument().
+ mInitialClientSource->DocShellExecutionReady(this);
+
+ // Next, check to see if the parent is controlled.
+ nsCOMPtr<nsIDocShell> parent = GetInProcessParentDocshell();
+ nsPIDOMWindowOuter* parentOuter = parent ? parent->GetWindow() : nullptr;
+ nsPIDOMWindowInner* parentInner =
+ parentOuter ? parentOuter->GetCurrentInnerWindow() : nullptr;
+ if (!parentInner) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "about:blank"_ns));
+
+ // We're done if there is no parent controller or if this docshell
+ // is not permitted to control for some reason.
+ Maybe<ServiceWorkerDescriptor> controller(parentInner->GetController());
+ if (controller.isNothing() ||
+ !ServiceWorkerAllowedToControlWindow(principal, uri)) {
+ return;
+ }
+
+ mInitialClientSource->InheritController(controller.ref());
+}
+
+Maybe<ClientInfo> nsDocShell::GetInitialClientInfo() const {
+ if (mInitialClientSource) {
+ Maybe<ClientInfo> result;
+ result.emplace(mInitialClientSource->Info());
+ return result;
+ }
+
+ nsPIDOMWindowInner* innerWindow =
+ mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : nullptr;
+ Document* doc = innerWindow ? innerWindow->GetExtantDoc() : nullptr;
+
+ if (!doc || !doc->IsInitialDocument()) {
+ return Maybe<ClientInfo>();
+ }
+
+ return innerWindow->GetClientInfo();
+}
+
+nsresult nsDocShell::SetDocLoaderParent(nsDocLoader* aParent) {
+ bool wasFrame = IsSubframe();
+
+ nsresult rv = nsDocLoader::SetDocLoaderParent(aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsPriority> priorityGroup = do_QueryInterface(mLoadGroup);
+ if (wasFrame != IsSubframe() && priorityGroup) {
+ priorityGroup->AdjustPriority(wasFrame ? -1 : 1);
+ }
+
+ // Curse ambiguous nsISupports inheritance!
+ nsISupports* parent = GetAsSupports(aParent);
+
+ // If parent is another docshell, we inherit all their flags for
+ // allowing plugins, scripting etc.
+ bool value;
+ nsCOMPtr<nsIDocShell> parentAsDocShell(do_QueryInterface(parent));
+
+ if (parentAsDocShell) {
+ if (mAllowMetaRedirects &&
+ NS_SUCCEEDED(parentAsDocShell->GetAllowMetaRedirects(&value))) {
+ SetAllowMetaRedirects(value);
+ }
+ if (mAllowSubframes &&
+ NS_SUCCEEDED(parentAsDocShell->GetAllowSubframes(&value))) {
+ SetAllowSubframes(value);
+ }
+ if (mAllowImages &&
+ NS_SUCCEEDED(parentAsDocShell->GetAllowImages(&value))) {
+ SetAllowImages(value);
+ }
+ SetAllowMedia(parentAsDocShell->GetAllowMedia() && mAllowMedia);
+ if (mAllowWindowControl &&
+ NS_SUCCEEDED(parentAsDocShell->GetAllowWindowControl(&value))) {
+ SetAllowWindowControl(value);
+ }
+ if (NS_FAILED(parentAsDocShell->GetAllowDNSPrefetch(&value))) {
+ value = false;
+ }
+ SetAllowDNSPrefetch(mAllowDNSPrefetch && value);
+
+ // We don't need to inherit metaViewportOverride, because the viewport
+ // is only relevant for the outermost nsDocShell, not for any iframes
+ // like this that might be embedded within it.
+ }
+
+ nsCOMPtr<nsIURIContentListener> parentURIListener(do_GetInterface(parent));
+ if (parentURIListener) {
+ mContentListener->SetParentContentListener(parentURIListener);
+ }
+
+ return NS_OK;
+}
+
+void nsDocShell::MaybeRestoreWindowName() {
+ if (!StaticPrefs::privacy_window_name_update_enabled()) {
+ return;
+ }
+
+ // We only restore window.name for the top-level content.
+ if (!mBrowsingContext->IsTopContent()) {
+ return;
+ }
+
+ nsAutoString name;
+
+ // Following implements https://html.spec.whatwg.org/#history-traversal:
+ // Step 4.4. Check if the loading entry has a name.
+
+ if (mLSHE) {
+ mLSHE->GetName(name);
+ }
+
+ if (mLoadingEntry) {
+ name = mLoadingEntry->mInfo.GetName();
+ }
+
+ if (name.IsEmpty()) {
+ return;
+ }
+
+ // Step 4.4.1. Set the name to the browsing context.
+ Unused << mBrowsingContext->SetName(name);
+
+ // Step 4.4.2. Clear the name of all entries that are contiguous and
+ // same-origin with the loading entry.
+ if (mLSHE) {
+ nsSHistory::WalkContiguousEntries(
+ mLSHE, [](nsISHEntry* aEntry) { aEntry->SetName(EmptyString()); });
+ }
+
+ if (mLoadingEntry) {
+ // Clear the name of the session entry in the child side. For parent side,
+ // the clearing will be done when we commit the history to the parent.
+ mLoadingEntry->mInfo.SetName(EmptyString());
+ }
+}
+
+void nsDocShell::StoreWindowNameToSHEntries() {
+ MOZ_ASSERT(mBrowsingContext->IsTopContent());
+
+ nsAutoString name;
+ mBrowsingContext->GetName(name);
+
+ if (mOSHE) {
+ nsSHistory::WalkContiguousEntries(
+ mOSHE, [&](nsISHEntry* aEntry) { aEntry->SetName(name); });
+ }
+
+ if (mozilla::SessionHistoryInParent()) {
+ if (XRE_IsParentProcess()) {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ nsSHistory::WalkContiguousEntries(
+ entry, [&](nsISHEntry* aEntry) { aEntry->SetName(name); });
+ }
+ } else {
+ // Ask parent process to store the name in entries.
+ mozilla::Unused
+ << ContentChild::GetSingleton()
+ ->SendSessionHistoryEntryStoreWindowNameInContiguousEntries(
+ mBrowsingContext, name);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::GetInProcessSameTypeParent(nsIDocShellTreeItem** aParent) {
+ if (BrowsingContext* parentBC = mBrowsingContext->GetParent()) {
+ *aParent = do_AddRef(parentBC->GetDocShell()).take();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetInProcessRootTreeItem(nsIDocShellTreeItem** aRootTreeItem) {
+ NS_ENSURE_ARG_POINTER(aRootTreeItem);
+
+ RefPtr<nsDocShell> root = this;
+ RefPtr<nsDocShell> parent = root->GetInProcessParentDocshell();
+ while (parent) {
+ root = parent;
+ parent = root->GetInProcessParentDocshell();
+ }
+
+ root.forget(aRootTreeItem);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetInProcessSameTypeRootTreeItem(
+ nsIDocShellTreeItem** aRootTreeItem) {
+ NS_ENSURE_ARG_POINTER(aRootTreeItem);
+ *aRootTreeItem = static_cast<nsIDocShellTreeItem*>(this);
+
+ nsCOMPtr<nsIDocShellTreeItem> parent;
+ NS_ENSURE_SUCCESS(GetInProcessSameTypeParent(getter_AddRefs(parent)),
+ NS_ERROR_FAILURE);
+ while (parent) {
+ *aRootTreeItem = parent;
+ NS_ENSURE_SUCCESS(
+ (*aRootTreeItem)->GetInProcessSameTypeParent(getter_AddRefs(parent)),
+ NS_ERROR_FAILURE);
+ }
+ NS_ADDREF(*aRootTreeItem);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetTreeOwner(nsIDocShellTreeOwner** aTreeOwner) {
+ NS_ENSURE_ARG_POINTER(aTreeOwner);
+
+ *aTreeOwner = mTreeOwner;
+ NS_IF_ADDREF(*aTreeOwner);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner) {
+ if (mIsBeingDestroyed && aTreeOwner) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Don't automatically set the progress based on the tree owner for frames
+ if (!IsSubframe()) {
+ nsCOMPtr<nsIWebProgress> webProgress =
+ do_QueryInterface(GetAsSupports(this));
+
+ if (webProgress) {
+ nsCOMPtr<nsIWebProgressListener> oldListener =
+ do_QueryInterface(mTreeOwner);
+ nsCOMPtr<nsIWebProgressListener> newListener =
+ do_QueryInterface(aTreeOwner);
+
+ if (oldListener) {
+ webProgress->RemoveProgressListener(oldListener);
+ }
+
+ if (newListener) {
+ webProgress->AddProgressListener(newListener,
+ nsIWebProgress::NOTIFY_ALL);
+ }
+ }
+ }
+
+ mTreeOwner = aTreeOwner; // Weak reference per API
+
+ for (auto* childDocLoader : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShellTreeItem> child = do_QueryObject(childDocLoader);
+ NS_ENSURE_TRUE(child, NS_ERROR_FAILURE);
+
+ if (child->ItemType() == mItemType) {
+ child->SetTreeOwner(aTreeOwner);
+ }
+ }
+
+ // If we're in the content process and have had a TreeOwner set on us, extract
+ // our BrowserChild actor. If we've already had our BrowserChild set, assert
+ // that it hasn't changed.
+ if (mTreeOwner && XRE_IsContentProcess()) {
+ nsCOMPtr<nsIBrowserChild> newBrowserChild = do_GetInterface(mTreeOwner);
+ MOZ_ASSERT(newBrowserChild,
+ "No BrowserChild actor for tree owner in Content!");
+
+ if (mBrowserChild) {
+ nsCOMPtr<nsIBrowserChild> oldBrowserChild =
+ do_QueryReferent(mBrowserChild);
+ MOZ_RELEASE_ASSERT(
+ oldBrowserChild == newBrowserChild,
+ "Cannot change BrowserChild during nsDocShell lifetime!");
+ } else {
+ mBrowserChild = do_GetWeakReference(newBrowserChild);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetHistoryID(nsID& aID) {
+ aID = mBrowsingContext->GetHistoryID();
+ return NS_OK;
+}
+
+const nsID& nsDocShell::HistoryID() { return mBrowsingContext->GetHistoryID(); }
+
+NS_IMETHODIMP
+nsDocShell::GetIsInUnload(bool* aIsInUnload) {
+ *aIsInUnload = mFiredUnloadEvent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetInProcessChildCount(int32_t* aChildCount) {
+ NS_ENSURE_ARG_POINTER(aChildCount);
+ *aChildCount = mChildList.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::AddChild(nsIDocShellTreeItem* aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+
+ RefPtr<nsDocLoader> childAsDocLoader = GetAsDocLoader(aChild);
+ NS_ENSURE_TRUE(childAsDocLoader, NS_ERROR_UNEXPECTED);
+
+ // Make sure we're not creating a loop in the docshell tree
+ nsDocLoader* ancestor = this;
+ do {
+ if (childAsDocLoader == ancestor) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ ancestor = ancestor->GetParent();
+ } while (ancestor);
+
+ // Make sure to remove the child from its current parent.
+ nsDocLoader* childsParent = childAsDocLoader->GetParent();
+ if (childsParent) {
+ nsresult rv = childsParent->RemoveChildLoader(childAsDocLoader);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Make sure to clear the treeowner in case this child is a different type
+ // from us.
+ aChild->SetTreeOwner(nullptr);
+
+ nsresult res = AddChildLoader(childAsDocLoader);
+ NS_ENSURE_SUCCESS(res, res);
+ NS_ASSERTION(!mChildList.IsEmpty(),
+ "child list must not be empty after a successful add");
+
+ /* Set the child's global history if the parent has one */
+ if (mBrowsingContext->GetUseGlobalHistory()) {
+ // childDocShell->SetUseGlobalHistory(true);
+ // this should be set through BC inherit
+ MOZ_ASSERT(aChild->GetBrowsingContext()->GetUseGlobalHistory());
+ }
+
+ if (aChild->ItemType() != mItemType) {
+ return NS_OK;
+ }
+
+ aChild->SetTreeOwner(mTreeOwner);
+
+ nsCOMPtr<nsIDocShell> childAsDocShell(do_QueryInterface(aChild));
+ if (!childAsDocShell) {
+ return NS_OK;
+ }
+
+ // charset, style-disabling, and zoom will be inherited in SetupNewViewer()
+
+ // Now take this document's charset and set the child's parentCharset field
+ // to it. We'll later use that field, in the loading process, for the
+ // charset choosing algorithm.
+ // If we fail, at any point, we just return NS_OK.
+ // This code has some performance impact. But this will be reduced when
+ // the current charset will finally be stored as an Atom, avoiding the
+ // alias resolution extra look-up.
+
+ // we are NOT going to propagate the charset is this Chrome's docshell
+ if (mItemType == nsIDocShellTreeItem::typeChrome) {
+ return NS_OK;
+ }
+
+ // get the parent's current charset
+ if (!mDocumentViewer) {
+ return NS_OK;
+ }
+ Document* doc = mDocumentViewer->GetDocument();
+ if (!doc) {
+ return NS_OK;
+ }
+
+ const Encoding* parentCS = doc->GetDocumentCharacterSet();
+ int32_t charsetSource = doc->GetDocumentCharacterSetSource();
+ // set the child's parentCharset
+ childAsDocShell->SetParentCharset(parentCS, charsetSource,
+ doc->NodePrincipal());
+
+ // printf("### 1 >>> Adding child. Parent CS = %s. ItemType = %d.\n",
+ // NS_LossyConvertUTF16toASCII(parentCS).get(), mItemType);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::RemoveChild(nsIDocShellTreeItem* aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+
+ RefPtr<nsDocLoader> childAsDocLoader = GetAsDocLoader(aChild);
+ NS_ENSURE_TRUE(childAsDocLoader, NS_ERROR_UNEXPECTED);
+
+ nsresult rv = RemoveChildLoader(childAsDocLoader);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aChild->SetTreeOwner(nullptr);
+
+ return nsDocLoader::AddDocLoaderAsChildOfRoot(childAsDocLoader);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetInProcessChildAt(int32_t aIndex, nsIDocShellTreeItem** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+
+ RefPtr<nsDocShell> child = GetInProcessChildAt(aIndex);
+ NS_ENSURE_TRUE(child, NS_ERROR_UNEXPECTED);
+
+ child.forget(aChild);
+
+ return NS_OK;
+}
+
+nsDocShell* nsDocShell::GetInProcessChildAt(int32_t aIndex) {
+#ifdef DEBUG
+ if (aIndex < 0) {
+ NS_WARNING("Negative index passed to GetChildAt");
+ } else if (static_cast<uint32_t>(aIndex) >= mChildList.Length()) {
+ NS_WARNING("Too large an index passed to GetChildAt");
+ }
+#endif
+
+ nsIDocumentLoader* child = ChildAt(aIndex);
+
+ // child may be nullptr here.
+ return static_cast<nsDocShell*>(child);
+}
+
+nsresult nsDocShell::AddChildSHEntry(nsISHEntry* aCloneRef,
+ nsISHEntry* aNewEntry,
+ int32_t aChildOffset, uint32_t aLoadType,
+ bool aCloneChildren) {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+ nsresult rv = NS_OK;
+
+ if (mLSHE && aLoadType != LOAD_PUSHSTATE) {
+ /* You get here if you are currently building a
+ * hierarchy ie.,you just visited a frameset page
+ */
+ if (NS_FAILED(mLSHE->ReplaceChild(aNewEntry))) {
+ rv = mLSHE->AddChild(aNewEntry, aChildOffset);
+ }
+ } else if (!aCloneRef) {
+ /* This is an initial load in some subframe. Just append it if we can */
+ if (mOSHE) {
+ rv = mOSHE->AddChild(aNewEntry, aChildOffset, UseRemoteSubframes());
+ }
+ } else {
+ RefPtr<ChildSHistory> shistory = GetRootSessionHistory();
+ if (shistory) {
+ rv = shistory->LegacySHistory()->AddChildSHEntryHelper(
+ aCloneRef, aNewEntry, mBrowsingContext->Top(), aCloneChildren);
+ }
+ }
+ return rv;
+}
+
+nsresult nsDocShell::AddChildSHEntryToParent(nsISHEntry* aNewEntry,
+ int32_t aChildOffset,
+ bool aCloneChildren) {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+ /* You will get here when you are in a subframe and
+ * a new url has been loaded on you.
+ * The mOSHE in this subframe will be the previous url's
+ * mOSHE. This mOSHE will be used as the identification
+ * for this subframe in the CloneAndReplace function.
+ */
+
+ // In this case, we will end up calling AddEntry, which increases the
+ // current index by 1
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (rootSH) {
+ mPreviousEntryIndex = rootSH->Index();
+ }
+
+ nsresult rv;
+ // XXX(farre): this is not Fission safe, expect errors. This never
+ // get's executed once session history in the parent is enabled.
+ nsCOMPtr<nsIDocShell> parent = do_QueryInterface(GetAsSupports(mParent), &rv);
+ NS_WARNING_ASSERTION(
+ parent || !UseRemoteSubframes(),
+ "Failed to add child session history entry! This will be resolved once "
+ "session history in the parent is enabled.");
+ if (parent) {
+ rv = nsDocShell::Cast(parent)->AddChildSHEntry(
+ mOSHE, aNewEntry, aChildOffset, mLoadType, aCloneChildren);
+ }
+
+ if (rootSH) {
+ mLoadedEntryIndex = rootSH->Index();
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Verbose))) {
+ MOZ_LOG(gPageCacheLog, LogLevel::Verbose,
+ ("Previous index: %d, Loaded index: %d", mPreviousEntryIndex,
+ mLoadedEntryIndex));
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCurrentSHEntry(nsISHEntry** aEntry, bool* aOSHE) {
+ *aOSHE = false;
+ *aEntry = nullptr;
+ if (mLSHE) {
+ NS_ADDREF(*aEntry = mLSHE);
+ } else if (mOSHE) {
+ NS_ADDREF(*aEntry = mOSHE);
+ *aOSHE = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDocShell::SynchronizeLayoutHistoryState() {
+ if (mActiveEntry && mActiveEntry->GetLayoutHistoryState() &&
+ mBrowsingContext) {
+ if (XRE_IsContentProcess()) {
+ dom::ContentChild* contentChild = dom::ContentChild::GetSingleton();
+ if (contentChild) {
+ contentChild->SendSynchronizeLayoutHistoryState(
+ mBrowsingContext, mActiveEntry->GetLayoutHistoryState());
+ }
+ } else {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ entry->SetLayoutHistoryState(mActiveEntry->GetLayoutHistoryState());
+ }
+ }
+ if (mLoadingEntry &&
+ mLoadingEntry->mInfo.SharedId() == mActiveEntry->SharedId()) {
+ mLoadingEntry->mInfo.SetLayoutHistoryState(
+ mActiveEntry->GetLayoutHistoryState());
+ }
+ }
+
+ return NS_OK;
+}
+
+void nsDocShell::SetLoadGroupDefaultLoadFlags(nsLoadFlags aLoadFlags) {
+ if (mLoadGroup) {
+ mLoadGroup->SetDefaultLoadFlags(aLoadFlags);
+ } else {
+ NS_WARNING(
+ "nsDocShell::SetLoadGroupDefaultLoadFlags has no loadGroup to "
+ "propagate the mode to");
+ }
+}
+
+nsIScriptGlobalObject* nsDocShell::GetScriptGlobalObject() {
+ NS_ENSURE_SUCCESS(EnsureScriptEnvironment(), nullptr);
+ return mScriptGlobal;
+}
+
+Document* nsDocShell::GetDocument() {
+ NS_ENSURE_SUCCESS(EnsureDocumentViewer(), nullptr);
+ return mDocumentViewer->GetDocument();
+}
+
+Document* nsDocShell::GetExtantDocument() {
+ return mDocumentViewer ? mDocumentViewer->GetDocument() : nullptr;
+}
+
+nsPIDOMWindowOuter* nsDocShell::GetWindow() {
+ if (NS_FAILED(EnsureScriptEnvironment())) {
+ return nullptr;
+ }
+ return mScriptGlobal;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDomWindow(mozIDOMWindowProxy** aWindow) {
+ NS_ENSURE_ARG_POINTER(aWindow);
+
+ nsresult rv = EnsureScriptEnvironment();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsGlobalWindowOuter> window = mScriptGlobal;
+ window.forget(aWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) {
+ RefPtr<ContentFrameMessageManager> mm;
+ if (RefPtr<BrowserChild> browserChild = BrowserChild::GetFrom(this)) {
+ mm = browserChild->GetMessageManager();
+ } else if (nsPIDOMWindowOuter* win = GetWindow()) {
+ mm = win->GetMessageManager();
+ }
+ mm.forget(aMessageManager);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetIsNavigating(bool* aOut) {
+ *aOut = mIsNavigating;
+ return NS_OK;
+}
+
+void nsDocShell::ClearFrameHistory(nsISHEntry* aEntry) {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (!rootSH || !aEntry) {
+ return;
+ }
+
+ rootSH->LegacySHistory()->RemoveFrameEntries(aEntry);
+}
+
+//-------------------------------------
+//-- Helper Method for Print discovery
+//-------------------------------------
+bool nsDocShell::NavigationBlockedByPrinting(bool aDisplayErrorDialog) {
+ if (!mBrowsingContext->Top()->GetIsPrinting()) {
+ return false;
+ }
+ if (aDisplayErrorDialog) {
+ DisplayLoadError(NS_ERROR_DOCUMENT_IS_PRINTMODE, nullptr, nullptr, nullptr);
+ }
+ return true;
+}
+
+bool nsDocShell::IsNavigationAllowed(bool aDisplayPrintErrorDialog,
+ bool aCheckIfUnloadFired) {
+ bool isAllowed = !NavigationBlockedByPrinting(aDisplayPrintErrorDialog) &&
+ (!aCheckIfUnloadFired || !mFiredUnloadEvent);
+ if (!isAllowed) {
+ return false;
+ }
+ if (!mDocumentViewer) {
+ return true;
+ }
+ bool firingBeforeUnload;
+ mDocumentViewer->GetBeforeUnloadFiring(&firingBeforeUnload);
+ return !firingBeforeUnload;
+}
+
+//*****************************************************************************
+// nsDocShell::nsIWebNavigation
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::GetCanGoBack(bool* aCanGoBack) {
+ *aCanGoBack = false;
+ if (!IsNavigationAllowed(false)) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (rootSH) {
+ *aCanGoBack = rootSH->CanGo(-1);
+ MOZ_LOG(gSHLog, LogLevel::Verbose,
+ ("nsDocShell %p CanGoBack()->%d", this, *aCanGoBack));
+
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCanGoForward(bool* aCanGoForward) {
+ *aCanGoForward = false;
+ if (!IsNavigationAllowed(false)) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (rootSH) {
+ *aCanGoForward = rootSH->CanGo(1);
+ MOZ_LOG(gSHLog, LogLevel::Verbose,
+ ("nsDocShell %p CanGoForward()->%d", this, *aCanGoForward));
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDocShell::GoBack(bool aRequireUserInteraction, bool aUserActivation) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+
+ auto cleanupIsNavigating = MakeScopeExit([&]() { mIsNavigating = false; });
+ mIsNavigating = true;
+
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ NS_ENSURE_TRUE(rootSH, NS_ERROR_FAILURE);
+ ErrorResult rv;
+ rootSH->Go(-1, aRequireUserInteraction, aUserActivation, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+nsDocShell::GoForward(bool aRequireUserInteraction, bool aUserActivation) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+
+ auto cleanupIsNavigating = MakeScopeExit([&]() { mIsNavigating = false; });
+ mIsNavigating = true;
+
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ NS_ENSURE_TRUE(rootSH, NS_ERROR_FAILURE);
+ ErrorResult rv;
+ rootSH->Go(1, aRequireUserInteraction, aUserActivation, rv);
+ return rv.StealNSResult();
+}
+
+// XXX(nika): We may want to stop exposing this API in the child process? Going
+// to a specific index from multiple different processes could definitely race.
+NS_IMETHODIMP
+nsDocShell::GotoIndex(int32_t aIndex, bool aUserActivation) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+
+ auto cleanupIsNavigating = MakeScopeExit([&]() { mIsNavigating = false; });
+ mIsNavigating = true;
+
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ NS_ENSURE_TRUE(rootSH, NS_ERROR_FAILURE);
+
+ ErrorResult rv;
+ rootSH->GotoIndex(aIndex, aIndex - rootSH->Index(), false, aUserActivation,
+ rv);
+ return rv.StealNSResult();
+}
+
+nsresult nsDocShell::LoadURI(nsIURI* aURI,
+ const LoadURIOptions& aLoadURIOptions) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+ RefPtr<nsDocShellLoadState> loadState;
+ nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions(
+ mBrowsingContext, aURI, aLoadURIOptions, getter_AddRefs(loadState));
+ MOZ_ASSERT(rv != NS_ERROR_MALFORMED_URI);
+ if (NS_FAILED(rv) || !loadState) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return LoadURI(loadState, true);
+}
+
+NS_IMETHODIMP
+nsDocShell::LoadURIFromScript(nsIURI* aURI,
+ JS::Handle<JS::Value> aLoadURIOptions,
+ JSContext* aCx) {
+ // generate dictionary for aLoadURIOptions and forward call
+ LoadURIOptions loadURIOptions;
+ if (!loadURIOptions.Init(aCx, aLoadURIOptions)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ return LoadURI(aURI, loadURIOptions);
+}
+
+nsresult nsDocShell::FixupAndLoadURIString(
+ const nsAString& aURIString, const LoadURIOptions& aLoadURIOptions) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+
+ RefPtr<nsDocShellLoadState> loadState;
+ nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions(
+ mBrowsingContext, aURIString, aLoadURIOptions, getter_AddRefs(loadState));
+
+ uint32_t loadFlags = aLoadURIOptions.mLoadFlags;
+ if (NS_ERROR_MALFORMED_URI == rv) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Creating an active entry on nsDocShell %p to %s (because "
+ "we're showing an error page)",
+ this, NS_ConvertUTF16toUTF8(aURIString).get()));
+
+ // We need to store a session history entry. We don't have a valid URI, so
+ // we use about:blank instead.
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "about:blank"_ns));
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ if (aLoadURIOptions.mTriggeringPrincipal) {
+ triggeringPrincipal = aLoadURIOptions.mTriggeringPrincipal;
+ } else {
+ triggeringPrincipal = nsContentUtils::GetSystemPrincipal();
+ }
+ if (mozilla::SessionHistoryInParent()) {
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(
+ uri, triggeringPrincipal, nullptr, nullptr, nullptr,
+ nsLiteralCString("text/html"));
+ mBrowsingContext->SetActiveSessionHistoryEntry(
+ Nothing(), mActiveEntry.get(), MAKE_LOAD_TYPE(LOAD_NORMAL, loadFlags),
+ /* aUpdatedCacheKey = */ 0);
+ }
+ if (DisplayLoadError(rv, nullptr, PromiseFlatString(aURIString).get(),
+ nullptr) &&
+ (loadFlags & LOAD_FLAGS_ERROR_LOAD_CHANGES_RV) != 0) {
+ return NS_ERROR_LOAD_SHOWED_ERRORPAGE;
+ }
+ }
+
+ if (NS_FAILED(rv) || !loadState) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return LoadURI(loadState, true);
+}
+
+NS_IMETHODIMP
+nsDocShell::FixupAndLoadURIStringFromScript(
+ const nsAString& aURIString, JS::Handle<JS::Value> aLoadURIOptions,
+ JSContext* aCx) {
+ // generate dictionary for aLoadURIOptions and forward call
+ LoadURIOptions loadURIOptions;
+ if (!loadURIOptions.Init(aCx, aLoadURIOptions)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ return FixupAndLoadURIString(aURIString, loadURIOptions);
+}
+
+void nsDocShell::UnblockEmbedderLoadEventForFailure(bool aFireFrameErrorEvent) {
+ // If we're not in a content frame, or are at a BrowsingContext tree boundary,
+ // such as the content-chrome boundary, don't fire the error event.
+ if (mBrowsingContext->IsTopContent() || mBrowsingContext->IsChrome()) {
+ return;
+ }
+
+ // If embedder is same-process, then unblocking the load event is already
+ // handled by nsDocLoader. Fire the error event on our embedder element if
+ // requested.
+ //
+ // XXX: Bug 1440212 is looking into potentially changing this behaviour to act
+ // more like the remote case when in-process.
+ RefPtr<Element> element = mBrowsingContext->GetEmbedderElement();
+ if (element) {
+ if (aFireFrameErrorEvent) {
+ if (RefPtr<nsFrameLoaderOwner> flo = do_QueryObject(element)) {
+ if (RefPtr<nsFrameLoader> fl = flo->GetFrameLoader()) {
+ fl->FireErrorEvent();
+ }
+ }
+ }
+ return;
+ }
+
+ // If we have a cross-process parent document, we must notify it that we no
+ // longer block its load event. This is necessary for OOP sub-documents
+ // because error documents do not result in a call to
+ // SendMaybeFireEmbedderLoadEvents via any of the normal call paths.
+ // (Obviously, we must do this before any of the returns below.)
+ RefPtr<BrowserChild> browserChild = BrowserChild::GetFrom(this);
+ if (browserChild &&
+ !mBrowsingContext->GetParentWindowContext()->IsInProcess()) {
+ mozilla::Unused << browserChild->SendMaybeFireEmbedderLoadEvents(
+ aFireFrameErrorEvent ? EmbedderElementEventType::ErrorEvent
+ : EmbedderElementEventType::NoEvent);
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
+ const char16_t* aURL, nsIChannel* aFailedChannel,
+ bool* aDisplayedErrorPage) {
+ MOZ_LOG(gDocShellLeakLog, LogLevel::Debug,
+ ("DOCSHELL %p DisplayLoadError %s\n", this,
+ aURI ? aURI->GetSpecOrDefault().get() : ""));
+
+ *aDisplayedErrorPage = false;
+ // Get prompt and string bundle services
+ nsCOMPtr<nsIPrompt> prompter;
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ GetPromptAndStringBundle(getter_AddRefs(prompter),
+ getter_AddRefs(stringBundle));
+
+ NS_ENSURE_TRUE(stringBundle, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(prompter, NS_ERROR_FAILURE);
+
+ const char* error = nullptr;
+ // The key used to select the appropriate error message from the properties
+ // file.
+ const char* errorDescriptionID = nullptr;
+ AutoTArray<nsString, 3> formatStrs;
+ bool addHostPort = false;
+ bool isBadStsCertError = false;
+ nsresult rv = NS_OK;
+ nsAutoString messageStr;
+ nsAutoCString cssClass;
+ nsAutoCString errorPage;
+
+ errorPage.AssignLiteral("neterror");
+
+ // Turn the error code into a human readable error message.
+ if (NS_ERROR_UNKNOWN_PROTOCOL == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ // Extract the schemes into a comma delimited list.
+ nsAutoCString scheme;
+ aURI->GetScheme(scheme);
+ CopyASCIItoUTF16(scheme, *formatStrs.AppendElement());
+ nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(aURI);
+ while (nestedURI) {
+ nsCOMPtr<nsIURI> tempURI;
+ nsresult rv2;
+ rv2 = nestedURI->GetInnerURI(getter_AddRefs(tempURI));
+ if (NS_SUCCEEDED(rv2) && tempURI) {
+ tempURI->GetScheme(scheme);
+ formatStrs[0].AppendLiteral(", ");
+ AppendASCIItoUTF16(scheme, formatStrs[0]);
+ }
+ nestedURI = do_QueryInterface(tempURI);
+ }
+ error = "unknownProtocolFound";
+ } else if (NS_ERROR_FILE_NOT_FOUND == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ error = "fileNotFound";
+ } else if (NS_ERROR_FILE_ACCESS_DENIED == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ error = "fileAccessDenied";
+ } else if (NS_ERROR_UNKNOWN_HOST == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ // Get the host
+ nsAutoCString host;
+ nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(aURI);
+ innermostURI->GetHost(host);
+ CopyUTF8toUTF16(host, *formatStrs.AppendElement());
+ errorDescriptionID = "dnsNotFound2";
+ error = "dnsNotFound";
+ } else if (NS_ERROR_CONNECTION_REFUSED == aError ||
+ NS_ERROR_PROXY_BAD_GATEWAY == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ addHostPort = true;
+ error = "connectionFailure";
+ } else if (NS_ERROR_NET_INTERRUPT == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ addHostPort = true;
+ error = "netInterrupt";
+ } else if (NS_ERROR_NET_TIMEOUT == aError ||
+ NS_ERROR_PROXY_GATEWAY_TIMEOUT == aError ||
+ NS_ERROR_NET_TIMEOUT_EXTERNAL == aError) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ // Get the host
+ nsAutoCString host;
+ aURI->GetHost(host);
+ CopyUTF8toUTF16(host, *formatStrs.AppendElement());
+ error = "netTimeout";
+ } else if (NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION == aError ||
+ NS_ERROR_CSP_FORM_ACTION_VIOLATION == aError) {
+ // CSP error
+ cssClass.AssignLiteral("neterror");
+ error = "cspBlocked";
+ } else if (NS_ERROR_XFO_VIOLATION == aError) {
+ // XFO error
+ cssClass.AssignLiteral("neterror");
+ error = "xfoBlocked";
+ } else if (NS_ERROR_GET_MODULE(aError) == NS_ERROR_MODULE_SECURITY) {
+ nsCOMPtr<nsINSSErrorsService> nsserr =
+ do_GetService(NS_NSS_ERRORS_SERVICE_CONTRACTID);
+
+ uint32_t errorClass;
+ if (!nsserr || NS_FAILED(nsserr->GetErrorClass(aError, &errorClass))) {
+ errorClass = nsINSSErrorsService::ERROR_CLASS_SSL_PROTOCOL;
+ }
+
+ nsCOMPtr<nsITransportSecurityInfo> tsi;
+ if (aFailedChannel) {
+ aFailedChannel->GetSecurityInfo(getter_AddRefs(tsi));
+ }
+ if (tsi) {
+ uint32_t securityState;
+ tsi->GetSecurityState(&securityState);
+ if (securityState & nsIWebProgressListener::STATE_USES_SSL_3) {
+ error = "sslv3Used";
+ addHostPort = true;
+ } else if (securityState &
+ nsIWebProgressListener::STATE_USES_WEAK_CRYPTO) {
+ error = "weakCryptoUsed";
+ addHostPort = true;
+ }
+ } else {
+ // No channel, let's obtain the generic error message
+ if (nsserr) {
+ nsserr->GetErrorMessage(aError, messageStr);
+ }
+ }
+ // We don't have a message string here anymore but DisplayLoadError
+ // requires a non-empty messageStr.
+ messageStr.Truncate();
+ messageStr.AssignLiteral(u" ");
+ if (errorClass == nsINSSErrorsService::ERROR_CLASS_BAD_CERT) {
+ error = "nssBadCert";
+
+ // If this is an HTTP Strict Transport Security host or a pinned host
+ // and the certificate is bad, don't allow overrides (RFC 6797 section
+ // 12.1).
+ bool isStsHost = false;
+ bool isPinnedHost = false;
+ OriginAttributes attrsForHSTS;
+ if (aFailedChannel) {
+ StoragePrincipalHelper::GetOriginAttributesForHSTS(aFailedChannel,
+ attrsForHSTS);
+ } else {
+ attrsForHSTS = GetOriginAttributes();
+ }
+
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsISiteSecurityService> sss =
+ do_GetService(NS_SSSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = sss->IsSecureURI(aURI, attrsForHSTS, &isStsHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ mozilla::dom::ContentChild* cc =
+ mozilla::dom::ContentChild::GetSingleton();
+ cc->SendIsSecureURI(aURI, attrsForHSTS, &isStsHost);
+ }
+ nsCOMPtr<nsIPublicKeyPinningService> pkps =
+ do_GetService(NS_PKPSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = pkps->HostHasPins(aURI, &isPinnedHost);
+
+ if (Preferences::GetBool("browser.xul.error_pages.expert_bad_cert",
+ false)) {
+ cssClass.AssignLiteral("expertBadCert");
+ }
+
+ // HSTS/pinning takes precedence over the expert bad cert pref. We
+ // never want to show the "Add Exception" button for these sites.
+ // In the future we should differentiate between an HSTS host and a
+ // pinned host and display a more informative message to the user.
+ if (isStsHost || isPinnedHost) {
+ isBadStsCertError = true;
+ cssClass.AssignLiteral("badStsCert");
+ }
+
+ errorPage.Assign("certerror");
+ } else {
+ error = "nssFailure2";
+ }
+ } else if (NS_ERROR_PHISHING_URI == aError ||
+ NS_ERROR_MALWARE_URI == aError ||
+ NS_ERROR_UNWANTED_URI == aError ||
+ NS_ERROR_HARMFUL_URI == aError) {
+ nsAutoCString host;
+ aURI->GetHost(host);
+ CopyUTF8toUTF16(host, *formatStrs.AppendElement());
+
+ // Malware and phishing detectors may want to use an alternate error
+ // page, but if the pref's not set, we'll fall back on the standard page
+ nsAutoCString alternateErrorPage;
+ nsresult rv = Preferences::GetCString("urlclassifier.alternate_error_page",
+ alternateErrorPage);
+ if (NS_SUCCEEDED(rv)) {
+ errorPage.Assign(alternateErrorPage);
+ }
+
+ if (NS_ERROR_PHISHING_URI == aError) {
+ error = "deceptiveBlocked";
+ } else if (NS_ERROR_MALWARE_URI == aError) {
+ error = "malwareBlocked";
+ } else if (NS_ERROR_UNWANTED_URI == aError) {
+ error = "unwantedBlocked";
+ } else if (NS_ERROR_HARMFUL_URI == aError) {
+ error = "harmfulBlocked";
+ }
+
+ cssClass.AssignLiteral("blacklist");
+ } else if (NS_ERROR_CONTENT_CRASHED == aError) {
+ errorPage.AssignLiteral("tabcrashed");
+ error = "tabcrashed";
+
+ RefPtr<EventTarget> handler = mChromeEventHandler;
+ if (handler) {
+ nsCOMPtr<Element> element = do_QueryInterface(handler);
+ element->GetAttribute(u"crashedPageTitle"_ns, messageStr);
+ }
+
+ // DisplayLoadError requires a non-empty messageStr to proceed and call
+ // LoadErrorPage. If the page doesn't have a title, we will use a blank
+ // space which will be trimmed and thus treated as empty by the front-end.
+ if (messageStr.IsEmpty()) {
+ messageStr.AssignLiteral(u" ");
+ }
+ } else if (NS_ERROR_FRAME_CRASHED == aError) {
+ errorPage.AssignLiteral("framecrashed");
+ error = "framecrashed";
+ messageStr.AssignLiteral(u" ");
+ } else if (NS_ERROR_BUILDID_MISMATCH == aError) {
+ errorPage.AssignLiteral("restartrequired");
+ error = "restartrequired";
+
+ // DisplayLoadError requires a non-empty messageStr to proceed and call
+ // LoadErrorPage. If the page doesn't have a title, we will use a blank
+ // space which will be trimmed and thus treated as empty by the front-end.
+ if (messageStr.IsEmpty()) {
+ messageStr.AssignLiteral(u" ");
+ }
+ } else {
+ // Errors requiring simple formatting
+ switch (aError) {
+ case NS_ERROR_MALFORMED_URI:
+ // URI is malformed
+ error = "malformedURI";
+ errorDescriptionID = "malformedURI2";
+ break;
+ case NS_ERROR_REDIRECT_LOOP:
+ // Doc failed to load because the server generated too many redirects
+ error = "redirectLoop";
+ break;
+ case NS_ERROR_UNKNOWN_SOCKET_TYPE:
+ // Doc failed to load because PSM is not installed
+ error = "unknownSocketType";
+ break;
+ case NS_ERROR_NET_RESET:
+ // Doc failed to load because the server kept reseting the connection
+ // before we could read any data from it
+ error = "netReset";
+ break;
+ case NS_ERROR_DOCUMENT_NOT_CACHED:
+ // Doc failed to load because the cache does not contain a copy of
+ // the document.
+ error = "notCached";
+ break;
+ case NS_ERROR_OFFLINE:
+ // Doc failed to load because we are offline.
+ error = "netOffline";
+ break;
+ case NS_ERROR_DOCUMENT_IS_PRINTMODE:
+ // Doc navigation attempted while Printing or Print Preview
+ error = "isprinting";
+ break;
+ case NS_ERROR_PORT_ACCESS_NOT_ALLOWED:
+ // Port blocked for security reasons
+ addHostPort = true;
+ error = "deniedPortAccess";
+ break;
+ case NS_ERROR_UNKNOWN_PROXY_HOST:
+ // Proxy hostname could not be resolved.
+ error = "proxyResolveFailure";
+ break;
+ case NS_ERROR_PROXY_CONNECTION_REFUSED:
+ case NS_ERROR_PROXY_FORBIDDEN:
+ case NS_ERROR_PROXY_NOT_IMPLEMENTED:
+ case NS_ERROR_PROXY_AUTHENTICATION_FAILED:
+ case NS_ERROR_PROXY_TOO_MANY_REQUESTS:
+ // Proxy connection was refused.
+ error = "proxyConnectFailure";
+ break;
+ case NS_ERROR_INVALID_CONTENT_ENCODING:
+ // Bad Content Encoding.
+ error = "contentEncodingError";
+ break;
+ case NS_ERROR_UNSAFE_CONTENT_TYPE:
+ // Channel refused to load from an unrecognized content type.
+ error = "unsafeContentType";
+ break;
+ case NS_ERROR_CORRUPTED_CONTENT:
+ // Broken Content Detected. e.g. Content-MD5 check failure.
+ error = "corruptedContentErrorv2";
+ break;
+ case NS_ERROR_INTERCEPTION_FAILED:
+ // ServiceWorker intercepted request, but something went wrong.
+ error = "corruptedContentErrorv2";
+ break;
+ case NS_ERROR_NET_INADEQUATE_SECURITY:
+ // Server negotiated bad TLS for HTTP/2.
+ error = "inadequateSecurityError";
+ addHostPort = true;
+ break;
+ case NS_ERROR_BLOCKED_BY_POLICY:
+ case NS_ERROR_DOM_COOP_FAILED:
+ case NS_ERROR_DOM_COEP_FAILED:
+ // Page blocked by policy
+ error = "blockedByPolicy";
+ break;
+ case NS_ERROR_NET_HTTP2_SENT_GOAWAY:
+ case NS_ERROR_NET_HTTP3_PROTOCOL_ERROR:
+ // HTTP/2 or HTTP/3 stack detected a protocol error
+ error = "networkProtocolError";
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ nsresult delegateErrorCode = aError;
+ // If the HTTPS-Only Mode upgraded this request and the upgrade might have
+ // caused this error, we replace the error-page with about:httpsonlyerror
+ if (nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(aFailedChannel, aError)) {
+ errorPage.AssignLiteral("httpsonlyerror");
+ delegateErrorCode = NS_ERROR_HTTPS_ONLY;
+ } else if (isBadStsCertError) {
+ delegateErrorCode = NS_ERROR_BAD_HSTS_CERT;
+ }
+
+ if (nsCOMPtr<nsILoadURIDelegate> loadURIDelegate = GetLoadURIDelegate()) {
+ nsCOMPtr<nsIURI> errorPageURI;
+ rv = loadURIDelegate->HandleLoadError(
+ aURI, delegateErrorCode, NS_ERROR_GET_MODULE(delegateErrorCode),
+ getter_AddRefs(errorPageURI));
+ // If the docshell is going away there's no point in showing an error page.
+ if (NS_FAILED(rv) || mIsBeingDestroyed) {
+ *aDisplayedErrorPage = false;
+ return NS_OK;
+ }
+
+ if (errorPageURI) {
+ *aDisplayedErrorPage =
+ NS_SUCCEEDED(LoadErrorPage(errorPageURI, aURI, aFailedChannel));
+ return NS_OK;
+ }
+ }
+
+ // Test if the error should be displayed
+ if (!error) {
+ return NS_OK;
+ }
+
+ if (!errorDescriptionID) {
+ errorDescriptionID = error;
+ }
+
+ Telemetry::AccumulateCategoricalKeyed(
+ IsSubframe() ? "frame"_ns : "top"_ns,
+ mozilla::dom::LoadErrorToTelemetryLabel(aError));
+
+ // Test if the error needs to be formatted
+ if (!messageStr.IsEmpty()) {
+ // already obtained message
+ } else {
+ if (addHostPort) {
+ // Build up the host:port string.
+ nsAutoCString hostport;
+ if (aURI) {
+ aURI->GetHostPort(hostport);
+ } else {
+ hostport.Assign('?');
+ }
+ CopyUTF8toUTF16(hostport, *formatStrs.AppendElement());
+ }
+
+ nsAutoCString spec;
+ rv = NS_ERROR_NOT_AVAILABLE;
+ auto& nextFormatStr = *formatStrs.AppendElement();
+ if (aURI) {
+ // displaying "file://" is aesthetically unpleasing and could even be
+ // confusing to the user
+ if (SchemeIsFile(aURI)) {
+ aURI->GetPathQueryRef(spec);
+ } else {
+ aURI->GetSpec(spec);
+ }
+
+ nsCOMPtr<nsITextToSubURI> textToSubURI(
+ do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = textToSubURI->UnEscapeURIForUI(spec, nextFormatStr);
+ }
+ } else {
+ spec.Assign('?');
+ }
+ if (NS_FAILED(rv)) {
+ CopyUTF8toUTF16(spec, nextFormatStr);
+ }
+ rv = NS_OK;
+
+ nsAutoString str;
+ rv =
+ stringBundle->FormatStringFromName(errorDescriptionID, formatStrs, str);
+ NS_ENSURE_SUCCESS(rv, rv);
+ messageStr.Assign(str);
+ }
+
+ // Display the error as a page or an alert prompt
+ NS_ENSURE_FALSE(messageStr.IsEmpty(), NS_ERROR_FAILURE);
+
+ if ((NS_ERROR_NET_INTERRUPT == aError || NS_ERROR_NET_RESET == aError) &&
+ SchemeIsHTTPS(aURI)) {
+ // Maybe TLS intolerant. Treat this as an SSL error.
+ error = "nssFailure2";
+ }
+
+ if (mBrowsingContext->GetUseErrorPages()) {
+ // Display an error page
+ nsresult loadedPage =
+ LoadErrorPage(aURI, aURL, errorPage.get(), error, messageStr.get(),
+ cssClass.get(), aFailedChannel);
+ *aDisplayedErrorPage = NS_SUCCEEDED(loadedPage);
+ } else {
+ // The prompter reqires that our private window has a document (or it
+ // asserts). Satisfy that assertion now since GetDoc will force
+ // creation of one if it hasn't already been created.
+ if (mScriptGlobal) {
+ Unused << mScriptGlobal->GetDoc();
+ }
+
+ // Display a message box
+ prompter->Alert(nullptr, messageStr.get());
+ }
+
+ return NS_OK;
+}
+
+#define PREF_SAFEBROWSING_ALLOWOVERRIDE "browser.safebrowsing.allowOverride"
+
+nsresult nsDocShell::LoadErrorPage(nsIURI* aURI, const char16_t* aURL,
+ const char* aErrorPage,
+ const char* aErrorType,
+ const char16_t* aDescription,
+ const char* aCSSClass,
+ nsIChannel* aFailedChannel) {
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+#if defined(DEBUG)
+ if (MOZ_LOG_TEST(gDocShellLog, LogLevel::Debug)) {
+ nsAutoCString chanName;
+ if (aFailedChannel) {
+ aFailedChannel->GetName(chanName);
+ } else {
+ chanName.AssignLiteral("<no channel>");
+ }
+
+ MOZ_LOG(gDocShellLog, LogLevel::Debug,
+ ("nsDocShell[%p]::LoadErrorPage(\"%s\", \"%s\", {...}, [%s])\n",
+ this, aURI ? aURI->GetSpecOrDefault().get() : "",
+ NS_ConvertUTF16toUTF8(aURL).get(), chanName.get()));
+ }
+#endif
+
+ nsAutoCString url;
+ if (aURI) {
+ nsresult rv = aURI->GetSpec(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (aURL) {
+ CopyUTF16toUTF8(MakeStringSpan(aURL), url);
+ } else {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ // Create a URL to pass all the error information through to the page.
+
+#undef SAFE_ESCAPE
+#define SAFE_ESCAPE(output, input, params) \
+ if (NS_WARN_IF(!NS_Escape(input, output, params))) { \
+ return NS_ERROR_OUT_OF_MEMORY; \
+ }
+
+ nsCString escapedUrl, escapedError, escapedDescription, escapedCSSClass;
+ SAFE_ESCAPE(escapedUrl, url, url_Path);
+ SAFE_ESCAPE(escapedError, nsDependentCString(aErrorType), url_Path);
+ SAFE_ESCAPE(escapedDescription, NS_ConvertUTF16toUTF8(aDescription),
+ url_Path);
+ if (aCSSClass) {
+ nsCString cssClass(aCSSClass);
+ SAFE_ESCAPE(escapedCSSClass, cssClass, url_Path);
+ }
+ nsCString errorPageUrl("about:");
+ errorPageUrl.AppendASCII(aErrorPage);
+ errorPageUrl.AppendLiteral("?e=");
+
+ errorPageUrl.AppendASCII(escapedError.get());
+ errorPageUrl.AppendLiteral("&u=");
+ errorPageUrl.AppendASCII(escapedUrl.get());
+ if ((strcmp(aErrorPage, "blocked") == 0) &&
+ Preferences::GetBool(PREF_SAFEBROWSING_ALLOWOVERRIDE, true)) {
+ errorPageUrl.AppendLiteral("&o=1");
+ }
+ if (!escapedCSSClass.IsEmpty()) {
+ errorPageUrl.AppendLiteral("&s=");
+ errorPageUrl.AppendASCII(escapedCSSClass.get());
+ }
+ errorPageUrl.AppendLiteral("&c=UTF-8");
+
+ nsCOMPtr<nsICaptivePortalService> cps = do_GetService(NS_CAPTIVEPORTAL_CID);
+ int32_t cpsState;
+ if (cps && NS_SUCCEEDED(cps->GetState(&cpsState)) &&
+ cpsState == nsICaptivePortalService::LOCKED_PORTAL) {
+ errorPageUrl.AppendLiteral("&captive=true");
+ }
+
+ errorPageUrl.AppendLiteral("&d=");
+ errorPageUrl.AppendASCII(escapedDescription.get());
+
+ nsCOMPtr<nsIURI> errorPageURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(errorPageURI), errorPageUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return LoadErrorPage(errorPageURI, aURI, aFailedChannel);
+}
+
+nsresult nsDocShell::LoadErrorPage(nsIURI* aErrorURI, nsIURI* aFailedURI,
+ nsIChannel* aFailedChannel) {
+ mFailedChannel = aFailedChannel;
+ mFailedURI = aFailedURI;
+ mFailedLoadType = mLoadType;
+
+ if (mLSHE) {
+ // Abandon mLSHE's BFCache entry and create a new one. This way, if
+ // we go back or forward to another SHEntry with the same doc
+ // identifier, the error page won't persist.
+ mLSHE->AbandonBFCacheEntry();
+ }
+
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aErrorURI);
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ if (mBrowsingContext) {
+ loadState->SetTriggeringSandboxFlags(mBrowsingContext->GetSandboxFlags());
+ loadState->SetTriggeringWindowId(
+ mBrowsingContext->GetCurrentInnerWindowId());
+ nsPIDOMWindowInner* innerWin = mScriptGlobal->GetCurrentInnerWindow();
+ if (innerWin) {
+ loadState->SetTriggeringStorageAccess(innerWin->UsingStorageAccess());
+ }
+ }
+ loadState->SetLoadType(LOAD_ERROR_PAGE);
+ loadState->SetFirstParty(true);
+ loadState->SetSourceBrowsingContext(mBrowsingContext);
+ if (mozilla::SessionHistoryInParent() && mLoadingEntry) {
+ // We keep the loading entry for the load that failed here. If the user
+ // reloads we want to try to reload the original load, not the error page.
+ loadState->SetLoadingSessionHistoryInfo(
+ MakeUnique<LoadingSessionHistoryInfo>(*mLoadingEntry));
+ }
+ return InternalLoad(loadState);
+}
+
+NS_IMETHODIMP
+nsDocShell::Reload(uint32_t aReloadFlags) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK; // JS may not handle returning of an error code
+ }
+
+ NS_ASSERTION(((aReloadFlags & INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS) == 0),
+ "Reload command not updated to use load flags!");
+ NS_ASSERTION((aReloadFlags & EXTRA_LOAD_FLAGS) == 0,
+ "Don't pass these flags to Reload");
+
+ uint32_t loadType = MAKE_LOAD_TYPE(LOAD_RELOAD_NORMAL, aReloadFlags);
+ NS_ENSURE_TRUE(IsValidLoadType(loadType), NS_ERROR_INVALID_ARG);
+
+ // Send notifications to the HistoryListener if any, about the impending
+ // reload
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (mozilla::SessionHistoryInParent()) {
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("nsDocShell %p Reload", this));
+ bool forceReload = IsForceReloadType(loadType);
+ if (!XRE_IsParentProcess()) {
+ ++mPendingReloadCount;
+ RefPtr<nsDocShell> docShell(this);
+ nsCOMPtr<nsIDocumentViewer> viewer(mDocumentViewer);
+ NS_ENSURE_STATE(viewer);
+
+ bool okToUnload = true;
+ MOZ_TRY(viewer->PermitUnload(&okToUnload));
+ if (!okToUnload) {
+ return NS_OK;
+ }
+
+ RefPtr<Document> doc(GetDocument());
+ RefPtr<BrowsingContext> browsingContext(mBrowsingContext);
+ nsCOMPtr<nsIURI> currentURI(mCurrentURI);
+ nsCOMPtr<nsIReferrerInfo> referrerInfo(mReferrerInfo);
+ RefPtr<StopDetector> stopDetector = new StopDetector();
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ GetLoadGroup(getter_AddRefs(loadGroup));
+ if (loadGroup) {
+ // loadGroup may be null in theory. In that case stopDetector just
+ // doesn't do anything.
+ loadGroup->AddRequest(stopDetector, nullptr);
+ }
+
+ ContentChild::GetSingleton()->SendNotifyOnHistoryReload(
+ mBrowsingContext, forceReload,
+ [docShell, doc, loadType, browsingContext, currentURI, referrerInfo,
+ loadGroup, stopDetector](
+ std::tuple<bool, Maybe<NotNull<RefPtr<nsDocShellLoadState>>>,
+ Maybe<bool>>&& aResult) {
+ auto scopeExit = MakeScopeExit([loadGroup, stopDetector]() {
+ if (loadGroup) {
+ loadGroup->RemoveRequest(stopDetector, nullptr, NS_OK);
+ }
+ });
+
+ // Decrease mPendingReloadCount before any other early returns!
+ if (--(docShell->mPendingReloadCount) > 0) {
+ return;
+ }
+
+ if (stopDetector->Canceled()) {
+ return;
+ }
+ bool canReload;
+ Maybe<NotNull<RefPtr<nsDocShellLoadState>>> loadState;
+ Maybe<bool> reloadingActiveEntry;
+
+ std::tie(canReload, loadState, reloadingActiveEntry) = aResult;
+
+ if (!canReload) {
+ return;
+ }
+
+ if (loadState.isSome()) {
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ ("nsDocShell %p Reload - LoadHistoryEntry", docShell.get()));
+ loadState.ref()->SetNotifiedBeforeUnloadListeners(true);
+ docShell->LoadHistoryEntry(loadState.ref(), loadType,
+ reloadingActiveEntry.ref());
+ } else {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p ReloadDocument", docShell.get()));
+ ReloadDocument(docShell, doc, loadType, browsingContext,
+ currentURI, referrerInfo,
+ /* aNotifiedBeforeUnloadListeners */ true);
+ }
+ },
+ [](mozilla::ipc::ResponseRejectReason) {});
+ } else {
+ // Parent process
+ bool canReload = false;
+ Maybe<NotNull<RefPtr<nsDocShellLoadState>>> loadState;
+ Maybe<bool> reloadingActiveEntry;
+ if (!mBrowsingContext->IsDiscarded()) {
+ mBrowsingContext->Canonical()->NotifyOnHistoryReload(
+ forceReload, canReload, loadState, reloadingActiveEntry);
+ }
+ if (canReload) {
+ if (loadState.isSome()) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p Reload - LoadHistoryEntry", this));
+ LoadHistoryEntry(loadState.ref(), loadType,
+ reloadingActiveEntry.ref());
+ } else {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p ReloadDocument", this));
+ RefPtr<Document> doc = GetDocument();
+ RefPtr<BrowsingContext> bc = mBrowsingContext;
+ nsCOMPtr<nsIURI> currentURI = mCurrentURI;
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = mReferrerInfo;
+ ReloadDocument(this, doc, loadType, bc, currentURI, referrerInfo);
+ }
+ }
+ }
+ return NS_OK;
+ }
+
+ bool canReload = true;
+ if (rootSH) {
+ rootSH->LegacySHistory()->NotifyOnHistoryReload(&canReload);
+ }
+
+ if (!canReload) {
+ return NS_OK;
+ }
+
+ /* If you change this part of code, make sure bug 45297 does not re-occur */
+ if (mOSHE) {
+ nsCOMPtr<nsISHEntry> oshe = mOSHE;
+ return LoadHistoryEntry(
+ oshe, loadType,
+ aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION);
+ }
+
+ if (mLSHE) { // In case a reload happened before the current load is done
+ nsCOMPtr<nsISHEntry> lshe = mLSHE;
+ return LoadHistoryEntry(
+ lshe, loadType,
+ aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION);
+ }
+
+ RefPtr<Document> doc = GetDocument();
+ RefPtr<BrowsingContext> bc = mBrowsingContext;
+ nsCOMPtr<nsIURI> currentURI = mCurrentURI;
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = mReferrerInfo;
+ return ReloadDocument(this, doc, loadType, bc, currentURI, referrerInfo);
+}
+
+/* static */
+nsresult nsDocShell::ReloadDocument(nsDocShell* aDocShell, Document* aDocument,
+ uint32_t aLoadType,
+ BrowsingContext* aBrowsingContext,
+ nsIURI* aCurrentURI,
+ nsIReferrerInfo* aReferrerInfo,
+ bool aNotifiedBeforeUnloadListeners) {
+ if (!aDocument) {
+ return NS_OK;
+ }
+
+ // Do not inherit owner from document
+ uint32_t flags = INTERNAL_LOAD_FLAGS_NONE;
+ nsAutoString srcdoc;
+ nsIURI* baseURI = nullptr;
+ nsCOMPtr<nsIURI> originalURI;
+ nsCOMPtr<nsIURI> resultPrincipalURI;
+ bool loadReplace = false;
+
+ nsIPrincipal* triggeringPrincipal = aDocument->NodePrincipal();
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
+ uint32_t triggeringSandboxFlags = aDocument->GetSandboxFlags();
+ uint64_t triggeringWindowId = aDocument->InnerWindowID();
+ bool triggeringStorageAccess = aDocument->UsingStorageAccess();
+
+ nsAutoString contentTypeHint;
+ aDocument->GetContentType(contentTypeHint);
+
+ if (aDocument->IsSrcdocDocument()) {
+ aDocument->GetSrcdocData(srcdoc);
+ flags |= INTERNAL_LOAD_FLAGS_IS_SRCDOC;
+ baseURI = aDocument->GetBaseURI();
+ } else {
+ srcdoc = VoidString();
+ }
+ nsCOMPtr<nsIChannel> chan = aDocument->GetChannel();
+ if (chan) {
+ uint32_t loadFlags;
+ chan->GetLoadFlags(&loadFlags);
+ loadReplace = loadFlags & nsIChannel::LOAD_REPLACE;
+ nsCOMPtr<nsIHttpChannel> httpChan(do_QueryInterface(chan));
+ if (httpChan) {
+ httpChan->GetOriginalURI(getter_AddRefs(originalURI));
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo();
+ loadInfo->GetResultPrincipalURI(getter_AddRefs(resultPrincipalURI));
+ }
+
+ if (!triggeringPrincipal) {
+ MOZ_ASSERT(false, "Reload needs a valid triggeringPrincipal");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Stack variables to ensure changes to the member variables don't affect to
+ // the call.
+ nsCOMPtr<nsIURI> currentURI = aCurrentURI;
+
+ // Reload always rewrites result principal URI.
+ Maybe<nsCOMPtr<nsIURI>> emplacedResultPrincipalURI;
+ emplacedResultPrincipalURI.emplace(std::move(resultPrincipalURI));
+
+ RefPtr<WindowContext> context = aBrowsingContext->GetCurrentWindowContext();
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(currentURI);
+ loadState->SetReferrerInfo(aReferrerInfo);
+ loadState->SetOriginalURI(originalURI);
+ loadState->SetMaybeResultPrincipalURI(emplacedResultPrincipalURI);
+ loadState->SetLoadReplace(loadReplace);
+ loadState->SetTriggeringPrincipal(triggeringPrincipal);
+ loadState->SetTriggeringSandboxFlags(triggeringSandboxFlags);
+ loadState->SetTriggeringWindowId(triggeringWindowId);
+ loadState->SetTriggeringStorageAccess(triggeringStorageAccess);
+ loadState->SetPrincipalToInherit(triggeringPrincipal);
+ loadState->SetCsp(csp);
+ loadState->SetInternalLoadFlags(flags);
+ loadState->SetTypeHint(NS_ConvertUTF16toUTF8(contentTypeHint));
+ loadState->SetLoadType(aLoadType);
+ loadState->SetFirstParty(true);
+ loadState->SetSrcdocData(srcdoc);
+ loadState->SetSourceBrowsingContext(aBrowsingContext);
+ loadState->SetBaseURI(baseURI);
+ loadState->SetHasValidUserGestureActivation(
+ context && context->HasValidTransientUserGestureActivation());
+ loadState->SetNotifiedBeforeUnloadListeners(aNotifiedBeforeUnloadListeners);
+ return aDocShell->InternalLoad(loadState);
+}
+
+NS_IMETHODIMP
+nsDocShell::Stop(uint32_t aStopFlags) {
+ // Revoke any pending event related to content viewer restoration
+ mRestorePresentationEvent.Revoke();
+
+ if (mLoadType == LOAD_ERROR_PAGE) {
+ if (mLSHE) {
+ // Since error page loads never unset mLSHE, do so now
+ SetHistoryEntryAndUpdateBC(Some(nullptr), Some<nsISHEntry*>(mLSHE));
+ }
+ mActiveEntryIsLoadingFromSessionHistory = false;
+
+ mFailedChannel = nullptr;
+ mFailedURI = nullptr;
+ }
+
+ if (nsIWebNavigation::STOP_CONTENT & aStopFlags) {
+ // Stop the document loading and animations
+ if (mDocumentViewer) {
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ viewer->Stop();
+ }
+ } else if (nsIWebNavigation::STOP_NETWORK & aStopFlags) {
+ // Stop the document loading only
+ if (mDocumentViewer) {
+ RefPtr<Document> doc = mDocumentViewer->GetDocument();
+ if (doc) {
+ doc->StopDocumentLoad();
+ }
+ }
+ }
+
+ if (nsIWebNavigation::STOP_NETWORK & aStopFlags) {
+ // Suspend any timers that were set for this loader. We'll clear
+ // them out for good in CreateDocumentViewer.
+ if (mRefreshURIList) {
+ SuspendRefreshURIs();
+ mSavedRefreshURIList.swap(mRefreshURIList);
+ mRefreshURIList = nullptr;
+ }
+
+ // XXXbz We could also pass |this| to nsIURILoader::Stop. That will
+ // just call Stop() on us as an nsIDocumentLoader... We need fewer
+ // redundant apis!
+ Stop();
+
+ // Clear out mChannelToDisconnectOnPageHide. This page won't go in the
+ // BFCache now, and the Stop above will have removed the DocumentChannel
+ // from the loadgroup.
+ mChannelToDisconnectOnPageHide = 0;
+ }
+
+ for (auto* child : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIWebNavigation> shellAsNav(do_QueryObject(child));
+ if (shellAsNav) {
+ shellAsNav->Stop(aStopFlags);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDocument(Document** aDocument) {
+ NS_ENSURE_ARG_POINTER(aDocument);
+ NS_ENSURE_SUCCESS(EnsureDocumentViewer(), NS_ERROR_FAILURE);
+
+ RefPtr<Document> doc = mDocumentViewer->GetDocument();
+ if (!doc) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ doc.forget(aDocument);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCurrentURI(nsIURI** aURI) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsCOMPtr<nsIURI> uri = mCurrentURI;
+ uri.forget(aURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetSessionHistoryXPCOM(nsISupports** aSessionHistory) {
+ NS_ENSURE_ARG_POINTER(aSessionHistory);
+ RefPtr<ChildSHistory> shistory = GetSessionHistory();
+ shistory.forget(aSessionHistory);
+ return NS_OK;
+}
+
+//*****************************************************************************
+// nsDocShell::nsIWebPageDescriptor
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::LoadPageAsViewSource(nsIDocShell* aOtherDocShell,
+ const nsAString& aURI) {
+ if (!aOtherDocShell) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(newURI), aURI);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ RefPtr<nsDocShellLoadState> loadState;
+ uint32_t cacheKey;
+ auto* otherDocShell = nsDocShell::Cast(aOtherDocShell);
+ if (mozilla::SessionHistoryInParent()) {
+ loadState = new nsDocShellLoadState(newURI);
+ if (!otherDocShell->FillLoadStateFromCurrentEntry(*loadState)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+ cacheKey = otherDocShell->GetCacheKeyFromCurrentEntry().valueOr(0);
+ } else {
+ nsCOMPtr<nsISHEntry> entry;
+ bool isOriginalSHE;
+ otherDocShell->GetCurrentSHEntry(getter_AddRefs(entry), &isOriginalSHE);
+ if (!entry) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+ rv = entry->CreateLoadInfo(getter_AddRefs(loadState));
+ NS_ENSURE_SUCCESS(rv, rv);
+ entry->GetCacheKey(&cacheKey);
+ loadState->SetURI(newURI);
+ loadState->SetSHEntry(nullptr);
+ }
+
+ // We're doing a load of the page, via an API that
+ // is only exposed to system code. The triggering principal for this load
+ // should be the system principal.
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ loadState->SetOriginalURI(nullptr);
+ loadState->SetResultPrincipalURI(nullptr);
+
+ return InternalLoad(loadState, Some(cacheKey));
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCurrentDescriptor(nsISupports** aPageDescriptor) {
+ MOZ_ASSERT(aPageDescriptor, "Null out param?");
+
+ *aPageDescriptor = nullptr;
+
+ nsISHEntry* src = mOSHE ? mOSHE : mLSHE;
+ if (src) {
+ nsCOMPtr<nsISHEntry> dest;
+
+ nsresult rv = src->Clone(getter_AddRefs(dest));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // null out inappropriate cloned attributes...
+ dest->SetParent(nullptr);
+ dest->SetIsSubFrame(false);
+
+ return CallQueryInterface(dest, aPageDescriptor);
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+already_AddRefed<nsIInputStream> nsDocShell::GetPostDataFromCurrentEntry()
+ const {
+ nsCOMPtr<nsIInputStream> postData;
+ if (mozilla::SessionHistoryInParent()) {
+ if (mActiveEntry) {
+ postData = mActiveEntry->GetPostData();
+ } else if (mLoadingEntry) {
+ postData = mLoadingEntry->mInfo.GetPostData();
+ }
+ } else {
+ if (mOSHE) {
+ postData = mOSHE->GetPostData();
+ } else if (mLSHE) {
+ postData = mLSHE->GetPostData();
+ }
+ }
+
+ return postData.forget();
+}
+
+Maybe<uint32_t> nsDocShell::GetCacheKeyFromCurrentEntry() const {
+ if (mozilla::SessionHistoryInParent()) {
+ if (mActiveEntry) {
+ return Some(mActiveEntry->GetCacheKey());
+ }
+
+ if (mLoadingEntry) {
+ return Some(mLoadingEntry->mInfo.GetCacheKey());
+ }
+ } else {
+ if (mOSHE) {
+ return Some(mOSHE->GetCacheKey());
+ }
+
+ if (mLSHE) {
+ return Some(mLSHE->GetCacheKey());
+ }
+ }
+
+ return Nothing();
+}
+
+bool nsDocShell::FillLoadStateFromCurrentEntry(
+ nsDocShellLoadState& aLoadState) {
+ if (mLoadingEntry) {
+ mLoadingEntry->mInfo.FillLoadInfo(aLoadState);
+ return true;
+ }
+ if (mActiveEntry) {
+ mActiveEntry->FillLoadInfo(aLoadState);
+ return true;
+ }
+ return false;
+}
+
+//*****************************************************************************
+// nsDocShell::nsIBaseWindow
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::InitWindow(nativeWindow aParentNativeWindow,
+ nsIWidget* aParentWidget, int32_t aX, int32_t aY,
+ int32_t aWidth, int32_t aHeight) {
+ SetParentWidget(aParentWidget);
+ SetPositionAndSize(aX, aY, aWidth, aHeight, 0);
+ NS_ENSURE_TRUE(Initialize(), NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::Destroy() {
+ // XXX: We allow this function to be called just once. If you are going to
+ // reset new variables in this function, please make sure the variables will
+ // never be re-initialized. Adding assertions to check |mIsBeingDestroyed|
+ // in the setter functions for the variables would be enough.
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_DOCSHELL_DYING;
+ }
+
+ NS_ASSERTION(mItemType == typeContent || mItemType == typeChrome,
+ "Unexpected item type in docshell");
+
+ nsCOMPtr<nsIObserverService> serv = services::GetObserverService();
+ if (serv) {
+ const char* msg = mItemType == typeContent
+ ? NS_WEBNAVIGATION_DESTROY
+ : NS_CHROME_WEBNAVIGATION_DESTROY;
+ serv->NotifyObservers(GetAsSupports(this), msg, nullptr);
+ }
+
+ mIsBeingDestroyed = true;
+
+ // Brak the cycle with the initial client, if present.
+ mInitialClientSource.reset();
+
+ // Make sure to blow away our mLoadingURI just in case. No loads
+ // from inside this pagehide.
+ mLoadingURI = nullptr;
+
+ // Fire unload event before we blow anything away.
+ (void)FirePageHideNotification(true);
+
+ // Clear pointers to any detached nsEditorData that's lying
+ // around in shistory entries. Breaks cycle. See bug 430921.
+ if (mOSHE) {
+ mOSHE->SetEditorData(nullptr);
+ }
+ if (mLSHE) {
+ mLSHE->SetEditorData(nullptr);
+ }
+
+ // Note: mContentListener can be null if Init() failed and we're being
+ // called from the destructor.
+ if (mContentListener) {
+ mContentListener->DropDocShellReference();
+ mContentListener->SetParentContentListener(nullptr);
+ // Note that we do NOT set mContentListener to null here; that
+ // way if someone tries to do a load in us after this point
+ // the nsDSURIContentListener will block it. All of which
+ // means that we should do this before calling Stop(), of
+ // course.
+ }
+
+ // Stop any URLs that are currently being loaded...
+ Stop(nsIWebNavigation::STOP_ALL);
+
+ mEditorData = nullptr;
+
+ // Save the state of the current document, before destroying the window.
+ // This is needed to capture the state of a frameset when the new document
+ // causes the frameset to be destroyed...
+ PersistLayoutHistoryState();
+
+ // Remove this docshell from its parent's child list
+ nsCOMPtr<nsIDocShellTreeItem> docShellParentAsItem =
+ do_QueryInterface(GetAsSupports(mParent));
+ if (docShellParentAsItem) {
+ docShellParentAsItem->RemoveChild(this);
+ }
+
+ if (mDocumentViewer) {
+ mDocumentViewer->Close(nullptr);
+ mDocumentViewer->Destroy();
+ mDocumentViewer = nullptr;
+ }
+
+ nsDocLoader::Destroy();
+
+ mParentWidget = nullptr;
+ SetCurrentURIInternal(nullptr);
+
+ if (mScriptGlobal) {
+ mScriptGlobal->DetachFromDocShell(!mWillChangeProcess);
+ mScriptGlobal = nullptr;
+ }
+
+ if (GetSessionHistory()) {
+ // We want to destroy these content viewers now rather than
+ // letting their destruction wait for the session history
+ // entries to get garbage collected. (Bug 488394)
+ GetSessionHistory()->EvictLocalDocumentViewers();
+ }
+
+ if (mWillChangeProcess && !mBrowsingContext->IsDiscarded()) {
+ mBrowsingContext->PrepareForProcessChange();
+ }
+
+ SetTreeOwner(nullptr);
+
+ mBrowserChild = nullptr;
+
+ mChromeEventHandler = nullptr;
+
+ // Cancel any timers that were set for this docshell; this is needed
+ // to break the cycle between us and the timers.
+ CancelRefreshURITimers();
+
+ return NS_OK;
+}
+
+double nsDocShell::GetWidgetCSSToDeviceScale() {
+ if (mParentWidget) {
+ return mParentWidget->GetDefaultScale().scale;
+ }
+ if (nsCOMPtr<nsIBaseWindow> ownerWindow = do_QueryInterface(mTreeOwner)) {
+ return ownerWindow->GetWidgetCSSToDeviceScale();
+ }
+ return 1.0;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDevicePixelsPerDesktopPixel(double* aScale) {
+ if (mParentWidget) {
+ *aScale = mParentWidget->GetDesktopToDeviceScale().scale;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIBaseWindow> ownerWindow(do_QueryInterface(mTreeOwner));
+ if (ownerWindow) {
+ return ownerWindow->GetDevicePixelsPerDesktopPixel(aScale);
+ }
+
+ *aScale = 1.0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetPosition(int32_t aX, int32_t aY) {
+ mBounds.MoveTo(aX, aY);
+
+ if (mDocumentViewer) {
+ NS_ENSURE_SUCCESS(mDocumentViewer->Move(aX, aY), NS_ERROR_FAILURE);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetPositionDesktopPix(int32_t aX, int32_t aY) {
+ nsCOMPtr<nsIBaseWindow> ownerWindow(do_QueryInterface(mTreeOwner));
+ if (ownerWindow) {
+ return ownerWindow->SetPositionDesktopPix(aX, aY);
+ }
+
+ double scale = 1.0;
+ GetDevicePixelsPerDesktopPixel(&scale);
+ return SetPosition(NSToIntRound(aX * scale), NSToIntRound(aY * scale));
+}
+
+NS_IMETHODIMP
+nsDocShell::GetPosition(int32_t* aX, int32_t* aY) {
+ return GetPositionAndSize(aX, aY, nullptr, nullptr);
+}
+
+NS_IMETHODIMP
+nsDocShell::SetSize(int32_t aWidth, int32_t aHeight, bool aRepaint) {
+ int32_t x = 0, y = 0;
+ GetPosition(&x, &y);
+ return SetPositionAndSize(x, y, aWidth, aHeight,
+ aRepaint ? nsIBaseWindow::eRepaint : 0);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetSize(int32_t* aWidth, int32_t* aHeight) {
+ return GetPositionAndSize(nullptr, nullptr, aWidth, aHeight);
+}
+
+NS_IMETHODIMP
+nsDocShell::SetPositionAndSize(int32_t aX, int32_t aY, int32_t aWidth,
+ int32_t aHeight, uint32_t aFlags) {
+ mBounds.SetRect(aX, aY, aWidth, aHeight);
+
+ // Hold strong ref, since SetBounds can make us null out mDocumentViewer
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ if (viewer) {
+ uint32_t cvflags = (aFlags & nsIBaseWindow::eDelayResize)
+ ? nsIDocumentViewer::eDelayResize
+ : 0;
+ // XXX Border figured in here or is that handled elsewhere?
+ nsresult rv = viewer->SetBoundsWithFlags(mBounds, cvflags);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) {
+ if (mParentWidget) {
+ // ensure size is up-to-date if window has changed resolution
+ LayoutDeviceIntRect r = mParentWidget->GetClientBounds();
+ SetPositionAndSize(mBounds.X(), mBounds.Y(), r.Width(), r.Height(), 0);
+ }
+
+ // We should really consider just getting this information from
+ // our window instead of duplicating the storage and code...
+ if (aWidth || aHeight) {
+ // Caller wants to know our size; make sure to give them up to
+ // date information.
+ RefPtr<Document> doc(do_GetInterface(GetAsSupports(mParent)));
+ if (doc) {
+ doc->FlushPendingNotifications(FlushType::Layout);
+ }
+ }
+
+ DoGetPositionAndSize(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
+void nsDocShell::DoGetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) {
+ if (aX) {
+ *aX = mBounds.X();
+ }
+ if (aY) {
+ *aY = mBounds.Y();
+ }
+ if (aWidth) {
+ *aWidth = mBounds.Width();
+ }
+ if (aHeight) {
+ *aHeight = mBounds.Height();
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::SetDimensions(DimensionRequest&& aRequest) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDimensions(DimensionKind aDimensionKind, int32_t* aX,
+ int32_t* aY, int32_t* aCX, int32_t* aCY) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShell::Repaint(bool aForce) {
+ PresShell* presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+
+ RefPtr<nsViewManager> viewManager = presShell->GetViewManager();
+ NS_ENSURE_TRUE(viewManager, NS_ERROR_FAILURE);
+
+ viewManager->InvalidateAllViews();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetParentWidget(nsIWidget** aParentWidget) {
+ NS_ENSURE_ARG_POINTER(aParentWidget);
+
+ *aParentWidget = mParentWidget;
+ NS_IF_ADDREF(*aParentWidget);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetParentWidget(nsIWidget* aParentWidget) {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+ mParentWidget = aParentWidget;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetParentNativeWindow(nativeWindow* aParentNativeWindow) {
+ NS_ENSURE_ARG_POINTER(aParentNativeWindow);
+
+ if (mParentWidget) {
+ *aParentNativeWindow = mParentWidget->GetNativeData(NS_NATIVE_WIDGET);
+ } else {
+ *aParentNativeWindow = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetParentNativeWindow(nativeWindow aParentNativeWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetNativeHandle(nsAString& aNativeHandle) {
+ // the nativeHandle should be accessed from nsIAppWindow
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetVisibility(bool* aVisibility) {
+ NS_ENSURE_ARG_POINTER(aVisibility);
+
+ *aVisibility = false;
+
+ if (!mDocumentViewer) {
+ return NS_OK;
+ }
+
+ PresShell* presShell = GetPresShell();
+ if (!presShell) {
+ return NS_OK;
+ }
+
+ // get the view manager
+ nsViewManager* vm = presShell->GetViewManager();
+ NS_ENSURE_TRUE(vm, NS_ERROR_FAILURE);
+
+ // get the root view
+ nsView* view = vm->GetRootView(); // views are not ref counted
+ NS_ENSURE_TRUE(view, NS_ERROR_FAILURE);
+
+ // if our root view is hidden, we are not visible
+ if (view->GetVisibility() == ViewVisibility::Hide) {
+ return NS_OK;
+ }
+
+ // otherwise, we must walk up the document and view trees checking
+ // for a hidden view, unless we're an off screen browser, which
+ // would make this test meaningless.
+
+ RefPtr<nsDocShell> docShell = this;
+ RefPtr<nsDocShell> parentItem = docShell->GetInProcessParentDocshell();
+ while (parentItem) {
+ // Null-check for crash in bug 267804
+ if (!parentItem->GetPresShell()) {
+ MOZ_ASSERT_UNREACHABLE("parent docshell has null pres shell");
+ return NS_OK;
+ }
+
+ vm = docShell->GetPresShell()->GetViewManager();
+ if (vm) {
+ view = vm->GetRootView();
+ }
+
+ if (view) {
+ view = view->GetParent(); // anonymous inner view
+ if (view) {
+ view = view->GetParent(); // subdocumentframe's view
+ }
+ }
+
+ nsIFrame* frame = view ? view->GetFrame() : nullptr;
+ if (frame && !frame->IsVisibleConsideringAncestors(
+ nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
+ return NS_OK;
+ }
+
+ docShell = parentItem;
+ parentItem = docShell->GetInProcessParentDocshell();
+ }
+
+ nsCOMPtr<nsIBaseWindow> treeOwnerAsWin(do_QueryInterface(mTreeOwner));
+ if (!treeOwnerAsWin) {
+ *aVisibility = true;
+ return NS_OK;
+ }
+
+ // Check with the tree owner as well to give embedders a chance to
+ // expose visibility as well.
+ nsresult rv = treeOwnerAsWin->GetVisibility(aVisibility);
+ if (rv == NS_ERROR_NOT_IMPLEMENTED) {
+ // The tree owner had no opinion on our visibility.
+ *aVisibility = true;
+ return NS_OK;
+ }
+ return rv;
+}
+
+void nsDocShell::ActivenessMaybeChanged() {
+ const bool isActive = mBrowsingContext->IsActive();
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->ActivenessMaybeChanged();
+ }
+
+ // Tell the window about it
+ if (mScriptGlobal) {
+ mScriptGlobal->SetIsBackground(!isActive);
+ if (RefPtr<Document> doc = mScriptGlobal->GetExtantDoc()) {
+ // Update orientation when the top-level browsing context becomes active.
+ if (isActive && mBrowsingContext->IsTop()) {
+ // We only care about the top-level browsing context.
+ auto orientation = mBrowsingContext->GetOrientationLock();
+ ScreenOrientation::UpdateActiveOrientationLock(orientation);
+ }
+
+ doc->PostVisibilityUpdateEvent();
+ }
+ }
+
+ // Tell the nsDOMNavigationTiming about it
+ RefPtr<nsDOMNavigationTiming> timing = mTiming;
+ if (!timing && mDocumentViewer) {
+ if (Document* doc = mDocumentViewer->GetDocument()) {
+ timing = doc->GetNavigationTiming();
+ }
+ }
+ if (timing) {
+ timing->NotifyDocShellStateChanged(
+ isActive ? nsDOMNavigationTiming::DocShellState::eActive
+ : nsDOMNavigationTiming::DocShellState::eInactive);
+ }
+
+ // Restart or stop meta refresh timers if necessary
+ if (mDisableMetaRefreshWhenInactive) {
+ if (isActive) {
+ ResumeRefreshURIs();
+ } else {
+ SuspendRefreshURIs();
+ }
+ }
+
+ if (InputTaskManager::CanSuspendInputEvent()) {
+ mBrowsingContext->Group()->UpdateInputTaskManagerIfNeeded(isActive);
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::SetDefaultLoadFlags(uint32_t aDefaultLoadFlags) {
+ if (!mWillChangeProcess) {
+ // Intentionally ignoring handling discarded browsing contexts.
+ Unused << mBrowsingContext->SetDefaultLoadFlags(aDefaultLoadFlags);
+ } else {
+ // Bug 1623565: DevTools tries to clean up defaultLoadFlags on
+ // shutdown. Sorry DevTools, your DocShell is in another process.
+ NS_WARNING("nsDocShell::SetDefaultLoadFlags called on Zombie DocShell");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDefaultLoadFlags(uint32_t* aDefaultLoadFlags) {
+ *aDefaultLoadFlags = mBrowsingContext->GetDefaultLoadFlags();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetFailedChannel(nsIChannel** aFailedChannel) {
+ NS_ENSURE_ARG_POINTER(aFailedChannel);
+ Document* doc = GetDocument();
+ if (!doc) {
+ *aFailedChannel = nullptr;
+ return NS_OK;
+ }
+ NS_IF_ADDREF(*aFailedChannel = doc->GetFailedChannel());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetVisibility(bool aVisibility) {
+ // Show()/Hide() may change mDocumentViewer.
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ if (!viewer) {
+ return NS_OK;
+ }
+ if (aVisibility) {
+ viewer->Show();
+ } else {
+ viewer->Hide();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetEnabled(bool* aEnabled) {
+ NS_ENSURE_ARG_POINTER(aEnabled);
+ *aEnabled = true;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetEnabled(bool aEnabled) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+nsDocShell::GetMainWidget(nsIWidget** aMainWidget) {
+ // We don't create our own widget, so simply return the parent one.
+ return GetParentWidget(aMainWidget);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetTitle(nsAString& aTitle) {
+ aTitle = mTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetTitle(const nsAString& aTitle) {
+ // Avoid unnecessary updates of the title if the URI and the title haven't
+ // changed.
+ if (mTitleValidForCurrentURI && mTitle == aTitle) {
+ return NS_OK;
+ }
+
+ // Store local title
+ mTitle = aTitle;
+ mTitleValidForCurrentURI = true;
+
+ // When title is set on the top object it should then be passed to the
+ // tree owner.
+ if (mBrowsingContext->IsTop()) {
+ nsCOMPtr<nsIBaseWindow> treeOwnerAsWin(do_QueryInterface(mTreeOwner));
+ if (treeOwnerAsWin) {
+ treeOwnerAsWin->SetTitle(aTitle);
+ }
+ }
+
+ if (mCurrentURI && mLoadType != LOAD_ERROR_PAGE) {
+ UpdateGlobalHistoryTitle(mCurrentURI);
+ }
+
+ // Update SessionHistory with the document's title.
+ if (mLoadType != LOAD_BYPASS_HISTORY && mLoadType != LOAD_ERROR_PAGE) {
+ SetTitleOnHistoryEntry(true);
+ }
+
+ return NS_OK;
+}
+
+void nsDocShell::SetTitleOnHistoryEntry(bool aUpdateEntryInSessionHistory) {
+ if (mOSHE) {
+ mOSHE->SetTitle(mTitle);
+ }
+
+ if (mActiveEntry && mBrowsingContext) {
+ mActiveEntry->SetTitle(mTitle);
+ if (aUpdateEntryInSessionHistory) {
+ if (XRE_IsParentProcess()) {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ entry->SetTitle(mTitle);
+ }
+ } else {
+ mozilla::Unused
+ << ContentChild::GetSingleton()->SendSessionHistoryEntryTitle(
+ mBrowsingContext, mTitle);
+ }
+ }
+ }
+}
+
+nsPoint nsDocShell::GetCurScrollPos() {
+ nsPoint scrollPos;
+ if (nsIScrollableFrame* sf = GetRootScrollFrame()) {
+ scrollPos = sf->GetVisualViewportOffset();
+ }
+ return scrollPos;
+}
+
+nsresult nsDocShell::SetCurScrollPosEx(int32_t aCurHorizontalPos,
+ int32_t aCurVerticalPos) {
+ nsIScrollableFrame* sf = GetRootScrollFrame();
+ NS_ENSURE_TRUE(sf, NS_ERROR_FAILURE);
+
+ ScrollMode scrollMode =
+ sf->IsSmoothScroll() ? ScrollMode::SmoothMsd : ScrollMode::Instant;
+
+ nsPoint targetPos(aCurHorizontalPos, aCurVerticalPos);
+ sf->ScrollTo(targetPos, scrollMode);
+
+ // Set the visual viewport offset as well.
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ // Only the root content document can have a distinct visual viewport offset.
+ if (!presContext->IsRootContentDocumentCrossProcess()) {
+ return NS_OK;
+ }
+
+ // Not on a platform with a distinct visual viewport - don't bother setting
+ // the visual viewport offset.
+ if (!presShell->IsVisualViewportSizeSet()) {
+ return NS_OK;
+ }
+
+ presShell->ScrollToVisual(targetPos, layers::FrameMetrics::eMainThread,
+ scrollMode);
+
+ return NS_OK;
+}
+
+void nsDocShell::SetScrollbarPreference(mozilla::ScrollbarPreference aPref) {
+ if (mScrollbarPref == aPref) {
+ return;
+ }
+ mScrollbarPref = aPref;
+ auto* ps = GetPresShell();
+ if (!ps) {
+ return;
+ }
+ nsIFrame* scrollFrame = ps->GetRootScrollFrame();
+ if (!scrollFrame) {
+ return;
+ }
+ ps->FrameNeedsReflow(scrollFrame,
+ IntrinsicDirty::FrameAncestorsAndDescendants,
+ NS_FRAME_IS_DIRTY);
+}
+
+//*****************************************************************************
+// nsDocShell::nsIRefreshURI
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::RefreshURI(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ uint32_t aDelay) {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ NS_ENSURE_ARG(aURI);
+
+ /* Check if Meta refresh/redirects are permitted. Some
+ * embedded applications may not want to do this.
+ * Must do this before sending out NOTIFY_REFRESH events
+ * because listeners may have side effects (e.g. displaying a
+ * button to manually trigger the refresh later).
+ */
+ bool allowRedirects = true;
+ GetAllowMetaRedirects(&allowRedirects);
+ if (!allowRedirects) {
+ return NS_OK;
+ }
+
+ // If any web progress listeners are listening for NOTIFY_REFRESH events,
+ // give them a chance to block this refresh.
+ bool sameURI;
+ nsresult rv = aURI->Equals(mCurrentURI, &sameURI);
+ if (NS_FAILED(rv)) {
+ sameURI = false;
+ }
+ if (!RefreshAttempted(this, aURI, aDelay, sameURI)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITimerCallback> refreshTimer =
+ new nsRefreshTimer(this, aURI, aPrincipal, aDelay);
+
+ BusyFlags busyFlags = GetBusyFlags();
+
+ if (!mRefreshURIList) {
+ mRefreshURIList = nsArray::Create();
+ }
+
+ if (busyFlags & BUSY_FLAGS_BUSY ||
+ (!mBrowsingContext->IsActive() && mDisableMetaRefreshWhenInactive)) {
+ // We don't want to create the timer right now. Instead queue up the
+ // request and trigger the timer in EndPageLoad() or whenever we become
+ // active.
+ mRefreshURIList->AppendElement(refreshTimer);
+ } else {
+ // There is no page loading going on right now. Create the
+ // timer and fire it right away.
+ nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow();
+ NS_ENSURE_TRUE(win, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsITimer> timer;
+ MOZ_TRY_VAR(timer, NS_NewTimerWithCallback(refreshTimer, aDelay,
+ nsITimer::TYPE_ONE_SHOT));
+
+ mRefreshURIList->AppendElement(timer); // owning timer ref
+ }
+ return NS_OK;
+}
+
+nsresult nsDocShell::ForceRefreshURIFromTimer(nsIURI* aURI,
+ nsIPrincipal* aPrincipal,
+ uint32_t aDelay,
+ nsITimer* aTimer) {
+ MOZ_ASSERT(aTimer, "Must have a timer here");
+
+ // Remove aTimer from mRefreshURIList if needed
+ if (mRefreshURIList) {
+ uint32_t n = 0;
+ mRefreshURIList->GetLength(&n);
+
+ for (uint32_t i = 0; i < n; ++i) {
+ nsCOMPtr<nsITimer> timer = do_QueryElementAt(mRefreshURIList, i);
+ if (timer == aTimer) {
+ mRefreshURIList->RemoveElementAt(i);
+ break;
+ }
+ }
+ }
+
+ return ForceRefreshURI(aURI, aPrincipal, aDelay);
+}
+
+NS_IMETHODIMP
+nsDocShell::ForceRefreshURI(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ uint32_t aDelay) {
+ NS_ENSURE_ARG(aURI);
+
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI);
+ loadState->SetOriginalURI(mCurrentURI);
+ loadState->SetResultPrincipalURI(aURI);
+ loadState->SetResultPrincipalURIIsSome(true);
+ loadState->SetKeepResultPrincipalURIIfSet(true);
+ loadState->SetIsMetaRefresh(true);
+
+ // Set the triggering pricipal to aPrincipal if available, or current
+ // document's principal otherwise.
+ nsCOMPtr<nsIPrincipal> principal = aPrincipal;
+ RefPtr<Document> doc = GetDocument();
+ if (!principal) {
+ if (!doc) {
+ return NS_ERROR_FAILURE;
+ }
+ principal = doc->NodePrincipal();
+ }
+ loadState->SetTriggeringPrincipal(principal);
+ if (doc) {
+ loadState->SetCsp(doc->GetCsp());
+ loadState->SetHasValidUserGestureActivation(
+ doc->HasValidTransientUserGestureActivation());
+ loadState->SetTriggeringSandboxFlags(doc->GetSandboxFlags());
+ loadState->SetTriggeringWindowId(doc->InnerWindowID());
+ loadState->SetTriggeringStorageAccess(doc->UsingStorageAccess());
+ }
+
+ loadState->SetPrincipalIsExplicit(true);
+
+ /* Check if this META refresh causes a redirection
+ * to another site.
+ */
+ bool equalUri = false;
+ nsresult rv = aURI->Equals(mCurrentURI, &equalUri);
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ if (NS_SUCCEEDED(rv) && !equalUri && aDelay <= REFRESH_REDIRECT_TIMER) {
+ /* It is a META refresh based redirection within the threshold time
+ * we have in mind (15000 ms as defined by REFRESH_REDIRECT_TIMER).
+ * Pass a REPLACE flag to LoadURI().
+ */
+ loadState->SetLoadType(LOAD_REFRESH_REPLACE);
+
+ /* For redirects we mimic HTTP, which passes the
+ * original referrer.
+ * We will pass in referrer but will not send to server
+ */
+ if (mReferrerInfo) {
+ referrerInfo = static_cast<ReferrerInfo*>(mReferrerInfo.get())
+ ->CloneWithNewSendReferrer(false);
+ }
+ } else {
+ loadState->SetLoadType(LOAD_REFRESH);
+ /* We do need to pass in a referrer, but we don't want it to
+ * be sent to the server.
+ * For most refreshes the current URI is an appropriate
+ * internal referrer.
+ */
+ referrerInfo = new ReferrerInfo(mCurrentURI, ReferrerPolicy::_empty, false);
+ }
+
+ loadState->SetReferrerInfo(referrerInfo);
+ loadState->SetLoadFlags(
+ nsIWebNavigation::LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL);
+ loadState->SetFirstParty(true);
+
+ /*
+ * LoadURI(...) will cancel all refresh timers... This causes the
+ * Timer and its refreshData instance to be released...
+ */
+ LoadURI(loadState, false);
+
+ return NS_OK;
+}
+
+static const char16_t* SkipASCIIWhitespace(const char16_t* aStart,
+ const char16_t* aEnd) {
+ const char16_t* iter = aStart;
+ while (iter != aEnd && mozilla::IsAsciiWhitespace(*iter)) {
+ ++iter;
+ }
+ return iter;
+}
+
+static std::tuple<const char16_t*, const char16_t*> ExtractURLString(
+ const char16_t* aPosition, const char16_t* aEnd) {
+ MOZ_ASSERT(aPosition != aEnd);
+
+ // 1. Let urlString be the substring of input from the code point at
+ // position to the end of the string.
+ const char16_t* urlStart = aPosition;
+ const char16_t* urlEnd = aEnd;
+
+ // 2. If the code point in input pointed to by position is U+0055 (U) or
+ // U+0075 (u), then advance position to the next code point.
+ // Otherwise, jump to the step labeled skip quotes.
+ if (*aPosition == 'U' || *aPosition == 'u') {
+ ++aPosition;
+
+ // 3. If the code point in input pointed to by position is U+0052 (R) or
+ // U+0072 (r), then advance position to the next code point.
+ // Otherwise, jump to the step labeled parse.
+ if (aPosition == aEnd || (*aPosition != 'R' && *aPosition != 'r')) {
+ return std::make_tuple(urlStart, urlEnd);
+ }
+
+ ++aPosition;
+
+ // 4. If the code point in input pointed to by position is U+004C (L) or
+ // U+006C (l), then advance position to the next code point.
+ // Otherwise, jump to the step labeled parse.
+ if (aPosition == aEnd || (*aPosition != 'L' && *aPosition != 'l')) {
+ return std::make_tuple(urlStart, urlEnd);
+ }
+
+ ++aPosition;
+
+ // 5. Skip ASCII whitespace within input given position.
+ aPosition = SkipASCIIWhitespace(aPosition, aEnd);
+
+ // 6. If the code point in input pointed to by position is U+003D (=),
+ // then advance position to the next code point. Otherwise, jump to
+ // the step labeled parse.
+ if (aPosition == aEnd || *aPosition != '=') {
+ return std::make_tuple(urlStart, urlEnd);
+ }
+
+ ++aPosition;
+
+ // 7. Skip ASCII whitespace within input given position.
+ aPosition = SkipASCIIWhitespace(aPosition, aEnd);
+ }
+
+ // 8. Skip quotes: If the code point in input pointed to by position is
+ // U+0027 (') or U+0022 ("), then let quote be that code point, and
+ // advance position to the next code point. Otherwise, let quote be
+ // the empty string.
+ Maybe<char> quote;
+ if (aPosition != aEnd && (*aPosition == '\'' || *aPosition == '"')) {
+ quote.emplace(*aPosition);
+ ++aPosition;
+ }
+
+ // 9. Set urlString to the substring of input from the code point at
+ // position to the end of the string.
+ urlStart = aPosition;
+ urlEnd = aEnd;
+
+ // 10. If quote is not the empty string, and there is a code point in
+ // urlString equal to quote, then truncate urlString at that code
+ // point, so that it and all subsequent code points are removed.
+ const char16_t* quotePos;
+ if (quote.isSome() &&
+ (quotePos = nsCharTraits<char16_t>::find(
+ urlStart, std::distance(urlStart, aEnd), quote.value()))) {
+ urlEnd = quotePos;
+ }
+
+ return std::make_tuple(urlStart, urlEnd);
+}
+
+void nsDocShell::SetupRefreshURIFromHeader(Document* aDocument,
+ const nsAString& aHeader) {
+ if (mIsBeingDestroyed) {
+ return;
+ }
+
+ const char16_t* position = aHeader.BeginReading();
+ const char16_t* end = aHeader.EndReading();
+
+ // See
+ // https://html.spec.whatwg.org/#pragma-directives:shared-declarative-refresh-steps.
+
+ // 3. Skip ASCII whitespace
+ position = SkipASCIIWhitespace(position, end);
+
+ // 4. Let time be 0.
+ CheckedInt<uint32_t> milliSeconds;
+
+ // 5. Collect a sequence of code points that are ASCII digits
+ const char16_t* digitsStart = position;
+ while (position != end && mozilla::IsAsciiDigit(*position)) {
+ ++position;
+ }
+
+ if (position == digitsStart) {
+ // 6. If timeString is the empty string, then:
+ // 1. If the code point in input pointed to by position is not U+002E
+ // (.), then return.
+ if (position == end || *position != '.') {
+ return;
+ }
+ } else {
+ // 7. Otherwise, set time to the result of parsing timeString using the
+ // rules for parsing non-negative integers.
+ nsContentUtils::ParseHTMLIntegerResultFlags result;
+ uint32_t seconds =
+ nsContentUtils::ParseHTMLInteger(digitsStart, position, &result);
+ MOZ_ASSERT(!(result & nsContentUtils::eParseHTMLInteger_Negative));
+ if (result & nsContentUtils::eParseHTMLInteger_Error) {
+ // The spec assumes no errors here (since we only pass ASCII digits in),
+ // but we can still overflow, so this block should deal with that (and
+ // only that).
+ MOZ_ASSERT(!(result & nsContentUtils::eParseHTMLInteger_ErrorOverflow));
+ return;
+ }
+ MOZ_ASSERT(
+ !(result & nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput));
+
+ milliSeconds = seconds;
+ milliSeconds *= 1000;
+ if (!milliSeconds.isValid()) {
+ return;
+ }
+ }
+
+ // 8. Collect a sequence of code points that are ASCII digits and U+002E FULL
+ // STOP characters (.) from input given position. Ignore any collected
+ // characters.
+ while (position != end &&
+ (mozilla::IsAsciiDigit(*position) || *position == '.')) {
+ ++position;
+ }
+
+ // 9. Let urlRecord be document's URL.
+ nsCOMPtr<nsIURI> urlRecord(aDocument->GetDocumentURI());
+
+ // 10. If position is not past the end of input
+ if (position != end) {
+ // 1. If the code point in input pointed to by position is not U+003B (;),
+ // U+002C (,), or ASCII whitespace, then return.
+ if (*position != ';' && *position != ',' &&
+ !mozilla::IsAsciiWhitespace(*position)) {
+ return;
+ }
+
+ // 2. Skip ASCII whitespace within input given position.
+ position = SkipASCIIWhitespace(position, end);
+
+ // 3. If the code point in input pointed to by position is U+003B (;) or
+ // U+002C (,), then advance position to the next code point.
+ if (position != end && (*position == ';' || *position == ',')) {
+ ++position;
+
+ // 4. Skip ASCII whitespace within input given position.
+ position = SkipASCIIWhitespace(position, end);
+ }
+
+ // 11. If position is not past the end of input, then:
+ if (position != end) {
+ const char16_t* urlStart;
+ const char16_t* urlEnd;
+
+ // 1-10. See ExtractURLString.
+ std::tie(urlStart, urlEnd) = ExtractURLString(position, end);
+
+ // 11. Parse: Parse urlString relative to document. If that fails, return.
+ // Otherwise, set urlRecord to the resulting URL record.
+ nsresult rv =
+ NS_NewURI(getter_AddRefs(urlRecord),
+ Substring(urlStart, std::distance(urlStart, urlEnd)),
+ /* charset = */ nullptr, aDocument->GetDocBaseURI());
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ }
+
+ nsIPrincipal* principal = aDocument->NodePrincipal();
+ nsCOMPtr<nsIScriptSecurityManager> securityManager =
+ nsContentUtils::GetSecurityManager();
+ nsresult rv = securityManager->CheckLoadURIWithPrincipal(
+ principal, urlRecord,
+ nsIScriptSecurityManager::LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT,
+ aDocument->InnerWindowID());
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ bool isjs = true;
+ rv = NS_URIChainHasFlags(
+ urlRecord, nsIProtocolHandler::URI_OPENING_EXECUTES_SCRIPT, &isjs);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (isjs) {
+ return;
+ }
+
+ RefreshURI(urlRecord, principal, milliSeconds.value());
+}
+
+static void DoCancelRefreshURITimers(nsIMutableArray* aTimerList) {
+ if (!aTimerList) {
+ return;
+ }
+
+ uint32_t n = 0;
+ aTimerList->GetLength(&n);
+
+ while (n) {
+ nsCOMPtr<nsITimer> timer(do_QueryElementAt(aTimerList, --n));
+
+ aTimerList->RemoveElementAt(n); // bye bye owning timer ref
+
+ if (timer) {
+ timer->Cancel();
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::CancelRefreshURITimers() {
+ DoCancelRefreshURITimers(mRefreshURIList);
+ DoCancelRefreshURITimers(mSavedRefreshURIList);
+ DoCancelRefreshURITimers(mBFCachedRefreshURIList);
+ mRefreshURIList = nullptr;
+ mSavedRefreshURIList = nullptr;
+ mBFCachedRefreshURIList = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetRefreshPending(bool* aResult) {
+ if (!mRefreshURIList) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ uint32_t count;
+ nsresult rv = mRefreshURIList->GetLength(&count);
+ if (NS_SUCCEEDED(rv)) {
+ *aResult = (count != 0);
+ }
+ return rv;
+}
+
+void nsDocShell::RefreshURIToQueue() {
+ if (mRefreshURIList) {
+ uint32_t n = 0;
+ mRefreshURIList->GetLength(&n);
+
+ for (uint32_t i = 0; i < n; ++i) {
+ nsCOMPtr<nsITimer> timer = do_QueryElementAt(mRefreshURIList, i);
+ if (!timer) {
+ continue; // this must be a nsRefreshURI already
+ }
+
+ // Replace this timer object with a nsRefreshTimer object.
+ nsCOMPtr<nsITimerCallback> callback;
+ timer->GetCallback(getter_AddRefs(callback));
+
+ timer->Cancel();
+
+ mRefreshURIList->ReplaceElementAt(callback, i);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::SuspendRefreshURIs() {
+ RefreshURIToQueue();
+
+ // Suspend refresh URIs for our child shells as well.
+ for (auto* child : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> shell = do_QueryObject(child);
+ if (shell) {
+ shell->SuspendRefreshURIs();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::ResumeRefreshURIs() {
+ RefreshURIFromQueue();
+
+ // Resume refresh URIs for our child shells as well.
+ for (auto* child : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> shell = do_QueryObject(child);
+ if (shell) {
+ shell->ResumeRefreshURIs();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::RefreshURIFromQueue() {
+ if (!mRefreshURIList) {
+ return NS_OK;
+ }
+ uint32_t n = 0;
+ mRefreshURIList->GetLength(&n);
+
+ while (n) {
+ nsCOMPtr<nsITimerCallback> refreshInfo =
+ do_QueryElementAt(mRefreshURIList, --n);
+
+ if (refreshInfo) {
+ // This is the nsRefreshTimer object, waiting to be
+ // setup in a timer object and fired.
+ // Create the timer and trigger it.
+ uint32_t delay = static_cast<nsRefreshTimer*>(
+ static_cast<nsITimerCallback*>(refreshInfo))
+ ->GetDelay();
+ nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow();
+ if (win) {
+ nsCOMPtr<nsITimer> timer;
+ NS_NewTimerWithCallback(getter_AddRefs(timer), refreshInfo, delay,
+ nsITimer::TYPE_ONE_SHOT);
+
+ if (timer) {
+ // Replace the nsRefreshTimer element in the queue with
+ // its corresponding timer object, so that in case another
+ // load comes through before the timer can go off, the timer will
+ // get cancelled in CancelRefreshURITimer()
+ mRefreshURIList->ReplaceElementAt(timer, n);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+static bool IsFollowupPartOfMultipart(nsIRequest* aRequest) {
+ nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
+ bool firstPart = false;
+ return multiPartChannel &&
+ NS_SUCCEEDED(multiPartChannel->GetIsFirstPart(&firstPart)) &&
+ !firstPart;
+}
+
+nsresult nsDocShell::Embed(nsIDocumentViewer* aDocumentViewer,
+ WindowGlobalChild* aWindowActor,
+ bool aIsTransientAboutBlank, bool aPersist,
+ nsIRequest* aRequest, nsIURI* aPreviousURI) {
+ // Save the LayoutHistoryState of the previous document, before
+ // setting up new document
+ PersistLayoutHistoryState();
+
+ nsresult rv = SetupNewViewer(aDocumentViewer, aWindowActor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXX What if SetupNewViewer fails?
+ if (mozilla::SessionHistoryInParent() ? !!mLoadingEntry : !!mLSHE) {
+ // Set history.state
+ SetDocCurrentStateObj(mLSHE,
+ mLoadingEntry ? &mLoadingEntry->mInfo : nullptr);
+ }
+
+ if (mLSHE) {
+ // Restore the editing state, if it's stored in session history.
+ if (mLSHE->HasDetachedEditor()) {
+ ReattachEditorToWindow(mLSHE);
+ }
+
+ SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE));
+ }
+
+ if (!aIsTransientAboutBlank && mozilla::SessionHistoryInParent() &&
+ !IsFollowupPartOfMultipart(aRequest)) {
+ bool expired = false;
+ uint32_t cacheKey = 0;
+ nsCOMPtr<nsICacheInfoChannel> cacheChannel = do_QueryInterface(aRequest);
+ if (cacheChannel) {
+ // Check if the page has expired from cache
+ uint32_t expTime = 0;
+ cacheChannel->GetCacheTokenExpirationTime(&expTime);
+ uint32_t now = PRTimeToSeconds(PR_Now());
+ if (expTime <= now) {
+ expired = true;
+ }
+
+ // The checks for updating cache key are similar to the old session
+ // history in OnNewURI. Try to update the cache key if
+ // - we should update session history and aren't doing a session
+ // history load.
+ // - we're doing a forced reload.
+ if (((!mLoadingEntry || !mLoadingEntry->mLoadIsFromSessionHistory) &&
+ mBrowsingContext->ShouldUpdateSessionHistory(mLoadType)) ||
+ IsForceReloadType(mLoadType)) {
+ cacheChannel->GetCacheKey(&cacheKey);
+ }
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("document %p Embed", this));
+ MoveLoadingToActiveEntry(aPersist, expired, cacheKey, aPreviousURI);
+ }
+
+ bool updateHistory = true;
+
+ // Determine if this type of load should update history
+ switch (mLoadType) {
+ case LOAD_NORMAL_REPLACE:
+ case LOAD_REFRESH_REPLACE:
+ case LOAD_STOP_CONTENT_AND_REPLACE:
+ case LOAD_RELOAD_BYPASS_CACHE:
+ case LOAD_RELOAD_BYPASS_PROXY:
+ case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE:
+ case LOAD_REPLACE_BYPASS_CACHE:
+ updateHistory = false;
+ break;
+ default:
+ break;
+ }
+
+ if (!updateHistory) {
+ SetLayoutHistoryState(nullptr);
+ }
+
+ return NS_OK;
+}
+
+//*****************************************************************************
+// nsDocShell::nsIWebProgressListener
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::OnProgressChange(nsIWebProgress* aProgress, nsIRequest* aRequest,
+ int32_t aCurSelfProgress, int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::OnStateChange(nsIWebProgress* aProgress, nsIRequest* aRequest,
+ uint32_t aStateFlags, nsresult aStatus) {
+ if ((~aStateFlags & (STATE_START | STATE_IS_NETWORK)) == 0) {
+ // Save timing statistics.
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsAutoCString aURI;
+ uri->GetAsciiSpec(aURI);
+
+ if (this == aProgress) {
+ mozilla::Unused << MaybeInitTiming();
+ mTiming->NotifyFetchStart(uri,
+ ConvertLoadTypeToNavigationType(mLoadType));
+ // If we are starting a DocumentChannel, we need to pass the timing
+ // statistics so that should a process switch occur, the starting type can
+ // be passed to the new DocShell running in the other content process.
+ if (RefPtr<DocumentChannel> docChannel = do_QueryObject(aRequest)) {
+ docChannel->SetNavigationTiming(mTiming);
+ }
+ }
+
+ // Page has begun to load
+ mBusyFlags = (BusyFlags)(BUSY_FLAGS_BUSY | BUSY_FLAGS_BEFORE_PAGE_LOAD);
+
+ if ((aStateFlags & STATE_RESTORING) == 0) {
+ if (StaticPrefs::browser_sessionstore_platform_collection_AtStartup()) {
+ if (IsForceReloadType(mLoadType)) {
+ if (WindowContext* windowContext =
+ mBrowsingContext->GetCurrentWindowContext()) {
+ SessionStoreChild::From(windowContext->GetWindowGlobalChild())
+ ->ResetSessionStore(mBrowsingContext,
+ mBrowsingContext->GetSessionStoreEpoch());
+ }
+ }
+ }
+ }
+ } else if ((~aStateFlags & (STATE_TRANSFERRING | STATE_IS_DOCUMENT)) == 0) {
+ // Page is loading
+ mBusyFlags = (BusyFlags)(BUSY_FLAGS_BUSY | BUSY_FLAGS_PAGE_LOADING);
+ } else if ((aStateFlags & STATE_STOP) && (aStateFlags & STATE_IS_NETWORK)) {
+ // Page has finished loading
+ mBusyFlags = BUSY_FLAGS_NONE;
+ }
+
+ if ((~aStateFlags & (STATE_IS_DOCUMENT | STATE_STOP)) == 0) {
+ nsCOMPtr<nsIWebProgress> webProgress =
+ do_QueryInterface(GetAsSupports(this));
+ // Is the document stop notification for this document?
+ if (aProgress == webProgress.get()) {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ EndPageLoad(aProgress, channel, aStatus);
+ }
+ }
+ // note that redirect state changes will go through here as well, but it
+ // is better to handle those in OnRedirectStateChange where more
+ // information is available.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::OnLocationChange(nsIWebProgress* aProgress, nsIRequest* aRequest,
+ nsIURI* aURI, uint32_t aFlags) {
+ // Since we've now changed Documents, notify the BrowsingContext that we've
+ // changed. Ideally we'd just let the BrowsingContext do this when it
+ // changes the current window global, but that happens before this and we
+ // have a lot of tests that depend on the specific ordering of messages.
+ bool isTopLevel = false;
+ if (XRE_IsParentProcess() &&
+ !(aFlags & nsIWebProgressListener::LOCATION_CHANGE_SAME_DOCUMENT) &&
+ NS_SUCCEEDED(aProgress->GetIsTopLevel(&isTopLevel)) && isTopLevel) {
+ GetBrowsingContext()->Canonical()->UpdateSecurityState();
+ }
+ return NS_OK;
+}
+
+void nsDocShell::OnRedirectStateChange(nsIChannel* aOldChannel,
+ nsIChannel* aNewChannel,
+ uint32_t aRedirectFlags,
+ uint32_t aStateFlags) {
+ NS_ASSERTION(aStateFlags & STATE_REDIRECTING,
+ "Calling OnRedirectStateChange when there is no redirect");
+
+ if (!(aStateFlags & STATE_IS_DOCUMENT)) {
+ return; // not a toplevel document
+ }
+
+ nsCOMPtr<nsIURI> oldURI, newURI;
+ aOldChannel->GetURI(getter_AddRefs(oldURI));
+ aNewChannel->GetURI(getter_AddRefs(newURI));
+ if (!oldURI || !newURI) {
+ return;
+ }
+
+ // DocumentChannel adds redirect chain to global history in the parent
+ // process. The redirect chain can't be queried from the content process, so
+ // there's no need to update global history here.
+ RefPtr<DocumentChannel> docChannel = do_QueryObject(aOldChannel);
+ if (!docChannel) {
+ // Below a URI visit is saved (see AddURIVisit method doc).
+ // The visit chain looks something like:
+ // ...
+ // Site N - 1
+ // => Site N
+ // (redirect to =>) Site N + 1 (we are here!)
+
+ // Get N - 1 and transition type
+ nsCOMPtr<nsIURI> previousURI;
+ uint32_t previousFlags = 0;
+ ExtractLastVisit(aOldChannel, getter_AddRefs(previousURI), &previousFlags);
+
+ if (aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL ||
+ net::ChannelIsPost(aOldChannel)) {
+ // 1. Internal redirects are ignored because they are specific to the
+ // channel implementation.
+ // 2. POSTs are not saved by global history.
+ //
+ // Regardless, we need to propagate the previous visit to the new
+ // channel.
+ SaveLastVisit(aNewChannel, previousURI, previousFlags);
+ } else {
+ // Get the HTTP response code, if available.
+ uint32_t responseStatus = 0;
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aOldChannel);
+ if (httpChannel) {
+ Unused << httpChannel->GetResponseStatus(&responseStatus);
+ }
+
+ // Add visit N -1 => N
+ AddURIVisit(oldURI, previousURI, previousFlags, responseStatus);
+
+ // Since N + 1 could be the final destination, we will not save N => N + 1
+ // here. OnNewURI will do that, so we will cache it.
+ SaveLastVisit(aNewChannel, oldURI, aRedirectFlags);
+ }
+ }
+
+ if (!(aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) &&
+ mLoadType & (LOAD_CMD_RELOAD | LOAD_CMD_HISTORY)) {
+ mLoadType = LOAD_NORMAL_REPLACE;
+ SetHistoryEntryAndUpdateBC(Some(nullptr), Nothing());
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ nsresult aStatus, const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+already_AddRefed<nsIURIFixupInfo> nsDocShell::KeywordToURI(
+ const nsACString& aKeyword, bool aIsPrivateContext) {
+ nsCOMPtr<nsIURIFixupInfo> info;
+ if (!XRE_IsContentProcess()) {
+ nsCOMPtr<nsIURIFixup> uriFixup = components::URIFixup::Service();
+ if (uriFixup) {
+ uriFixup->KeywordToURI(aKeyword, aIsPrivateContext, getter_AddRefs(info));
+ }
+ }
+ return info.forget();
+}
+
+/* static */
+already_AddRefed<nsIURI> nsDocShell::MaybeFixBadCertDomainErrorURI(
+ nsIChannel* aChannel, nsIURI* aUrl) {
+ if (!aChannel) {
+ return nullptr;
+ }
+
+ nsresult rv = NS_OK;
+ nsAutoCString host;
+ rv = aUrl->GetAsciiHost(host);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // No point in going further if "www." is included in the hostname
+ // already. That is the only hueristic we're applying in this function.
+ if (StringBeginsWith(host, "www."_ns)) {
+ return nullptr;
+ }
+
+ // Return if fixup enable pref is turned off.
+ if (!mozilla::StaticPrefs::security_bad_cert_domain_error_url_fix_enabled()) {
+ return nullptr;
+ }
+
+ // Return if scheme is not HTTPS.
+ if (!SchemeIsHTTPS(aUrl)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsILoadInfo> info = aChannel->LoadInfo();
+ if (!info) {
+ return nullptr;
+ }
+
+ // Skip doing the fixup if our channel was redirected, because we
+ // shouldn't be guessing things about the post-redirect URI.
+ if (!info->RedirectChain().IsEmpty()) {
+ return nullptr;
+ }
+
+ int32_t port = 0;
+ rv = aUrl->GetPort(&port);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // Don't fix up hosts with ports.
+ if (port != -1) {
+ return nullptr;
+ }
+
+ // Don't fix up localhost url.
+ if (host == "localhost") {
+ return nullptr;
+ }
+
+ // Don't fix up hostnames with IP address.
+ if (net_IsValidIPv4Addr(host) || net_IsValidIPv6Addr(host)) {
+ return nullptr;
+ }
+
+ nsAutoCString userPass;
+ rv = aUrl->GetUserPass(userPass);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // Security - URLs with user / password info should NOT be modified.
+ if (!userPass.IsEmpty()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsITransportSecurityInfo> tsi;
+ rv = aChannel->GetSecurityInfo(getter_AddRefs(tsi));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!tsi)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIX509Cert> cert;
+ rv = tsi->GetServerCert(getter_AddRefs(cert));
+ if (NS_WARN_IF(NS_FAILED(rv) || !cert)) {
+ return nullptr;
+ }
+
+ nsTArray<uint8_t> certBytes;
+ rv = cert->GetRawDER(certBytes);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ mozilla::pkix::Input serverCertInput;
+ mozilla::pkix::Result rv1 =
+ serverCertInput.Init(certBytes.Elements(), certBytes.Length());
+ if (rv1 != mozilla::pkix::Success) {
+ return nullptr;
+ }
+
+ nsAutoCString newHost("www."_ns);
+ newHost.Append(host);
+
+ mozilla::pkix::Input newHostInput;
+ rv1 = newHostInput.Init(
+ BitwiseCast<const uint8_t*, const char*>(newHost.BeginReading()),
+ newHost.Length());
+ if (rv1 != mozilla::pkix::Success) {
+ return nullptr;
+ }
+
+ // Check if adding a "www." prefix to the request's hostname will
+ // cause the response's certificate to match.
+ rv1 = mozilla::pkix::CheckCertHostname(serverCertInput, newHostInput);
+ if (rv1 != mozilla::pkix::Success) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> newURI;
+ Unused << NS_MutateURI(aUrl).SetHost(newHost).Finalize(
+ getter_AddRefs(newURI));
+
+ return newURI.forget();
+}
+
+/* static */
+already_AddRefed<nsIURI> nsDocShell::AttemptURIFixup(
+ nsIChannel* aChannel, nsresult aStatus,
+ const mozilla::Maybe<nsCString>& aOriginalURIString, uint32_t aLoadType,
+ bool aIsTopFrame, bool aAllowKeywordFixup, bool aUsePrivateBrowsing,
+ bool aNotifyKeywordSearchLoading, nsIInputStream** aNewPostData,
+ bool* outWasSchemelessInput) {
+ if (aStatus != NS_ERROR_UNKNOWN_HOST && aStatus != NS_ERROR_NET_RESET &&
+ aStatus != NS_ERROR_CONNECTION_REFUSED &&
+ aStatus !=
+ mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN)) {
+ return nullptr;
+ }
+
+ if (!(aLoadType == LOAD_NORMAL && aIsTopFrame) && !aAllowKeywordFixup) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> url;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(url));
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ //
+ // Try and make an alternative URI from the old one
+ //
+ nsCOMPtr<nsIURI> newURI;
+ nsCOMPtr<nsIInputStream> newPostData;
+
+ nsAutoCString oldSpec;
+ url->GetSpec(oldSpec);
+
+ //
+ // First try keyword fixup
+ //
+ nsAutoString keywordProviderName, keywordAsSent;
+ if (aStatus == NS_ERROR_UNKNOWN_HOST && aAllowKeywordFixup) {
+ // we should only perform a keyword search under the following
+ // conditions:
+ // (0) Pref keyword.enabled is true
+ // (1) the url scheme is http (or https)
+ // (2) the url does not have a protocol scheme
+ // If we don't enforce such a policy, then we end up doing
+ // keyword searchs on urls we don't intend like imap, file,
+ // mailbox, etc. This could lead to a security problem where we
+ // send data to the keyword server that we shouldn't be.
+ // Someone needs to clean up keywords in general so we can
+ // determine on a per url basis if we want keywords
+ // enabled...this is just a bandaid...
+ nsAutoCString scheme;
+ Unused << url->GetScheme(scheme);
+ if (Preferences::GetBool("keyword.enabled", false) &&
+ StringBeginsWith(scheme, "http"_ns)) {
+ bool attemptFixup = false;
+ nsAutoCString host;
+ Unused << url->GetHost(host);
+ if (host.FindChar('.') == kNotFound) {
+ attemptFixup = true;
+ } else {
+ // For domains with dots, we check the public suffix validity.
+ nsCOMPtr<nsIEffectiveTLDService> tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+ if (tldService) {
+ nsAutoCString suffix;
+ attemptFixup =
+ NS_SUCCEEDED(tldService->GetKnownPublicSuffix(url, suffix)) &&
+ suffix.IsEmpty();
+ }
+ }
+ if (attemptFixup) {
+ nsCOMPtr<nsIURIFixupInfo> info;
+ // only send non-qualified hosts to the keyword server
+ if (aOriginalURIString && !aOriginalURIString->IsEmpty()) {
+ info = KeywordToURI(*aOriginalURIString, aUsePrivateBrowsing);
+ } else {
+ //
+ // If this string was passed through nsStandardURL by
+ // chance, then it may have been converted from UTF-8 to
+ // ACE, which would result in a completely bogus keyword
+ // query. Here we try to recover the original Unicode
+ // value, but this is not 100% correct since the value may
+ // have been normalized per the IDN normalization rules.
+ //
+ // Since we don't have access to the exact original string
+ // that was entered by the user, this will just have to do.
+ bool isACE;
+ nsAutoCString utf8Host;
+ nsCOMPtr<nsIIDNService> idnSrv =
+ do_GetService(NS_IDNSERVICE_CONTRACTID);
+ if (idnSrv && NS_SUCCEEDED(idnSrv->IsACE(host, &isACE)) && isACE &&
+ NS_SUCCEEDED(idnSrv->ConvertACEtoUTF8(host, utf8Host))) {
+ info = KeywordToURI(utf8Host, aUsePrivateBrowsing);
+
+ } else {
+ info = KeywordToURI(host, aUsePrivateBrowsing);
+ }
+ }
+ if (info) {
+ info->GetPreferredURI(getter_AddRefs(newURI));
+ info->GetWasSchemelessInput(outWasSchemelessInput);
+ if (newURI) {
+ info->GetKeywordAsSent(keywordAsSent);
+ info->GetKeywordProviderName(keywordProviderName);
+ info->GetPostData(getter_AddRefs(newPostData));
+ }
+ }
+ }
+ }
+ }
+
+ //
+ // Now try change the address, e.g. turn http://foo into
+ // http://www.foo.com, and if that doesn't work try https with
+ // https://foo and https://www.foo.com.
+ //
+ if (aStatus == NS_ERROR_UNKNOWN_HOST || aStatus == NS_ERROR_NET_RESET) {
+ // Skip fixup for anything except a normal document load
+ // operation on the topframe.
+ bool doCreateAlternate = aLoadType == LOAD_NORMAL && aIsTopFrame;
+
+ if (doCreateAlternate) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsIPrincipal* principal = loadInfo->TriggeringPrincipal();
+ // Only do this if our channel was loaded directly by the user from the
+ // URL bar or similar (system principal) and not redirected, because we
+ // shouldn't be guessing things about links from other sites, or a
+ // post-redirect URI.
+ doCreateAlternate = principal && principal->IsSystemPrincipal() &&
+ loadInfo->RedirectChain().IsEmpty();
+ }
+ // Test if keyword lookup produced a new URI or not
+ if (doCreateAlternate && newURI) {
+ bool sameURI = false;
+ url->Equals(newURI, &sameURI);
+ if (!sameURI) {
+ // Keyword lookup made a new URI so no need to try
+ // an alternate one.
+ doCreateAlternate = false;
+ }
+ }
+ if (doCreateAlternate) {
+ newURI = nullptr;
+ newPostData = nullptr;
+ keywordProviderName.Truncate();
+ keywordAsSent.Truncate();
+ nsCOMPtr<nsIURIFixup> uriFixup = components::URIFixup::Service();
+ if (uriFixup) {
+ nsCOMPtr<nsIURIFixupInfo> fixupInfo;
+ uriFixup->GetFixupURIInfo(oldSpec, nsIURIFixup::FIXUP_FLAG_NONE,
+ getter_AddRefs(fixupInfo));
+ if (fixupInfo) {
+ fixupInfo->GetPreferredURI(getter_AddRefs(newURI));
+ }
+ }
+ }
+ } else if (aStatus == NS_ERROR_CONNECTION_REFUSED &&
+ Preferences::GetBool("browser.fixup.fallback-to-https", false)) {
+ // Try HTTPS, since http didn't work
+ if (SchemeIsHTTP(url)) {
+ int32_t port = 0;
+ url->GetPort(&port);
+
+ // Fall back to HTTPS only if port is default
+ if (port == -1) {
+ newURI = nullptr;
+ newPostData = nullptr;
+ Unused << NS_MutateURI(url)
+ .SetScheme("https"_ns)
+ .Finalize(getter_AddRefs(newURI));
+ }
+ }
+ }
+
+ // If we have a SSL_ERROR_BAD_CERT_DOMAIN error, try prefixing the domain name
+ // with www. to see if we can avoid showing the cert error page. For example,
+ // https://example.com -> https://www.example.com.
+ if (aStatus ==
+ mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN)) {
+ newPostData = nullptr;
+ newURI = MaybeFixBadCertDomainErrorURI(aChannel, url);
+ }
+
+ // Did we make a new URI that is different to the old one? If so
+ // load it.
+ //
+ if (newURI) {
+ // Make sure the new URI is different from the old one,
+ // otherwise there's little point trying to load it again.
+ bool sameURI = false;
+ url->Equals(newURI, &sameURI);
+ if (!sameURI) {
+ if (aNewPostData) {
+ newPostData.forget(aNewPostData);
+ }
+ if (aNotifyKeywordSearchLoading) {
+ // This notification is meant for Firefox Health Report so it
+ // can increment counts from the search engine
+ MaybeNotifyKeywordSearchLoading(keywordProviderName, keywordAsSent);
+ }
+ return newURI.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult nsDocShell::FilterStatusForErrorPage(
+ nsresult aStatus, nsIChannel* aChannel, uint32_t aLoadType,
+ bool aIsTopFrame, bool aUseErrorPages, bool aIsInitialDocument,
+ bool* aSkippedUnknownProtocolNavigation) {
+ // Errors to be shown only on top-level frames
+ if ((aStatus == NS_ERROR_UNKNOWN_HOST ||
+ aStatus == NS_ERROR_CONNECTION_REFUSED ||
+ aStatus == NS_ERROR_UNKNOWN_PROXY_HOST ||
+ aStatus == NS_ERROR_PROXY_CONNECTION_REFUSED ||
+ aStatus == NS_ERROR_PROXY_FORBIDDEN ||
+ aStatus == NS_ERROR_PROXY_NOT_IMPLEMENTED ||
+ aStatus == NS_ERROR_PROXY_AUTHENTICATION_FAILED ||
+ aStatus == NS_ERROR_PROXY_TOO_MANY_REQUESTS ||
+ aStatus == NS_ERROR_MALFORMED_URI ||
+ aStatus == NS_ERROR_BLOCKED_BY_POLICY ||
+ aStatus == NS_ERROR_DOM_COOP_FAILED ||
+ aStatus == NS_ERROR_DOM_COEP_FAILED) &&
+ (aIsTopFrame || aUseErrorPages)) {
+ return aStatus;
+ }
+
+ if (aStatus == NS_ERROR_NET_TIMEOUT ||
+ aStatus == NS_ERROR_NET_TIMEOUT_EXTERNAL ||
+ aStatus == NS_ERROR_PROXY_GATEWAY_TIMEOUT ||
+ aStatus == NS_ERROR_REDIRECT_LOOP ||
+ aStatus == NS_ERROR_UNKNOWN_SOCKET_TYPE ||
+ aStatus == NS_ERROR_NET_INTERRUPT || aStatus == NS_ERROR_NET_RESET ||
+ aStatus == NS_ERROR_PROXY_BAD_GATEWAY || aStatus == NS_ERROR_OFFLINE ||
+ aStatus == NS_ERROR_MALWARE_URI || aStatus == NS_ERROR_PHISHING_URI ||
+ aStatus == NS_ERROR_UNWANTED_URI || aStatus == NS_ERROR_HARMFUL_URI ||
+ aStatus == NS_ERROR_UNSAFE_CONTENT_TYPE ||
+ aStatus == NS_ERROR_INTERCEPTION_FAILED ||
+ aStatus == NS_ERROR_NET_INADEQUATE_SECURITY ||
+ aStatus == NS_ERROR_NET_HTTP2_SENT_GOAWAY ||
+ aStatus == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR ||
+ aStatus == NS_ERROR_DOM_BAD_URI || aStatus == NS_ERROR_FILE_NOT_FOUND ||
+ aStatus == NS_ERROR_FILE_ACCESS_DENIED ||
+ aStatus == NS_ERROR_CORRUPTED_CONTENT ||
+ aStatus == NS_ERROR_INVALID_CONTENT_ENCODING ||
+ NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_SECURITY) {
+ // Errors to be shown for any frame
+ return aStatus;
+ }
+
+ if (aStatus == NS_ERROR_UNKNOWN_PROTOCOL) {
+ // For unknown protocols we only display an error if the load is triggered
+ // by the browser itself, or we're replacing the initial document (and
+ // nothing else). Showing the error for page-triggered navigations causes
+ // annoying behavior for users, see bug 1528305.
+ //
+ // We could, maybe, try to detect if this is in response to some user
+ // interaction (like clicking a link, or something else) and maybe show
+ // the error page in that case. But this allows for ctrl+clicking and such
+ // to see the error page.
+ nsCOMPtr<nsILoadInfo> info = aChannel->LoadInfo();
+ if (!info->TriggeringPrincipal()->IsSystemPrincipal() &&
+ StaticPrefs::dom_no_unknown_protocol_error_enabled() &&
+ !aIsInitialDocument) {
+ if (aSkippedUnknownProtocolNavigation) {
+ *aSkippedUnknownProtocolNavigation = true;
+ }
+ return NS_OK;
+ }
+ return aStatus;
+ }
+
+ if (aStatus == NS_ERROR_DOCUMENT_NOT_CACHED) {
+ // Non-caching channels will simply return NS_ERROR_OFFLINE.
+ // Caching channels would have to look at their flags to work
+ // out which error to return. Or we can fix up the error here.
+ if (!(aLoadType & LOAD_CMD_HISTORY)) {
+ return NS_ERROR_OFFLINE;
+ }
+ return aStatus;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
+ nsIChannel* aChannel, nsresult aStatus) {
+ MOZ_LOG(gDocShellLeakLog, LogLevel::Debug,
+ ("DOCSHELL %p EndPageLoad status: %" PRIx32 "\n", this,
+ static_cast<uint32_t>(aStatus)));
+ if (!aChannel) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Make sure to discard the initial client if we never created the initial
+ // about:blank document. Do this before possibly returning from the method
+ // due to an error.
+ mInitialClientSource.reset();
+
+ nsCOMPtr<nsIConsoleReportCollector> reporter = do_QueryInterface(aChannel);
+ if (reporter) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (loadGroup) {
+ reporter->FlushConsoleReports(loadGroup);
+ } else {
+ reporter->FlushConsoleReports(GetDocument());
+ }
+ }
+
+ nsCOMPtr<nsIURI> url;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(url));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsITimedChannel> timingChannel = do_QueryInterface(aChannel);
+ if (timingChannel) {
+ TimeStamp channelCreationTime;
+ rv = timingChannel->GetChannelCreation(&channelCreationTime);
+ if (NS_SUCCEEDED(rv) && !channelCreationTime.IsNull()) {
+ glean::performance_page::total_content_page_load.AccumulateRawDuration(
+ TimeStamp::Now() - channelCreationTime);
+ }
+ }
+
+ // Timing is picked up by the window, we don't need it anymore
+ mTiming = nullptr;
+
+ // clean up reload state for meta charset
+ if (eCharsetReloadRequested == mCharsetReloadState) {
+ mCharsetReloadState = eCharsetReloadStopOrigional;
+ } else {
+ mCharsetReloadState = eCharsetReloadInit;
+ }
+
+ // Save a pointer to the currently-loading history entry.
+ // nsDocShell::EndPageLoad will clear mLSHE, but we may need this history
+ // entry further down in this method.
+ nsCOMPtr<nsISHEntry> loadingSHE = mLSHE;
+ mozilla::Unused << loadingSHE; // XXX: Not sure if we need this anymore
+
+ //
+ // one of many safeguards that prevent death and destruction if
+ // someone is so very very rude as to bring this window down
+ // during this load handler.
+ //
+ nsCOMPtr<nsIDocShell> kungFuDeathGrip(this);
+
+ // Notify the DocumentViewer that the Document has finished loading. This
+ // will cause any OnLoad(...) and PopState(...) handlers to fire.
+ if (!mEODForCurrentDocument && mDocumentViewer) {
+ mIsExecutingOnLoadHandler = true;
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ viewer->LoadComplete(aStatus);
+ mIsExecutingOnLoadHandler = false;
+
+ mEODForCurrentDocument = true;
+ }
+ /* Check if the httpChannel has any cache-control related response headers,
+ * like no-store, no-cache. If so, update SHEntry so that
+ * when a user goes back/forward to this page, we appropriately do
+ * form value restoration or load from server.
+ */
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel));
+ if (!httpChannel) {
+ // HttpChannel could be hiding underneath a Multipart channel.
+ GetHttpChannel(aChannel, getter_AddRefs(httpChannel));
+ }
+
+ if (httpChannel) {
+ // figure out if SH should be saving layout state.
+ bool discardLayoutState = ShouldDiscardLayoutState(httpChannel);
+ if (mLSHE && discardLayoutState && (mLoadType & LOAD_CMD_NORMAL) &&
+ (mLoadType != LOAD_BYPASS_HISTORY) && (mLoadType != LOAD_ERROR_PAGE)) {
+ mLSHE->SetSaveLayoutStateFlag(false);
+ }
+ }
+
+ // Clear mLSHE after calling the onLoadHandlers. This way, if the
+ // onLoadHandler tries to load something different in
+ // itself or one of its children, we can deal with it appropriately.
+ if (mLSHE) {
+ mLSHE->SetLoadType(LOAD_HISTORY);
+
+ // Clear the mLSHE reference to indicate document loading is done one
+ // way or another.
+ SetHistoryEntryAndUpdateBC(Some(nullptr), Nothing());
+ }
+ mActiveEntryIsLoadingFromSessionHistory = false;
+
+ // if there's a refresh header in the channel, this method
+ // will set it up for us.
+ if (mBrowsingContext->IsActive() || !mDisableMetaRefreshWhenInactive)
+ RefreshURIFromQueue();
+
+ // Test whether this is the top frame or a subframe
+ bool isTopFrame = mBrowsingContext->IsTop();
+
+ bool hadErrorStatus = false;
+ // If status code indicates an error it means that DocumentChannel already
+ // tried to fixup the uri and failed. Throw an error dialog box here.
+ if (NS_FAILED(aStatus)) {
+ // If we got CONTENT_BLOCKED from EndPageLoad, then we need to fire
+ // the error event to our embedder, since tests are relying on this.
+ // The error event is usually fired by the caller of InternalLoad, but
+ // this particular error can happen asynchronously.
+ // Bug 1629201 is filed for having much clearer decision making around
+ // which cases need error events.
+ bool fireFrameErrorEvent = (aStatus == NS_ERROR_CONTENT_BLOCKED_SHOW_ALT ||
+ aStatus == NS_ERROR_CONTENT_BLOCKED);
+ UnblockEmbedderLoadEventForFailure(fireFrameErrorEvent);
+
+ bool isInitialDocument =
+ !GetExtantDocument() || GetExtantDocument()->IsInitialDocument();
+ bool skippedUnknownProtocolNavigation = false;
+ aStatus = FilterStatusForErrorPage(aStatus, aChannel, mLoadType, isTopFrame,
+ mBrowsingContext->GetUseErrorPages(),
+ isInitialDocument,
+ &skippedUnknownProtocolNavigation);
+ hadErrorStatus = true;
+ if (NS_FAILED(aStatus)) {
+ if (!mIsBeingDestroyed) {
+ DisplayLoadError(aStatus, url, nullptr, aChannel);
+ }
+ } else if (skippedUnknownProtocolNavigation) {
+ nsTArray<nsString> params;
+ if (NS_FAILED(
+ NS_GetSanitizedURIStringFromURI(url, *params.AppendElement()))) {
+ params.LastElement().AssignLiteral(u"(unknown uri)");
+ }
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "DOM"_ns, GetExtantDocument(),
+ nsContentUtils::eDOM_PROPERTIES, "UnknownProtocolNavigationPrevented",
+ params);
+ }
+ } else {
+ // If we have a host
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ PredictorLearnRedirect(url, aChannel, loadInfo->GetOriginAttributes());
+ }
+
+ if (hadErrorStatus) {
+ // Don't send session store updates if the reason EndPageLoad was called is
+ // because we are process switching. Sometimes the update takes too long and
+ // incorrectly overrides session store data from the following load.
+ return NS_OK;
+ }
+ if (StaticPrefs::browser_sessionstore_platform_collection_AtStartup()) {
+ if (WindowContext* windowContext =
+ mBrowsingContext->GetCurrentWindowContext()) {
+ using Change = SessionStoreChangeListener::Change;
+
+ // We've finished loading the page and now we want to collect all the
+ // session store state that the page is initialized with.
+ SessionStoreChangeListener::CollectSessionStoreData(
+ windowContext,
+ EnumSet<Change>(Change::Input, Change::Scroll, Change::SessionHistory,
+ Change::WireFrame));
+ }
+ }
+
+ return NS_OK;
+}
+
+//*****************************************************************************
+// nsDocShell: Content Viewer Management
+//*****************************************************************************
+
+nsresult nsDocShell::EnsureDocumentViewer() {
+ if (mDocumentViewer) {
+ return NS_OK;
+ }
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> cspToInheritForAboutBlank;
+ nsCOMPtr<nsIURI> baseURI;
+ nsIPrincipal* principal = GetInheritedPrincipal(false);
+ nsIPrincipal* partitionedPrincipal = GetInheritedPrincipal(false, true);
+
+ nsCOMPtr<nsIDocShellTreeItem> parentItem;
+ GetInProcessSameTypeParent(getter_AddRefs(parentItem));
+ if (parentItem) {
+ if (nsCOMPtr<nsPIDOMWindowOuter> domWin = GetWindow()) {
+ nsCOMPtr<Element> parentElement = domWin->GetFrameElementInternal();
+ if (parentElement) {
+ baseURI = parentElement->GetBaseURI();
+ cspToInheritForAboutBlank = parentElement->GetCsp();
+ }
+ }
+ }
+
+ nsresult rv = CreateAboutBlankDocumentViewer(
+ principal, partitionedPrincipal, cspToInheritForAboutBlank, baseURI,
+ /* aIsInitialDocument */ true);
+
+ NS_ENSURE_STATE(mDocumentViewer);
+
+ if (NS_SUCCEEDED(rv)) {
+ RefPtr<Document> doc(GetDocument());
+ MOZ_ASSERT(doc,
+ "Should have doc if CreateAboutBlankDocumentViewer "
+ "succeeded!");
+ MOZ_ASSERT(doc->IsInitialDocument(), "Document should be initial document");
+
+ // Documents created using EnsureDocumentViewer may be transient
+ // placeholders created by framescripts before content has a
+ // chance to load. In some cases, window.open(..., "noopener")
+ // will create such a document and then synchronously tear it
+ // down, firing a "pagehide" event. Doing so violates our
+ // assertions about DocGroups. It's easier to silence the
+ // assertion here than to avoid creating the extra document.
+ doc->IgnoreDocGroupMismatches();
+ }
+
+ return rv;
+}
+
+nsresult nsDocShell::CreateAboutBlankDocumentViewer(
+ nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal,
+ nsIContentSecurityPolicy* aCSP, nsIURI* aBaseURI, bool aIsInitialDocument,
+ const Maybe<nsILoadInfo::CrossOriginEmbedderPolicy>& aCOEP,
+ bool aTryToSaveOldPresentation, bool aCheckPermitUnload,
+ WindowGlobalChild* aActor) {
+ RefPtr<Document> blankDoc;
+ nsCOMPtr<nsIDocumentViewer> viewer;
+ nsresult rv = NS_ERROR_FAILURE;
+
+ MOZ_ASSERT_IF(aActor, aActor->DocumentPrincipal() == aPrincipal);
+
+ /* mCreatingDocument should never be true at this point. However, it's
+ a theoretical possibility. We want to know about it and make it stop,
+ and this sounds like a job for an assertion. */
+ NS_ASSERTION(!mCreatingDocument,
+ "infinite(?) loop creating document averted");
+ if (mCreatingDocument) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mBrowsingContext->AncestorsAreCurrent() ||
+ mBrowsingContext->IsInBFCache()) {
+ mBrowsingContext->RemoveRootFromBFCacheSync();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // mDocumentViewer->PermitUnload may release |this| docshell.
+ nsCOMPtr<nsIDocShell> kungFuDeathGrip(this);
+
+ AutoRestore<bool> creatingDocument(mCreatingDocument);
+ mCreatingDocument = true;
+
+ if (aPrincipal && !aPrincipal->IsSystemPrincipal() &&
+ mItemType != typeChrome) {
+ MOZ_ASSERT(aPrincipal->OriginAttributesRef() ==
+ mBrowsingContext->OriginAttributesRef());
+ }
+
+ // Make sure timing is created. But first record whether we had it
+ // already, so we don't clobber the timing for an in-progress load.
+ bool hadTiming = mTiming;
+ bool toBeReset = MaybeInitTiming();
+ if (mDocumentViewer) {
+ if (aCheckPermitUnload) {
+ // We've got a content viewer already. Make sure the user
+ // permits us to discard the current document and replace it
+ // with about:blank. And also ensure we fire the unload events
+ // in the current document.
+
+ // Unload gets fired first for
+ // document loaded from the session history.
+ mTiming->NotifyBeforeUnload();
+
+ bool okToUnload;
+ rv = mDocumentViewer->PermitUnload(&okToUnload);
+
+ if (NS_SUCCEEDED(rv) && !okToUnload) {
+ // The user chose not to unload the page, interrupt the load.
+ MaybeResetInitTiming(toBeReset);
+ return NS_ERROR_FAILURE;
+ }
+ if (mTiming) {
+ mTiming->NotifyUnloadAccepted(mCurrentURI);
+ }
+ }
+
+ mSavingOldViewer =
+ aTryToSaveOldPresentation &&
+ CanSavePresentation(LOAD_NORMAL, nullptr, nullptr,
+ /* aReportBFCacheComboTelemetry */ true);
+
+ // Make sure to blow away our mLoadingURI just in case. No loads
+ // from inside this pagehide.
+ mLoadingURI = nullptr;
+
+ // Stop any in-progress loading, so that we don't accidentally trigger any
+ // PageShow notifications from Embed() interrupting our loading below.
+ Stop();
+
+ // Notify the current document that it is about to be unloaded!!
+ //
+ // It is important to fire the unload() notification *before* any state
+ // is changed within the DocShell - otherwise, javascript will get the
+ // wrong information :-(
+ //
+ (void)FirePageHideNotification(!mSavingOldViewer);
+ // pagehide notification might destroy this docshell.
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_DOCSHELL_DYING;
+ }
+ }
+
+ // Now make sure we don't think we're in the middle of firing unload after
+ // this point. This will make us fire unload when the about:blank document
+ // unloads... but that's ok, more or less. Would be nice if it fired load
+ // too, of course.
+ mFiredUnloadEvent = false;
+
+ nsCOMPtr<nsIDocumentLoaderFactory> docFactory =
+ nsContentUtils::FindInternalDocumentViewer("text/html"_ns);
+
+ if (docFactory) {
+ nsCOMPtr<nsIPrincipal> principal, partitionedPrincipal;
+ const uint32_t sandboxFlags =
+ mBrowsingContext->GetHasLoadedNonInitialDocument()
+ ? mBrowsingContext->GetSandboxFlags()
+ : mBrowsingContext->GetInitialSandboxFlags();
+ // If we're sandboxed, then create a new null principal. We skip
+ // this if we're being created from WindowGlobalChild, since in
+ // that case we already have a null principal if required.
+ // We can't compare againt the BrowsingContext sandbox flag, since
+ // the value was taken when the load initiated and may have since
+ // changed.
+ if ((sandboxFlags & SANDBOXED_ORIGIN) && !aActor) {
+ if (aPrincipal) {
+ principal = NullPrincipal::CreateWithInheritedAttributes(aPrincipal);
+ } else {
+ principal = NullPrincipal::Create(GetOriginAttributes());
+ }
+ partitionedPrincipal = principal;
+ } else {
+ principal = aPrincipal;
+ partitionedPrincipal = aPartitionedPrincipal;
+ }
+
+ // We cannot get the foreign partitioned prinicpal for the initial
+ // about:blank page. So, we change to check if we need to use the
+ // partitioned principal for the service worker here.
+ MaybeCreateInitialClientSource(
+ StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker(
+ this)
+ ? partitionedPrincipal
+ : principal);
+
+ // generate (about:blank) document to load
+ blankDoc = nsContentDLF::CreateBlankDocument(mLoadGroup, principal,
+ partitionedPrincipal, this);
+ if (blankDoc) {
+ // Hack: manually set the CSP for the new document
+ // Please create an actual copy of the CSP (do not share the same
+ // reference) otherwise appending a new policy within the new
+ // document will be incorrectly propagated to the opening doc.
+ if (aCSP) {
+ RefPtr<nsCSPContext> cspToInherit = new nsCSPContext();
+ cspToInherit->InitFromOther(static_cast<nsCSPContext*>(aCSP));
+ blankDoc->SetCsp(cspToInherit);
+ }
+
+ blankDoc->SetIsInitialDocument(aIsInitialDocument);
+
+ blankDoc->SetEmbedderPolicy(aCOEP);
+
+ // Hack: set the base URI manually, since this document never
+ // got Reset() with a channel.
+ blankDoc->SetBaseURI(aBaseURI);
+
+ // Copy our sandbox flags to the document. These are immutable
+ // after being set here.
+ blankDoc->SetSandboxFlags(sandboxFlags);
+
+ blankDoc->InitFeaturePolicy();
+
+ // create a content viewer for us and the new document
+ docFactory->CreateInstanceForDocument(
+ NS_ISUPPORTS_CAST(nsIDocShell*, this), blankDoc, "view",
+ getter_AddRefs(viewer));
+
+ // hook 'em up
+ if (viewer) {
+ viewer->SetContainer(this);
+ rv = Embed(viewer, aActor, true, false, nullptr, mCurrentURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetCurrentURI(blankDoc->GetDocumentURI(), nullptr,
+ /* aFireLocationChange */ true,
+ /* aIsInitialAboutBlank */ true,
+ /* aLocationFlags */ 0);
+ rv = mIsBeingDestroyed ? NS_ERROR_NOT_AVAILABLE : NS_OK;
+ }
+ }
+ }
+
+ // The transient about:blank viewer doesn't have a session history entry.
+ SetHistoryEntryAndUpdateBC(Nothing(), Some(nullptr));
+
+ // Clear out our mTiming like we would in EndPageLoad, if we didn't
+ // have one before entering this function.
+ if (!hadTiming) {
+ mTiming = nullptr;
+ mBlankTiming = true;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocShell::CreateAboutBlankDocumentViewer(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal,
+ nsIContentSecurityPolicy* aCSP) {
+ return CreateAboutBlankDocumentViewer(aPrincipal, aPartitionedPrincipal, aCSP,
+ nullptr,
+ /* aIsInitialDocument */ false);
+}
+
+nsresult nsDocShell::CreateDocumentViewerForActor(
+ WindowGlobalChild* aWindowActor) {
+ MOZ_ASSERT(aWindowActor);
+
+ // FIXME: WindowGlobalChild should provide the PartitionedPrincipal.
+ // FIXME: We may want to support non-initial documents here.
+ nsresult rv = CreateAboutBlankDocumentViewer(
+ aWindowActor->DocumentPrincipal(), aWindowActor->DocumentPrincipal(),
+ /* aCsp */ nullptr,
+ /* aBaseURI */ nullptr,
+ /* aIsInitialDocument */ true,
+ /* aCOEP */ Nothing(),
+ /* aTryToSaveOldPresentation */ true,
+ /* aCheckPermitUnload */ true, aWindowActor);
+#ifdef DEBUG
+ if (NS_SUCCEEDED(rv)) {
+ RefPtr<Document> doc(GetDocument());
+ MOZ_ASSERT(
+ doc,
+ "Should have a document if CreateAboutBlankDocumentViewer succeeded");
+ MOZ_ASSERT(doc->GetOwnerGlobal() == aWindowActor->GetWindowGlobal(),
+ "New document should be in the same global as our actor");
+ MOZ_ASSERT(doc->IsInitialDocument(),
+ "New document should be an initial document");
+ }
+#endif
+
+ return rv;
+}
+
+bool nsDocShell::CanSavePresentation(uint32_t aLoadType,
+ nsIRequest* aNewRequest,
+ Document* aNewDocument,
+ bool aReportBFCacheComboTelemetry) {
+ if (!mOSHE) {
+ return false; // no entry to save into
+ }
+
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent(),
+ "mOSHE cannot be non-null with SHIP");
+ nsCOMPtr<nsIDocumentViewer> viewer = mOSHE->GetDocumentViewer();
+ if (viewer) {
+ NS_WARNING("mOSHE already has a content viewer!");
+ return false;
+ }
+
+ // Only save presentation for "normal" loads and link loads. Anything else
+ // probably wants to refetch the page, so caching the old presentation
+ // would be incorrect.
+ if (aLoadType != LOAD_NORMAL && aLoadType != LOAD_HISTORY &&
+ aLoadType != LOAD_LINK && aLoadType != LOAD_STOP_CONTENT &&
+ aLoadType != LOAD_STOP_CONTENT_AND_REPLACE &&
+ aLoadType != LOAD_ERROR_PAGE) {
+ return false;
+ }
+
+ // If the session history entry has the saveLayoutState flag set to false,
+ // then we should not cache the presentation.
+ if (!mOSHE->GetSaveLayoutStateFlag()) {
+ return false;
+ }
+
+ // If the document is not done loading, don't cache it.
+ if (!mScriptGlobal || mScriptGlobal->IsLoading()) {
+ MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
+ ("Blocked due to document still loading"));
+ return false;
+ }
+
+ if (mScriptGlobal->WouldReuseInnerWindow(aNewDocument)) {
+ return false;
+ }
+
+ // Avoid doing the work of saving the presentation state in the case where
+ // the content viewer cache is disabled.
+ if (nsSHistory::GetMaxTotalViewers() == 0) {
+ return false;
+ }
+
+ // Don't cache the content viewer if we're in a subframe.
+ if (mBrowsingContext->GetParent()) {
+ return false; // this is a subframe load
+ }
+
+ // If the document does not want its presentation cached, then don't.
+ RefPtr<Document> doc = mScriptGlobal->GetExtantDoc();
+
+ uint32_t bfCacheCombo = 0;
+ bool canSavePresentation =
+ doc->CanSavePresentation(aNewRequest, bfCacheCombo, true);
+ MOZ_ASSERT_IF(canSavePresentation, bfCacheCombo == 0);
+ if (canSavePresentation && doc->IsTopLevelContentDocument()) {
+ auto* browsingContextGroup = mBrowsingContext->Group();
+ nsTArray<RefPtr<BrowsingContext>>& topLevelContext =
+ browsingContextGroup->Toplevels();
+
+ for (const auto& browsingContext : topLevelContext) {
+ if (browsingContext != mBrowsingContext) {
+ if (StaticPrefs::docshell_shistory_bfcache_require_no_opener()) {
+ canSavePresentation = false;
+ }
+ bfCacheCombo |= BFCacheStatus::NOT_ONLY_TOPLEVEL_IN_BCG;
+ break;
+ }
+ }
+ }
+
+ if (aReportBFCacheComboTelemetry) {
+ ReportBFCacheComboTelemetry(bfCacheCombo);
+ }
+ return doc && canSavePresentation;
+}
+
+/* static */
+void nsDocShell::ReportBFCacheComboTelemetry(uint32_t aCombo) {
+ // There are 11 possible reasons to make a request fails to use BFCache
+ // (see BFCacheStatus in dom/base/Document.h), and we'd like to record
+ // the common combinations for reasons which make requests fail to use
+ // BFCache. These combinations are generated based on some local browsings,
+ // we need to adjust them when necessary.
+ enum BFCacheStatusCombo : uint32_t {
+ BFCACHE_SUCCESS,
+ NOT_ONLY_TOPLEVEL = mozilla::dom::BFCacheStatus::NOT_ONLY_TOPLEVEL_IN_BCG,
+ // If both unload and beforeunload listeners are presented, it'll be
+ // recorded as unload
+ UNLOAD = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER,
+ UNLOAD_REQUEST = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+ mozilla::dom::BFCacheStatus::REQUEST,
+ REQUEST = mozilla::dom::BFCacheStatus::REQUEST,
+ UNLOAD_REQUEST_PEER = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+ mozilla::dom::BFCacheStatus::REQUEST |
+ mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION,
+ UNLOAD_REQUEST_PEER_MSE =
+ mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+ mozilla::dom::BFCacheStatus::REQUEST |
+ mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION |
+ mozilla::dom::BFCacheStatus::CONTAINS_MSE_CONTENT,
+ UNLOAD_REQUEST_MSE = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+ mozilla::dom::BFCacheStatus::REQUEST |
+ mozilla::dom::BFCacheStatus::CONTAINS_MSE_CONTENT,
+ SUSPENDED_UNLOAD_REQUEST_PEER =
+ mozilla::dom::BFCacheStatus::SUSPENDED |
+ mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+ mozilla::dom::BFCacheStatus::REQUEST |
+ mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION,
+ REMOTE_SUBFRAMES = mozilla::dom::BFCacheStatus::CONTAINS_REMOTE_SUBFRAMES,
+ BEFOREUNLOAD = mozilla::dom::BFCacheStatus::BEFOREUNLOAD_LISTENER,
+ };
+
+ // Beforeunload is recorded as a blocker only if it is the only one to block
+ // bfcache.
+ if (aCombo != mozilla::dom::BFCacheStatus::BEFOREUNLOAD_LISTENER) {
+ aCombo &= ~mozilla::dom::BFCacheStatus::BEFOREUNLOAD_LISTENER;
+ }
+ switch (aCombo) {
+ case BFCACHE_SUCCESS:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::BFCache_Success);
+ break;
+ case NOT_ONLY_TOPLEVEL:
+ if (StaticPrefs::docshell_shistory_bfcache_require_no_opener()) {
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Other);
+ break;
+ }
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::BFCache_Success);
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Success_Not_Toplevel);
+ break;
+ case UNLOAD:
+ Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Unload);
+ break;
+ case BEFOREUNLOAD:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Beforeunload);
+ break;
+ case UNLOAD_REQUEST:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Unload_Req);
+ break;
+ case REQUEST:
+ Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Req);
+ break;
+ case UNLOAD_REQUEST_PEER:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_Peer);
+ break;
+ case UNLOAD_REQUEST_PEER_MSE:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_Peer_MSE);
+ break;
+ case UNLOAD_REQUEST_MSE:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_MSE);
+ break;
+ case SUSPENDED_UNLOAD_REQUEST_PEER:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::SPD_Unload_Req_Peer);
+ break;
+ case REMOTE_SUBFRAMES:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_BFCACHE_COMBO::Remote_Subframes);
+ break;
+ default:
+ Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Other);
+ break;
+ }
+};
+
+void nsDocShell::ReattachEditorToWindow(nsISHEntry* aSHEntry) {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ NS_ASSERTION(!mEditorData,
+ "Why reattach an editor when we already have one?");
+ NS_ASSERTION(aSHEntry && aSHEntry->HasDetachedEditor(),
+ "Reattaching when there's not a detached editor.");
+
+ if (mEditorData || !aSHEntry) {
+ return;
+ }
+
+ mEditorData = WrapUnique(aSHEntry->ForgetEditorData());
+ if (mEditorData) {
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ mEditorData->ReattachToWindow(this);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to reattach editing session");
+ }
+}
+
+void nsDocShell::DetachEditorFromWindow() {
+ if (!mEditorData || mEditorData->WaitingForLoad()) {
+ // If there's nothing to detach, or if the editor data is actually set
+ // up for the _new_ page that's coming in, don't detach.
+ return;
+ }
+
+ NS_ASSERTION(!mOSHE || !mOSHE->HasDetachedEditor(),
+ "Detaching editor when it's already detached.");
+
+ nsresult res = mEditorData->DetachFromWindow();
+ NS_ASSERTION(NS_SUCCEEDED(res), "Failed to detach editor");
+
+ if (NS_SUCCEEDED(res)) {
+ // Make mOSHE hold the owning ref to the editor data.
+ if (mOSHE) {
+ MOZ_ASSERT(!mIsBeingDestroyed || !mOSHE->HasDetachedEditor(),
+ "We should not set the editor data again once after we "
+ "detached the editor data during destroying this docshell");
+ mOSHE->SetEditorData(mEditorData.release());
+ } else {
+ mEditorData = nullptr;
+ }
+ }
+
+#ifdef DEBUG
+ {
+ bool isEditable;
+ GetEditable(&isEditable);
+ NS_ASSERTION(!isEditable,
+ "Window is still editable after detaching editor.");
+ }
+#endif // DEBUG
+}
+
+nsresult nsDocShell::CaptureState() {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+
+ if (!mOSHE || mOSHE == mLSHE) {
+ // No entry to save into, or we're replacing the existing entry.
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mScriptGlobal) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsISupports> windowState = mScriptGlobal->SaveWindowState();
+ NS_ENSURE_TRUE(windowState, NS_ERROR_FAILURE);
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Debug))) {
+ nsAutoCString spec;
+ nsCOMPtr<nsIURI> uri = mOSHE->GetURI();
+ if (uri) {
+ uri->GetSpec(spec);
+ }
+ MOZ_LOG(gPageCacheLog, LogLevel::Debug,
+ ("Saving presentation into session history, URI: %s", spec.get()));
+ }
+
+ mOSHE->SetWindowState(windowState);
+
+ // Suspend refresh URIs and save off the timer queue
+ mOSHE->SetRefreshURIList(mSavedRefreshURIList);
+
+ // Capture the current content viewer bounds.
+ if (mDocumentViewer) {
+ nsIntRect bounds;
+ mDocumentViewer->GetBounds(bounds);
+ mOSHE->SetViewerBounds(bounds);
+ }
+
+ // Capture the docshell hierarchy.
+ mOSHE->ClearChildShells();
+
+ uint32_t childCount = mChildList.Length();
+ for (uint32_t i = 0; i < childCount; ++i) {
+ nsCOMPtr<nsIDocShellTreeItem> childShell = do_QueryInterface(ChildAt(i));
+ NS_ASSERTION(childShell, "null child shell");
+
+ mOSHE->AddChildShell(childShell);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::RestorePresentationEvent::Run() {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+
+ if (mDocShell && NS_FAILED(mDocShell->RestoreFromHistory())) {
+ NS_WARNING("RestoreFromHistory failed");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::BeginRestore(nsIDocumentViewer* aDocumentViewer, bool aTop) {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+
+ nsresult rv;
+ if (!aDocumentViewer) {
+ rv = EnsureDocumentViewer();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aDocumentViewer = mDocumentViewer;
+ }
+
+ // Dispatch events for restoring the presentation. We try to simulate
+ // the progress notifications loading the document would cause, so we add
+ // the document's channel to the loadgroup to initiate stateChange
+ // notifications.
+
+ RefPtr<Document> doc = aDocumentViewer->GetDocument();
+ if (doc) {
+ nsIChannel* channel = doc->GetChannel();
+ if (channel) {
+ mEODForCurrentDocument = false;
+ mIsRestoringDocument = true;
+ mLoadGroup->AddRequest(channel, nullptr);
+ mIsRestoringDocument = false;
+ }
+ }
+
+ if (!aTop) {
+ // This point corresponds to us having gotten OnStartRequest or
+ // STATE_START, so do the same thing that CreateDocumentViewer does at
+ // this point to ensure that unload/pagehide events for this document
+ // will fire when it's unloaded again.
+ mFiredUnloadEvent = false;
+
+ // For non-top frames, there is no notion of making sure that the
+ // previous document is in the domwindow when STATE_START notifications
+ // happen. We can just call BeginRestore for all of the child shells
+ // now.
+ rv = BeginRestoreChildren();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::BeginRestoreChildren() {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+
+ for (auto* childDocLoader : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> child = do_QueryObject(childDocLoader);
+ if (child) {
+ nsresult rv = child->BeginRestore(nullptr, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::FinishRestore() {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+
+ // First we call finishRestore() on our children. In the simulated load,
+ // all of the child frames finish loading before the main document.
+
+ for (auto* childDocLoader : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> child = do_QueryObject(childDocLoader);
+ if (child) {
+ child->FinishRestore();
+ }
+ }
+
+ if (mOSHE && mOSHE->HasDetachedEditor()) {
+ ReattachEditorToWindow(mOSHE);
+ }
+
+ RefPtr<Document> doc = GetDocument();
+ if (doc) {
+ // Finally, we remove the request from the loadgroup. This will
+ // cause onStateChange(STATE_STOP) to fire, which will fire the
+ // pageshow event to the chrome.
+
+ nsIChannel* channel = doc->GetChannel();
+ if (channel) {
+ mIsRestoringDocument = true;
+ mLoadGroup->RemoveRequest(channel, nullptr, NS_OK);
+ mIsRestoringDocument = false;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetRestoringDocument(bool* aRestoring) {
+ *aRestoring = mIsRestoringDocument;
+ return NS_OK;
+}
+
+nsresult nsDocShell::RestorePresentation(nsISHEntry* aSHEntry,
+ bool* aRestoring) {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ NS_ASSERTION(mLoadType & LOAD_CMD_HISTORY,
+ "RestorePresentation should only be called for history loads");
+
+ nsCOMPtr<nsIDocumentViewer> viewer = aSHEntry->GetDocumentViewer();
+
+ nsAutoCString spec;
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Debug))) {
+ nsCOMPtr<nsIURI> uri = aSHEntry->GetURI();
+ if (uri) {
+ uri->GetSpec(spec);
+ }
+ }
+
+ *aRestoring = false;
+
+ if (!viewer) {
+ MOZ_LOG(gPageCacheLog, LogLevel::Debug,
+ ("no saved presentation for uri: %s", spec.get()));
+ return NS_OK;
+ }
+
+ // We need to make sure the content viewer's container is this docshell.
+ // In subframe navigation, it's possible for the docshell that the
+ // content viewer was originally loaded into to be replaced with a
+ // different one. We don't currently support restoring the presentation
+ // in that case.
+
+ nsCOMPtr<nsIDocShell> container;
+ viewer->GetContainer(getter_AddRefs(container));
+ if (!::SameCOMIdentity(container, GetAsSupports(this))) {
+ MOZ_LOG(gPageCacheLog, LogLevel::Debug,
+ ("No valid container, clearing presentation"));
+ aSHEntry->SetDocumentViewer(nullptr);
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ASSERTION(mDocumentViewer != viewer, "Restoring existing presentation");
+
+ MOZ_LOG(gPageCacheLog, LogLevel::Debug,
+ ("restoring presentation from session history: %s", spec.get()));
+
+ SetHistoryEntryAndUpdateBC(Some(aSHEntry), Nothing());
+
+ // Post an event that will remove the request after we've returned
+ // to the event loop. This mimics the way it is called by nsIChannel
+ // implementations.
+
+ // Revoke any pending restore (just in case).
+ NS_ASSERTION(!mRestorePresentationEvent.IsPending(),
+ "should only have one RestorePresentationEvent");
+ mRestorePresentationEvent.Revoke();
+
+ RefPtr<RestorePresentationEvent> evt = new RestorePresentationEvent(this);
+ nsresult rv = Dispatch(do_AddRef(evt));
+ if (NS_SUCCEEDED(rv)) {
+ mRestorePresentationEvent = evt.get();
+ // The rest of the restore processing will happen on our event
+ // callback.
+ *aRestoring = true;
+ }
+
+ return rv;
+}
+
+namespace {
+class MOZ_STACK_CLASS PresentationEventForgetter {
+ public:
+ explicit PresentationEventForgetter(
+ nsRevocableEventPtr<nsDocShell::RestorePresentationEvent>&
+ aRestorePresentationEvent)
+ : mRestorePresentationEvent(aRestorePresentationEvent),
+ mEvent(aRestorePresentationEvent.get()) {}
+
+ ~PresentationEventForgetter() { Forget(); }
+
+ void Forget() {
+ if (mRestorePresentationEvent.get() == mEvent) {
+ mRestorePresentationEvent.Forget();
+ mEvent = nullptr;
+ }
+ }
+
+ private:
+ nsRevocableEventPtr<nsDocShell::RestorePresentationEvent>&
+ mRestorePresentationEvent;
+ RefPtr<nsDocShell::RestorePresentationEvent> mEvent;
+};
+
+} // namespace
+
+bool nsDocShell::SandboxFlagsImplyCookies(const uint32_t& aSandboxFlags) {
+ return (aSandboxFlags & (SANDBOXED_ORIGIN | SANDBOXED_SCRIPTS)) == 0;
+}
+
+nsresult nsDocShell::RestoreFromHistory() {
+ MOZ_ASSERT(!mozilla::SessionHistoryInParent());
+ MOZ_ASSERT(mRestorePresentationEvent.IsPending());
+ PresentationEventForgetter forgetter(mRestorePresentationEvent);
+
+ // This section of code follows the same ordering as CreateDocumentViewer.
+ if (!mLSHE) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDocumentViewer> viewer = mLSHE->GetDocumentViewer();
+ if (!viewer) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mSavingOldViewer) {
+ // We determined that it was safe to cache the document presentation
+ // at the time we initiated the new load. We need to check whether
+ // it's still safe to do so, since there may have been DOM mutations
+ // or new requests initiated.
+ RefPtr<Document> doc = viewer->GetDocument();
+ nsIRequest* request = nullptr;
+ if (doc) {
+ request = doc->GetChannel();
+ }
+ mSavingOldViewer = CanSavePresentation(
+ mLoadType, request, doc, /* aReportBFCacheComboTelemetry */ false);
+ }
+
+ // Protect against mLSHE going away via a load triggered from
+ // pagehide or unload.
+ nsCOMPtr<nsISHEntry> origLSHE = mLSHE;
+
+ // Make sure to blow away our mLoadingURI just in case. No loads
+ // from inside this pagehide.
+ mLoadingURI = nullptr;
+
+ // Notify the old content viewer that it's being hidden.
+ FirePageHideNotification(!mSavingOldViewer);
+ // pagehide notification might destroy this docshell.
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_DOCSHELL_DYING;
+ }
+
+ // If mLSHE was changed as a result of the pagehide event, then
+ // something else was loaded. Don't finish restoring.
+ if (mLSHE != origLSHE) {
+ return NS_OK;
+ }
+
+ // Add the request to our load group. We do this before swapping out
+ // the content viewers so that consumers of STATE_START can access
+ // the old document. We only deal with the toplevel load at this time --
+ // to be consistent with normal document loading, subframes cannot start
+ // loading until after data arrives, which is after STATE_START completes.
+
+ RefPtr<RestorePresentationEvent> currentPresentationRestoration =
+ mRestorePresentationEvent.get();
+ Stop();
+ // Make sure we're still restoring the same presentation.
+ // If we aren't, docshell is in process doing another load already.
+ NS_ENSURE_STATE(currentPresentationRestoration ==
+ mRestorePresentationEvent.get());
+ BeginRestore(viewer, true);
+ NS_ENSURE_STATE(currentPresentationRestoration ==
+ mRestorePresentationEvent.get());
+ forgetter.Forget();
+
+ // Set mFiredUnloadEvent = false so that the unload handler for the
+ // *new* document will fire.
+ mFiredUnloadEvent = false;
+
+ mURIResultedInDocument = true;
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (rootSH) {
+ mPreviousEntryIndex = rootSH->Index();
+ rootSH->LegacySHistory()->UpdateIndex();
+ mLoadedEntryIndex = rootSH->Index();
+ MOZ_LOG(gPageCacheLog, LogLevel::Verbose,
+ ("Previous index: %d, Loaded index: %d", mPreviousEntryIndex,
+ mLoadedEntryIndex));
+ }
+
+ // Rather than call Embed(), we will retrieve the viewer from the session
+ // history entry and swap it in.
+ // XXX can we refactor this so that we can just call Embed()?
+ PersistLayoutHistoryState();
+ nsresult rv;
+ if (mDocumentViewer) {
+ if (mSavingOldViewer && NS_FAILED(CaptureState())) {
+ if (mOSHE) {
+ mOSHE->SyncPresentationState();
+ }
+ mSavingOldViewer = false;
+ }
+ }
+
+ mSavedRefreshURIList = nullptr;
+
+ // In cases where we use a transient about:blank viewer between loads,
+ // we never show the transient viewer, so _its_ previous viewer is never
+ // unhooked from the view hierarchy. Destroy any such previous viewer now,
+ // before we grab the root view sibling, so that we don't grab a view
+ // that's about to go away.
+
+ if (mDocumentViewer) {
+ // Make sure to hold a strong ref to previousViewer here while we
+ // drop the reference to it from mDocumentViewer.
+ nsCOMPtr<nsIDocumentViewer> previousViewer =
+ mDocumentViewer->GetPreviousViewer();
+ if (previousViewer) {
+ mDocumentViewer->SetPreviousViewer(nullptr);
+ previousViewer->Destroy();
+ }
+ }
+
+ // Save off the root view's parent and sibling so that we can insert the
+ // new content viewer's root view at the same position. Also save the
+ // bounds of the root view's widget.
+
+ nsView* rootViewSibling = nullptr;
+ nsView* rootViewParent = nullptr;
+ nsIntRect newBounds(0, 0, 0, 0);
+
+ PresShell* oldPresShell = GetPresShell();
+ if (oldPresShell) {
+ nsViewManager* vm = oldPresShell->GetViewManager();
+ if (vm) {
+ nsView* oldRootView = vm->GetRootView();
+
+ if (oldRootView) {
+ rootViewSibling = oldRootView->GetNextSibling();
+ rootViewParent = oldRootView->GetParent();
+
+ mDocumentViewer->GetBounds(newBounds);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIContent> container;
+ RefPtr<Document> sibling;
+ if (rootViewParent && rootViewParent->GetParent()) {
+ nsIFrame* frame = rootViewParent->GetParent()->GetFrame();
+ container = frame ? frame->GetContent() : nullptr;
+ }
+ if (rootViewSibling) {
+ nsIFrame* frame = rootViewSibling->GetFrame();
+ sibling = frame ? frame->PresShell()->GetDocument() : nullptr;
+ }
+
+ // Transfer ownership to mDocumentViewer. By ensuring that either the
+ // docshell or the session history, but not both, have references to the
+ // content viewer, we prevent the viewer from being torn down after
+ // Destroy() is called.
+
+ if (mDocumentViewer) {
+ mDocumentViewer->Close(mSavingOldViewer ? mOSHE.get() : nullptr);
+ viewer->SetPreviousViewer(mDocumentViewer);
+ }
+ if (mOSHE && (!mDocumentViewer || !mSavingOldViewer)) {
+ // We don't plan to save a viewer in mOSHE; tell it to drop
+ // any other state it's holding.
+ mOSHE->SyncPresentationState();
+ }
+
+ // Order the mDocumentViewer setup just like Embed does.
+ mDocumentViewer = nullptr;
+
+ // Now that we're about to switch documents, forget all of our children.
+ // Note that we cached them as needed up in CaptureState above.
+ DestroyChildren();
+
+ mDocumentViewer.swap(viewer);
+
+ // Grab all of the related presentation from the SHEntry now.
+ // Clearing the viewer from the SHEntry will clear all of this state.
+ nsCOMPtr<nsISupports> windowState = mLSHE->GetWindowState();
+ mLSHE->SetWindowState(nullptr);
+
+ bool sticky = mLSHE->GetSticky();
+
+ RefPtr<Document> document = mDocumentViewer->GetDocument();
+
+ nsCOMArray<nsIDocShellTreeItem> childShells;
+ int32_t i = 0;
+ nsCOMPtr<nsIDocShellTreeItem> child;
+ while (NS_SUCCEEDED(mLSHE->ChildShellAt(i++, getter_AddRefs(child))) &&
+ child) {
+ childShells.AppendObject(child);
+ }
+
+ // get the previous content viewer size
+ nsIntRect oldBounds(0, 0, 0, 0);
+ mLSHE->GetViewerBounds(oldBounds);
+
+ // Restore the refresh URI list. The refresh timers will be restarted
+ // when EndPageLoad() is called.
+ nsCOMPtr<nsIMutableArray> refreshURIList = mLSHE->GetRefreshURIList();
+
+ // Reattach to the window object.
+ mIsRestoringDocument = true; // for MediaDocument::BecomeInteractive
+ rv = mDocumentViewer->Open(windowState, mLSHE);
+ mIsRestoringDocument = false;
+
+ // Hack to keep nsDocShellEditorData alive across the
+ // SetContentViewer(nullptr) call below.
+ UniquePtr<nsDocShellEditorData> data(mLSHE->ForgetEditorData());
+
+ // Now remove it from the cached presentation.
+ mLSHE->SetDocumentViewer(nullptr);
+ mEODForCurrentDocument = false;
+
+ mLSHE->SetEditorData(data.release());
+
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsIMutableArray> refreshURIs = mLSHE->GetRefreshURIList();
+ nsCOMPtr<nsIDocShellTreeItem> childShell;
+ mLSHE->ChildShellAt(0, getter_AddRefs(childShell));
+ NS_ASSERTION(!refreshURIs && !childShell,
+ "SHEntry should have cleared presentation state");
+ }
+#endif
+
+ // Restore the sticky state of the viewer. The viewer has set this state
+ // on the history entry in Destroy() just before marking itself non-sticky,
+ // to avoid teardown of the presentation.
+ mDocumentViewer->SetSticky(sticky);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // mLSHE is now our currently-loaded document.
+ SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE));
+
+ // We aren't going to restore any items from the LayoutHistoryState,
+ // but we don't want them to stay around in case the page is reloaded.
+ SetLayoutHistoryState(nullptr);
+
+ // This is the end of our Embed() replacement
+
+ mSavingOldViewer = false;
+ mEODForCurrentDocument = false;
+
+ if (document) {
+ RefPtr<nsDocShell> parent = GetInProcessParentDocshell();
+ if (parent) {
+ RefPtr<Document> d = parent->GetDocument();
+ if (d) {
+ if (d->EventHandlingSuppressed()) {
+ document->SuppressEventHandling(d->EventHandlingSuppressed());
+ }
+ }
+ }
+
+ // Use the uri from the mLSHE we had when we entered this function
+ // (which need not match the document's URI if anchors are involved),
+ // since that's the history entry we're loading. Note that if we use
+ // origLSHE we don't have to worry about whether the entry in question
+ // is still mLSHE or whether it's now mOSHE.
+ nsCOMPtr<nsIURI> uri = origLSHE->GetURI();
+ SetCurrentURI(uri, document->GetChannel(), /* aFireLocationChange */ true,
+ /* aIsInitialAboutBlank */ false,
+ /* aLocationFlags */ 0);
+ }
+
+ // This is the end of our CreateDocumentViewer() replacement.
+ // Now we simulate a load. First, we restore the state of the javascript
+ // window object.
+ nsCOMPtr<nsPIDOMWindowOuter> privWin = GetWindow();
+ NS_ASSERTION(privWin, "could not get nsPIDOMWindow interface");
+
+ // Now, dispatch a title change event which would happen as the
+ // <head> is parsed.
+ document->NotifyPossibleTitleChange(false);
+
+ // Now we simulate appending child docshells for subframes.
+ for (i = 0; i < childShells.Count(); ++i) {
+ nsIDocShellTreeItem* childItem = childShells.ObjectAt(i);
+ nsCOMPtr<nsIDocShell> childShell = do_QueryInterface(childItem);
+
+ // Make sure to not clobber the state of the child. Since AddChild
+ // always clobbers it, save it off first.
+ bool allowRedirects;
+ childShell->GetAllowMetaRedirects(&allowRedirects);
+
+ bool allowSubframes;
+ childShell->GetAllowSubframes(&allowSubframes);
+
+ bool allowImages;
+ childShell->GetAllowImages(&allowImages);
+
+ bool allowMedia = childShell->GetAllowMedia();
+
+ bool allowDNSPrefetch;
+ childShell->GetAllowDNSPrefetch(&allowDNSPrefetch);
+
+ bool allowContentRetargeting = childShell->GetAllowContentRetargeting();
+ bool allowContentRetargetingOnChildren =
+ childShell->GetAllowContentRetargetingOnChildren();
+
+ // this.AddChild(child) calls child.SetDocLoaderParent(this), meaning that
+ // the child inherits our state. Among other things, this means that the
+ // child inherits our mPrivateBrowsingId, which is what we want.
+ AddChild(childItem);
+
+ childShell->SetAllowMetaRedirects(allowRedirects);
+ childShell->SetAllowSubframes(allowSubframes);
+ childShell->SetAllowImages(allowImages);
+ childShell->SetAllowMedia(allowMedia);
+ childShell->SetAllowDNSPrefetch(allowDNSPrefetch);
+ childShell->SetAllowContentRetargeting(allowContentRetargeting);
+ childShell->SetAllowContentRetargetingOnChildren(
+ allowContentRetargetingOnChildren);
+
+ rv = childShell->BeginRestore(nullptr, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Make sure to restore the window state after adding the child shells back
+ // to the tree. This is necessary for Thaw() and Resume() to propagate
+ // properly.
+ rv = privWin->RestoreWindowState(windowState);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<PresShell> presShell = GetPresShell();
+
+ // We may be displayed on a different monitor (or in a different
+ // HiDPI mode) than when we got into the history list. So we need
+ // to check if this has happened. See bug 838239.
+
+ // Because the prescontext normally handles resolution changes via
+ // a runnable (see nsPresContext::UIResolutionChanged), its device
+ // context won't be -immediately- updated as a result of calling
+ // presShell->BackingScaleFactorChanged().
+
+ // But we depend on that device context when adjusting the view size
+ // via mDocumentViewer->SetBounds(newBounds) below. So we need to
+ // explicitly tell it to check for changed resolution here.
+ if (presShell) {
+ RefPtr<nsPresContext> pc = presShell->GetPresContext();
+ if (pc->DeviceContext()->CheckDPIChange()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ // Recompute zoom and text-zoom and such.
+ pc->RecomputeBrowsingContextDependentData();
+ }
+
+ nsViewManager* newVM = presShell ? presShell->GetViewManager() : nullptr;
+ nsView* newRootView = newVM ? newVM->GetRootView() : nullptr;
+
+ // Insert the new root view at the correct location in the view tree.
+ if (container) {
+ nsSubDocumentFrame* subDocFrame =
+ do_QueryFrame(container->GetPrimaryFrame());
+ rootViewParent = subDocFrame ? subDocFrame->EnsureInnerView() : nullptr;
+ } else {
+ rootViewParent = nullptr;
+ }
+ if (sibling && sibling->GetPresShell() &&
+ sibling->GetPresShell()->GetViewManager()) {
+ rootViewSibling = sibling->GetPresShell()->GetViewManager()->GetRootView();
+ } else {
+ rootViewSibling = nullptr;
+ }
+ if (rootViewParent && newRootView &&
+ newRootView->GetParent() != rootViewParent) {
+ nsViewManager* parentVM = rootViewParent->GetViewManager();
+ if (parentVM) {
+ // InsertChild(parent, child, sib, true) inserts the child after
+ // sib in content order, which is before sib in view order. BUT
+ // when sib is null it inserts at the end of the the document
+ // order, i.e., first in view order. But when oldRootSibling is
+ // null, the old root as at the end of the view list --- last in
+ // content order --- and we want to call InsertChild(parent, child,
+ // nullptr, false) in that case.
+ parentVM->InsertChild(rootViewParent, newRootView, rootViewSibling,
+ rootViewSibling ? true : false);
+
+ NS_ASSERTION(newRootView->GetNextSibling() == rootViewSibling,
+ "error in InsertChild");
+ }
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> privWinInner = privWin->GetCurrentInnerWindow();
+
+ // If parent is suspended, increase suspension count.
+ // This can't be done as early as event suppression since this
+ // depends on docshell tree.
+ privWinInner->SyncStateFromParentWindow();
+
+ // Now that all of the child docshells have been put into place, we can
+ // restart the timers for the window and all of the child frames.
+ privWinInner->Resume();
+
+ // Now that we have found the inner window of the page restored
+ // from the history, we have to make sure that
+ // performance.navigation.type is 2.
+ Performance* performance = privWinInner->GetPerformance();
+ if (performance) {
+ performance->GetDOMTiming()->NotifyRestoreStart();
+ }
+
+ // Restore the refresh URI list. The refresh timers will be restarted
+ // when EndPageLoad() is called.
+ mRefreshURIList = refreshURIList;
+
+ // Meta-refresh timers have been restarted for this shell, but not
+ // for our children. Walk the child shells and restart their timers.
+ for (auto* childDocLoader : mChildList.ForwardRange()) {
+ nsCOMPtr<nsIDocShell> child = do_QueryObject(childDocLoader);
+ if (child) {
+ child->ResumeRefreshURIs();
+ }
+ }
+
+ // Make sure this presentation is the same size as the previous
+ // presentation. If this is not the same size we showed it at last time,
+ // then we need to resize the widget.
+
+ // XXXbryner This interacts poorly with Firefox's infobar. If the old
+ // presentation had the infobar visible, then we will resize the new
+ // presentation to that smaller size. However, firing the locationchanged
+ // event will hide the infobar, which will immediately resize the window
+ // back to the larger size. A future optimization might be to restore
+ // the presentation at the "wrong" size, then fire the locationchanged
+ // event and check whether the docshell's new size is the same as the
+ // cached viewer size (skipping the resize if they are equal).
+
+ if (newRootView) {
+ if (!newBounds.IsEmpty() && !newBounds.IsEqualEdges(oldBounds)) {
+ MOZ_LOG(gPageCacheLog, LogLevel::Debug,
+ ("resize widget(%d, %d, %d, %d)", newBounds.x, newBounds.y,
+ newBounds.width, newBounds.height));
+ mDocumentViewer->SetBounds(newBounds);
+ } else {
+ nsIScrollableFrame* rootScrollFrame =
+ presShell->GetRootScrollFrameAsScrollable();
+ if (rootScrollFrame) {
+ rootScrollFrame->PostScrolledAreaEventForCurrentArea();
+ }
+ }
+ }
+
+ // The FinishRestore call below can kill these, null them out so we don't
+ // have invalid pointer lying around.
+ newRootView = rootViewSibling = rootViewParent = nullptr;
+ newVM = nullptr;
+
+ // If the IsUnderHiddenEmbedderElement() state has been changed, we need to
+ // update it.
+ if (oldPresShell && presShell &&
+ presShell->IsUnderHiddenEmbedderElement() !=
+ oldPresShell->IsUnderHiddenEmbedderElement()) {
+ presShell->SetIsUnderHiddenEmbedderElement(
+ oldPresShell->IsUnderHiddenEmbedderElement());
+ }
+
+ // Simulate the completion of the load.
+ nsDocShell::FinishRestore();
+
+ // Restart plugins, and paint the content.
+ if (presShell) {
+ presShell->Thaw();
+ }
+
+ return privWin->FireDelayedDOMEvents(true);
+}
+
+nsresult nsDocShell::CreateDocumentViewer(const nsACString& aContentType,
+ nsIRequest* aRequest,
+ nsIStreamListener** aContentHandler) {
+ *aContentHandler = nullptr;
+
+ if (!mTreeOwner || mIsBeingDestroyed) {
+ // If we don't have a tree owner, then we're in the process of being
+ // destroyed. Rather than continue trying to load something, just give up.
+ return NS_ERROR_DOCSHELL_DYING;
+ }
+
+ if (!mBrowsingContext->AncestorsAreCurrent() ||
+ mBrowsingContext->IsInBFCache()) {
+ mBrowsingContext->RemoveRootFromBFCacheSync();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Can we check the content type of the current content viewer
+ // and reuse it without destroying it and re-creating it?
+
+ NS_ASSERTION(mLoadGroup, "Someone ignored return from Init()?");
+
+ // Instantiate the content viewer object
+ nsCOMPtr<nsIDocumentViewer> viewer;
+ nsresult rv = NewDocumentViewerObj(aContentType, aRequest, mLoadGroup,
+ aContentHandler, getter_AddRefs(viewer));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Notify the current document that it is about to be unloaded!!
+ //
+ // It is important to fire the unload() notification *before* any state
+ // is changed within the DocShell - otherwise, javascript will get the
+ // wrong information :-(
+ //
+
+ if (mSavingOldViewer) {
+ // We determined that it was safe to cache the document presentation
+ // at the time we initiated the new load. We need to check whether
+ // it's still safe to do so, since there may have been DOM mutations
+ // or new requests initiated.
+ RefPtr<Document> doc = viewer->GetDocument();
+ mSavingOldViewer = CanSavePresentation(
+ mLoadType, aRequest, doc, /* aReportBFCacheComboTelemetry */ false);
+ }
+
+ NS_ASSERTION(!mLoadingURI, "Re-entering unload?");
+
+ nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest);
+ if (aOpenedChannel) {
+ aOpenedChannel->GetURI(getter_AddRefs(mLoadingURI));
+ }
+
+ // Grab the current URI, we need to pass it to Embed, and OnNewURI will reset
+ // it before we do call Embed.
+ nsCOMPtr<nsIURI> previousURI = mCurrentURI;
+
+ FirePageHideNotification(!mSavingOldViewer);
+ if (mIsBeingDestroyed) {
+ // Force to stop the newly created orphaned viewer.
+ viewer->Stop();
+ return NS_ERROR_DOCSHELL_DYING;
+ }
+ mLoadingURI = nullptr;
+
+ // Set mFiredUnloadEvent = false so that the unload handler for the
+ // *new* document will fire.
+ mFiredUnloadEvent = false;
+
+ // we've created a new document so go ahead and call
+ // OnNewURI(), but don't fire OnLocationChange()
+ // notifications before we've called Embed(). See bug 284993.
+ mURIResultedInDocument = true;
+ bool errorOnLocationChangeNeeded = false;
+ nsCOMPtr<nsIChannel> failedChannel = mFailedChannel;
+ nsCOMPtr<nsIURI> failedURI;
+
+ if (mLoadType == LOAD_ERROR_PAGE) {
+ // We need to set the SH entry and our current URI here and not
+ // at the moment we load the page. We want the same behavior
+ // of Stop() as for a normal page load. See bug 514232 for details.
+
+ // Revert mLoadType to load type to state the page load failed,
+ // following function calls need it.
+ mLoadType = mFailedLoadType;
+
+ Document* doc = viewer->GetDocument();
+ if (doc) {
+ doc->SetFailedChannel(failedChannel);
+ }
+
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ if (failedChannel) {
+ // Make sure we have a URI to set currentURI.
+ NS_GetFinalChannelURI(failedChannel, getter_AddRefs(failedURI));
+ } else {
+ // if there is no failed channel we have to explicitly provide
+ // a triggeringPrincipal for the history entry.
+ triggeringPrincipal = nsContentUtils::GetSystemPrincipal();
+ }
+
+ if (!failedURI) {
+ failedURI = mFailedURI;
+ }
+ if (!failedURI) {
+ // We need a URI object to store a session history entry, so make up a URI
+ NS_NewURI(getter_AddRefs(failedURI), "about:blank");
+ }
+
+ // When we don't have failedURI, something wrong will happen. See
+ // bug 291876.
+ MOZ_ASSERT(failedURI, "We don't have a URI for history APIs.");
+
+ mFailedChannel = nullptr;
+ mFailedURI = nullptr;
+
+ // Create an shistory entry for the old load.
+ if (failedURI) {
+ errorOnLocationChangeNeeded =
+ OnNewURI(failedURI, failedChannel, triggeringPrincipal, nullptr,
+ nullptr, nullptr, false, false);
+ }
+
+ // Be sure to have a correct mLSHE, it may have been cleared by
+ // EndPageLoad. See bug 302115.
+ ChildSHistory* shistory = GetSessionHistory();
+ if (!mozilla::SessionHistoryInParent() && shistory && !mLSHE) {
+ int32_t idx = shistory->LegacySHistory()->GetRequestedIndex();
+ if (idx == -1) {
+ idx = shistory->Index();
+ }
+ shistory->LegacySHistory()->GetEntryAtIndex(idx, getter_AddRefs(mLSHE));
+ }
+
+ mLoadType = LOAD_ERROR_PAGE;
+ }
+
+ nsCOMPtr<nsIURI> finalURI;
+ // If this a redirect, use the final url (uri)
+ // else use the original url
+ //
+ // Note that this should match what documents do (see Document::Reset).
+ NS_GetFinalChannelURI(aOpenedChannel, getter_AddRefs(finalURI));
+
+ bool onLocationChangeNeeded = false;
+ if (finalURI) {
+ // Pass false for aCloneSHChildren, since we're loading a new page here.
+ onLocationChangeNeeded = OnNewURI(finalURI, aOpenedChannel, nullptr,
+ nullptr, nullptr, nullptr, true, false);
+ }
+
+ // let's try resetting the load group if we need to...
+ nsCOMPtr<nsILoadGroup> currentLoadGroup;
+ NS_ENSURE_SUCCESS(
+ aOpenedChannel->GetLoadGroup(getter_AddRefs(currentLoadGroup)),
+ NS_ERROR_FAILURE);
+
+ if (currentLoadGroup != mLoadGroup) {
+ nsLoadFlags loadFlags = 0;
+
+ // Cancel any URIs that are currently loading...
+ // XXX: Need to do this eventually Stop();
+ //
+ // Retarget the document to this loadgroup...
+ //
+ /* First attach the channel to the right loadgroup
+ * and then remove from the old loadgroup. This
+ * puts the notifications in the right order and
+ * we don't null-out mLSHE in OnStateChange() for
+ * all redirected urls
+ */
+ aOpenedChannel->SetLoadGroup(mLoadGroup);
+
+ // Mark the channel as being a document URI...
+ aOpenedChannel->GetLoadFlags(&loadFlags);
+ loadFlags |= nsIChannel::LOAD_DOCUMENT_URI;
+ nsCOMPtr<nsILoadInfo> loadInfo = aOpenedChannel->LoadInfo();
+ if (SandboxFlagsImplyCookies(loadInfo->GetSandboxFlags())) {
+ loadFlags |= nsIRequest::LOAD_DOCUMENT_NEEDS_COOKIE;
+ }
+
+ aOpenedChannel->SetLoadFlags(loadFlags);
+
+ mLoadGroup->AddRequest(aRequest, nullptr);
+ if (currentLoadGroup) {
+ currentLoadGroup->RemoveRequest(aRequest, nullptr, NS_BINDING_RETARGETED);
+ }
+
+ // Update the notification callbacks, so that progress and
+ // status information are sent to the right docshell...
+ aOpenedChannel->SetNotificationCallbacks(this);
+ }
+
+ NS_ENSURE_SUCCESS(Embed(viewer, nullptr, false,
+ ShouldAddToSessionHistory(finalURI, aOpenedChannel),
+ aOpenedChannel, previousURI),
+ NS_ERROR_FAILURE);
+
+ if (!mBrowsingContext->GetHasLoadedNonInitialDocument()) {
+ MOZ_ALWAYS_SUCCEEDS(mBrowsingContext->SetHasLoadedNonInitialDocument(true));
+ }
+
+ mSavedRefreshURIList = nullptr;
+ mSavingOldViewer = false;
+ mEODForCurrentDocument = false;
+
+ // if this document is part of a multipart document,
+ // the ID can be used to distinguish it from the other parts.
+ nsCOMPtr<nsIMultiPartChannel> multiPartChannel(do_QueryInterface(aRequest));
+ if (multiPartChannel) {
+ if (PresShell* presShell = GetPresShell()) {
+ if (Document* doc = presShell->GetDocument()) {
+ uint32_t partID;
+ multiPartChannel->GetPartID(&partID);
+ doc->SetPartID(partID);
+ }
+ }
+ }
+
+ if (errorOnLocationChangeNeeded) {
+ FireOnLocationChange(this, failedChannel, failedURI,
+ LOCATION_CHANGE_ERROR_PAGE);
+ } else if (onLocationChangeNeeded) {
+ uint32_t locationFlags =
+ (mLoadType & LOAD_CMD_RELOAD) ? uint32_t(LOCATION_CHANGE_RELOAD) : 0;
+ FireOnLocationChange(this, aRequest, mCurrentURI, locationFlags);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::NewDocumentViewerObj(const nsACString& aContentType,
+ nsIRequest* aRequest,
+ nsILoadGroup* aLoadGroup,
+ nsIStreamListener** aContentHandler,
+ nsIDocumentViewer** aViewer) {
+ nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest);
+
+ nsCOMPtr<nsIDocumentLoaderFactory> docLoaderFactory =
+ nsContentUtils::FindInternalDocumentViewer(aContentType);
+ if (!docLoaderFactory) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now create an instance of the content viewer nsLayoutDLF makes the
+ // determination if it should be a "view-source" instead of "view"
+ nsresult rv = docLoaderFactory->CreateInstance(
+ "view", aOpenedChannel, aLoadGroup, aContentType, this, nullptr,
+ aContentHandler, aViewer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (*aViewer)->SetContainer(this);
+ return NS_OK;
+}
+
+nsresult nsDocShell::SetupNewViewer(nsIDocumentViewer* aNewViewer,
+ WindowGlobalChild* aWindowActor) {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ //
+ // Copy content viewer state from previous or parent content viewer.
+ //
+ // The following logic is mirrored in nsHTMLDocument::StartDocumentLoad!
+ //
+ // Do NOT to maintain a reference to the old content viewer outside
+ // of this "copying" block, or it will not be destroyed until the end of
+ // this routine and all <SCRIPT>s and event handlers fail! (bug 20315)
+ //
+ // In this block of code, if we get an error result, we return it
+ // but if we get a null pointer, that's perfectly legal for parent
+ // and parentContentViewer.
+ //
+
+ int32_t x = 0;
+ int32_t y = 0;
+ int32_t cx = 0;
+ int32_t cy = 0;
+
+ // This will get the size from the current content viewer or from the
+ // Init settings
+ DoGetPositionAndSize(&x, &y, &cx, &cy);
+
+ nsCOMPtr<nsIDocShellTreeItem> parentAsItem;
+ NS_ENSURE_SUCCESS(GetInProcessSameTypeParent(getter_AddRefs(parentAsItem)),
+ NS_ERROR_FAILURE);
+ nsCOMPtr<nsIDocShell> parent(do_QueryInterface(parentAsItem));
+
+ const Encoding* reloadEncoding = nullptr;
+ int32_t reloadEncodingSource = kCharsetUninitialized;
+ // |newMUDV| also serves as a flag to set the data from the above vars
+ nsCOMPtr<nsIDocumentViewer> newViewer;
+
+ if (mDocumentViewer || parent) {
+ nsCOMPtr<nsIDocumentViewer> oldViewer;
+ if (mDocumentViewer) {
+ // Get any interesting state from old content viewer
+ // XXX: it would be far better to just reuse the document viewer ,
+ // since we know we're just displaying the same document as before
+ oldViewer = mDocumentViewer;
+
+ // Tell the old content viewer to hibernate in session history when
+ // it is destroyed.
+
+ if (mSavingOldViewer && NS_FAILED(CaptureState())) {
+ if (mOSHE) {
+ mOSHE->SyncPresentationState();
+ }
+ mSavingOldViewer = false;
+ }
+ } else {
+ // No old content viewer, so get state from parent's content viewer
+ parent->GetDocViewer(getter_AddRefs(oldViewer));
+ }
+
+ if (oldViewer) {
+ newViewer = aNewViewer;
+ if (newViewer) {
+ reloadEncoding =
+ oldViewer->GetReloadEncodingAndSource(&reloadEncodingSource);
+ }
+ }
+ }
+
+ nscolor bgcolor = NS_RGBA(0, 0, 0, 0);
+ bool isUnderHiddenEmbedderElement = false;
+ // Ensure that the content viewer is destroyed *after* the GC - bug 71515
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ if (viewer) {
+ // Stop any activity that may be happening in the old document before
+ // releasing it...
+ viewer->Stop();
+
+ // Try to extract the canvas background color from the old
+ // presentation shell, so we can use it for the next document.
+ if (PresShell* presShell = viewer->GetPresShell()) {
+ bgcolor = presShell->GetCanvasBackground();
+ isUnderHiddenEmbedderElement = presShell->IsUnderHiddenEmbedderElement();
+ }
+
+ viewer->Close(mSavingOldViewer ? mOSHE.get() : nullptr);
+ aNewViewer->SetPreviousViewer(viewer);
+ }
+ if (mOSHE && (!mDocumentViewer || !mSavingOldViewer)) {
+ // We don't plan to save a viewer in mOSHE; tell it to drop
+ // any other state it's holding.
+ mOSHE->SyncPresentationState();
+ }
+
+ mDocumentViewer = nullptr;
+
+ // Now that we're about to switch documents, forget all of our children.
+ // Note that we cached them as needed up in CaptureState above.
+ DestroyChildren();
+
+ mDocumentViewer = aNewViewer;
+
+ nsCOMPtr<nsIWidget> widget;
+ NS_ENSURE_SUCCESS(GetMainWidget(getter_AddRefs(widget)), NS_ERROR_FAILURE);
+
+ nsIntRect bounds(x, y, cx, cy);
+
+ mDocumentViewer->SetNavigationTiming(mTiming);
+
+ if (NS_FAILED(mDocumentViewer->Init(widget, bounds, aWindowActor))) {
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ viewer->Close(nullptr);
+ viewer->Destroy();
+ mDocumentViewer = nullptr;
+ SetCurrentURIInternal(nullptr);
+ NS_WARNING("ContentViewer Initialization failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we have old state to copy, set the old state onto the new content
+ // viewer
+ if (newViewer) {
+ newViewer->SetReloadEncodingAndSource(reloadEncoding, reloadEncodingSource);
+ }
+
+ NS_ENSURE_TRUE(mDocumentViewer, NS_ERROR_FAILURE);
+
+ // Stuff the bgcolor from the old pres shell into the new
+ // pres shell. This improves page load continuity.
+ if (RefPtr<PresShell> presShell = mDocumentViewer->GetPresShell()) {
+ presShell->SetCanvasBackground(bgcolor);
+ presShell->ActivenessMaybeChanged();
+ if (isUnderHiddenEmbedderElement) {
+ presShell->SetIsUnderHiddenEmbedderElement(isUnderHiddenEmbedderElement);
+ }
+ }
+
+ // XXX: It looks like the LayoutState gets restored again in Embed()
+ // right after the call to SetupNewViewer(...)
+
+ // We don't show the mDocumentViewer yet, since we want to draw the old page
+ // until we have enough of the new page to show. Just return with the new
+ // viewer still set to hidden.
+
+ return NS_OK;
+}
+
+void nsDocShell::SetDocCurrentStateObj(nsISHEntry* aShEntry,
+ SessionHistoryInfo* aInfo) {
+ NS_ENSURE_TRUE_VOID(mDocumentViewer);
+
+ RefPtr<Document> document = GetDocument();
+ NS_ENSURE_TRUE_VOID(document);
+
+ nsCOMPtr<nsIStructuredCloneContainer> scContainer;
+ if (mozilla::SessionHistoryInParent()) {
+ // If aInfo is null, just set the document's state object to null.
+ if (aInfo) {
+ scContainer = aInfo->GetStateData();
+ }
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p SetCurrentDocState %p", this, scContainer.get()));
+ } else {
+ if (aShEntry) {
+ scContainer = aShEntry->GetStateData();
+
+ // If aShEntry is null, just set the document's state object to null.
+ }
+ }
+
+ // It's OK for scContainer too be null here; that just means there's no
+ // state data associated with this history entry.
+ document->SetStateObject(scContainer);
+}
+
+nsresult nsDocShell::CheckLoadingPermissions() {
+ // This method checks whether the caller may load content into
+ // this docshell. Even though we've done our best to hide windows
+ // from code that doesn't have the right to access them, it's
+ // still possible for an evil site to open a window and access
+ // frames in the new window through window.frames[] (which is
+ // allAccess for historic reasons), so we still need to do this
+ // check on load.
+ nsresult rv = NS_OK;
+
+ if (!IsSubframe()) {
+ // We're not a frame. Permit all loads.
+ return rv;
+ }
+
+ // Note - The check for a current JSContext here isn't necessarily sensical.
+ // It's just designed to preserve the old semantics during a mass-conversion
+ // patch.
+ if (!nsContentUtils::GetCurrentJSContext()) {
+ return NS_OK;
+ }
+
+ // Check if the caller is from the same origin as this docshell,
+ // or any of its ancestors.
+ for (RefPtr<BrowsingContext> bc = mBrowsingContext; bc;
+ bc = bc->GetParent()) {
+ // If the BrowsingContext is not in process, then it
+ // is true by construction that its principal will not
+ // subsume the current docshell principal.
+ if (!bc->IsInProcess()) {
+ continue;
+ }
+
+ nsCOMPtr<nsIScriptGlobalObject> sgo =
+ bc->GetDocShell()->GetScriptGlobalObject();
+ nsCOMPtr<nsIScriptObjectPrincipal> sop(do_QueryInterface(sgo));
+
+ nsIPrincipal* p;
+ if (!sop || !(p = sop->GetPrincipal())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (nsContentUtils::SubjectPrincipal()->Subsumes(p)) {
+ // Same origin, permit load
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_DOM_PROP_ACCESS_DENIED;
+}
+
+//*****************************************************************************
+// nsDocShell: Site Loading
+//*****************************************************************************
+
+void nsDocShell::CopyFavicon(nsIURI* aOldURI, nsIURI* aNewURI,
+ bool aInPrivateBrowsing) {
+ if (XRE_IsContentProcess()) {
+ dom::ContentChild* contentChild = dom::ContentChild::GetSingleton();
+ if (contentChild) {
+ contentChild->SendCopyFavicon(aOldURI, aNewURI, aInPrivateBrowsing);
+ }
+ return;
+ }
+
+#ifdef MOZ_PLACES
+ nsCOMPtr<nsIFaviconService> favSvc =
+ do_GetService("@mozilla.org/browser/favicon-service;1");
+ if (favSvc) {
+ favSvc->CopyFavicons(aOldURI, aNewURI,
+ aInPrivateBrowsing
+ ? nsIFaviconService::FAVICON_LOAD_PRIVATE
+ : nsIFaviconService::FAVICON_LOAD_NON_PRIVATE,
+ nullptr);
+ }
+#endif
+}
+
+class InternalLoadEvent : public Runnable {
+ public:
+ InternalLoadEvent(nsDocShell* aDocShell, nsDocShellLoadState* aLoadState)
+ : mozilla::Runnable("InternalLoadEvent"),
+ mDocShell(aDocShell),
+ mLoadState(aLoadState) {
+ // For events, both target and filename should be the version of "null" they
+ // expect. By the time the event is fired, both window targeting and file
+ // downloading have been handled, so we should never have an internal load
+ // event that retargets or had a download.
+ mLoadState->SetTarget(u""_ns);
+ mLoadState->SetFileName(VoidString());
+ }
+
+ NS_IMETHOD
+ Run() override {
+#ifndef ANDROID
+ MOZ_ASSERT(mLoadState->TriggeringPrincipal(),
+ "InternalLoadEvent: Should always have a principal here");
+#endif
+ return mDocShell->InternalLoad(mLoadState);
+ }
+
+ private:
+ RefPtr<nsDocShell> mDocShell;
+ RefPtr<nsDocShellLoadState> mLoadState;
+};
+
+/**
+ * Returns true if we started an asynchronous load (i.e., from the network), but
+ * the document we're loading there hasn't yet become this docshell's active
+ * document.
+ *
+ * When JustStartedNetworkLoad is true, you should be careful about modifying
+ * mLoadType and mLSHE. These are both set when the asynchronous load first
+ * starts, and the load expects that, when it eventually runs InternalLoad,
+ * mLoadType and mLSHE will have their original values.
+ */
+bool nsDocShell::JustStartedNetworkLoad() {
+ return mDocumentRequest && mDocumentRequest != GetCurrentDocChannel();
+}
+
+// The contentType will be INTERNAL_(I)FRAME if this docshell is for a
+// non-toplevel browsing context in spec terms. (frame, iframe, <object>,
+// <embed>, etc)
+//
+// This return value will be used when we call NS_CheckContentLoadPolicy, and
+// later when we call DoURILoad.
+nsContentPolicyType nsDocShell::DetermineContentType() {
+ if (!IsSubframe()) {
+ return nsIContentPolicy::TYPE_DOCUMENT;
+ }
+
+ const auto& maybeEmbedderElementType =
+ GetBrowsingContext()->GetEmbedderElementType();
+ if (!maybeEmbedderElementType) {
+ // If the EmbedderElementType hasn't been set yet, just assume we're
+ // an iframe since that's more common.
+ return nsIContentPolicy::TYPE_INTERNAL_IFRAME;
+ }
+
+ return maybeEmbedderElementType->EqualsLiteral("iframe")
+ ? nsIContentPolicy::TYPE_INTERNAL_IFRAME
+ : nsIContentPolicy::TYPE_INTERNAL_FRAME;
+}
+
+bool nsDocShell::NoopenerForceEnabled() {
+ // If current's top-level browsing context's active document's
+ // cross-origin-opener-policy is "same-origin" or "same-origin + COEP" then
+ // if currentDoc's origin is not same origin with currentDoc's top-level
+ // origin, noopener is force enabled, and name is cleared to "_blank".
+ auto topPolicy = mBrowsingContext->Top()->GetOpenerPolicy();
+ return (topPolicy == nsILoadInfo::OPENER_POLICY_SAME_ORIGIN ||
+ topPolicy ==
+ nsILoadInfo::
+ OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP) &&
+ !mBrowsingContext->SameOriginWithTop();
+}
+
+nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) {
+ MOZ_ASSERT(aLoadState, "need a load state!");
+ MOZ_ASSERT(!aLoadState->Target().IsEmpty(), "should have a target here!");
+ MOZ_ASSERT(aLoadState->TargetBrowsingContext().IsNull(),
+ "should not have picked target yet");
+
+ nsresult rv = NS_OK;
+ RefPtr<BrowsingContext> targetContext;
+
+ // Only _self, _parent, and _top are supported in noopener case. But we
+ // have to be careful to not apply that to the noreferrer case. See bug
+ // 1358469.
+ bool allowNamedTarget =
+ !aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_NO_OPENER) ||
+ aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER);
+ if (allowNamedTarget ||
+ aLoadState->Target().LowerCaseEqualsLiteral("_self") ||
+ aLoadState->Target().LowerCaseEqualsLiteral("_parent") ||
+ aLoadState->Target().LowerCaseEqualsLiteral("_top")) {
+ Document* document = GetDocument();
+ NS_ENSURE_TRUE(document, NS_ERROR_FAILURE);
+ WindowGlobalChild* wgc = document->GetWindowGlobalChild();
+ NS_ENSURE_TRUE(wgc, NS_ERROR_FAILURE);
+ targetContext = wgc->FindBrowsingContextWithName(
+ aLoadState->Target(), /* aUseEntryGlobalForAccessCheck */ false);
+ }
+
+ if (!targetContext) {
+ // If the targetContext doesn't exist, then this is a new docShell and we
+ // should consider this a TYPE_DOCUMENT load
+ //
+ // For example, when target="_blank"
+
+ // If there's no targetContext, that means we are about to create a new
+ // window. Perform a content policy check before creating the window. Please
+ // note for all other docshell loads content policy checks are performed
+ // within the contentSecurityManager when the channel is about to be
+ // openend.
+ nsISupports* requestingContext = nullptr;
+ if (XRE_IsContentProcess()) {
+ // In e10s the child process doesn't have access to the element that
+ // contains the browsing context (because that element is in the chrome
+ // process). So we just pass mScriptGlobal.
+ requestingContext = ToSupports(mScriptGlobal);
+ } else {
+ // This is for loading non-e10s tabs and toplevel windows of various
+ // sorts.
+ // For the toplevel window cases, requestingElement will be null.
+ nsCOMPtr<Element> requestingElement =
+ mScriptGlobal->GetFrameElementInternal();
+ requestingContext = requestingElement;
+ }
+
+ // Ideally we should use the same loadinfo as within DoURILoad which
+ // should match this one when both are applicable.
+ nsCOMPtr<nsILoadInfo> secCheckLoadInfo =
+ new LoadInfo(mScriptGlobal, aLoadState->URI(),
+ aLoadState->TriggeringPrincipal(), requestingContext,
+ nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, 0);
+
+ // Since Content Policy checks are performed within docShell as well as
+ // the ContentSecurityManager we need a reliable way to let certain
+ // nsIContentPolicy consumers ignore duplicate calls.
+ secCheckLoadInfo->SetSkipContentPolicyCheckForWebRequest(true);
+
+ int16_t shouldLoad = nsIContentPolicy::ACCEPT;
+ rv = NS_CheckContentLoadPolicy(aLoadState->URI(), secCheckLoadInfo,
+ &shouldLoad);
+
+ if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
+ if (NS_SUCCEEDED(rv)) {
+ if (shouldLoad == nsIContentPolicy::REJECT_TYPE) {
+ return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT;
+ }
+ if (shouldLoad == nsIContentPolicy::REJECT_POLICY) {
+ return NS_ERROR_BLOCKED_BY_POLICY;
+ }
+ }
+
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ }
+
+ //
+ // Resolve the window target before going any further...
+ // If the load has been targeted to another DocShell, then transfer the
+ // load to it...
+ //
+
+ // We've already done our owner-inheriting. Mask out that bit, so we
+ // don't try inheriting an owner from the target window if we came up
+ // with a null owner above.
+ aLoadState->UnsetInternalLoadFlag(INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL);
+
+ if (!targetContext) {
+ // If the docshell's document is sandboxed, only open a new window
+ // if the document's SANDBOXED_AUXILLARY_NAVIGATION flag is not set.
+ // (i.e. if allow-popups is specified)
+ NS_ENSURE_TRUE(mDocumentViewer, NS_ERROR_FAILURE);
+ Document* doc = mDocumentViewer->GetDocument();
+
+ const bool isDocumentAuxSandboxed =
+ doc && (doc->GetSandboxFlags() & SANDBOXED_AUXILIARY_NAVIGATION);
+
+ if (isDocumentAuxSandboxed) {
+ return NS_ERROR_DOM_INVALID_ACCESS_ERR;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow();
+ NS_ENSURE_TRUE(win, NS_ERROR_NOT_AVAILABLE);
+
+ RefPtr<BrowsingContext> newBC;
+ nsAutoCString spec;
+ aLoadState->URI()->GetSpec(spec);
+
+ // If we are a noopener load, we just hand the whole thing over to our
+ // window.
+ if (aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_NO_OPENER) ||
+ NoopenerForceEnabled()) {
+ // Various asserts that we know to hold because NO_OPENER loads can only
+ // happen for links.
+ MOZ_ASSERT(!aLoadState->LoadReplace());
+ MOZ_ASSERT(aLoadState->PrincipalToInherit() ==
+ aLoadState->TriggeringPrincipal());
+ MOZ_ASSERT(!(aLoadState->InternalLoadFlags() &
+ ~(INTERNAL_LOAD_FLAGS_NO_OPENER |
+ INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER)),
+ "Only INTERNAL_LOAD_FLAGS_NO_OPENER and "
+ "INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER can be set");
+ MOZ_ASSERT_IF(aLoadState->PostDataStream(),
+ aLoadState->IsFormSubmission());
+ MOZ_ASSERT(!aLoadState->HeadersStream());
+ // If OnLinkClickSync was invoked inside the onload handler, the load
+ // type would be set to LOAD_NORMAL_REPLACE; otherwise it should be
+ // LOAD_LINK.
+ MOZ_ASSERT(aLoadState->LoadType() == LOAD_LINK ||
+ aLoadState->LoadType() == LOAD_NORMAL_REPLACE);
+ MOZ_ASSERT(!aLoadState->LoadIsFromSessionHistory());
+ MOZ_ASSERT(aLoadState->FirstParty()); // Windowwatcher will assume this.
+
+ RefPtr<nsDocShellLoadState> loadState =
+ new nsDocShellLoadState(aLoadState->URI());
+
+ // Set up our loadinfo so it will do the load as much like we would have
+ // as possible.
+ loadState->SetReferrerInfo(aLoadState->GetReferrerInfo());
+ loadState->SetOriginalURI(aLoadState->OriginalURI());
+
+ Maybe<nsCOMPtr<nsIURI>> resultPrincipalURI;
+ aLoadState->GetMaybeResultPrincipalURI(resultPrincipalURI);
+
+ loadState->SetMaybeResultPrincipalURI(resultPrincipalURI);
+ loadState->SetKeepResultPrincipalURIIfSet(
+ aLoadState->KeepResultPrincipalURIIfSet());
+ // LoadReplace will always be false due to asserts above, skip setting
+ // it.
+ loadState->SetTriggeringPrincipal(aLoadState->TriggeringPrincipal());
+ loadState->SetTriggeringSandboxFlags(
+ aLoadState->TriggeringSandboxFlags());
+ loadState->SetTriggeringWindowId(aLoadState->TriggeringWindowId());
+ loadState->SetTriggeringStorageAccess(
+ aLoadState->TriggeringStorageAccess());
+ loadState->SetCsp(aLoadState->Csp());
+ loadState->SetInheritPrincipal(aLoadState->HasInternalLoadFlags(
+ INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL));
+ // Explicit principal because we do not want any guesses as to what the
+ // principal to inherit is: it should be aTriggeringPrincipal.
+ loadState->SetPrincipalIsExplicit(true);
+ loadState->SetLoadType(aLoadState->LoadType());
+ loadState->SetForceAllowDataURI(aLoadState->HasInternalLoadFlags(
+ INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI));
+
+ loadState->SetHasValidUserGestureActivation(
+ aLoadState->HasValidUserGestureActivation());
+
+ // Propagate POST data to the new load.
+ loadState->SetPostDataStream(aLoadState->PostDataStream());
+ loadState->SetIsFormSubmission(aLoadState->IsFormSubmission());
+
+ rv = win->Open(NS_ConvertUTF8toUTF16(spec),
+ aLoadState->Target(), // window name
+ u""_ns, // Features
+ loadState,
+ true, // aForceNoOpener
+ getter_AddRefs(newBC));
+ MOZ_ASSERT(!newBC);
+ return rv;
+ }
+
+ rv = win->OpenNoNavigate(NS_ConvertUTF8toUTF16(spec),
+ aLoadState->Target(), // window name
+ u""_ns, // Features
+ getter_AddRefs(newBC));
+
+ // In some cases the Open call doesn't actually result in a new
+ // window being opened. We can detect these cases by examining the
+ // document in |newBC|, if any.
+ nsCOMPtr<nsPIDOMWindowOuter> piNewWin =
+ newBC ? newBC->GetDOMWindow() : nullptr;
+ if (piNewWin) {
+ RefPtr<Document> newDoc = piNewWin->GetExtantDoc();
+ if (!newDoc || newDoc->IsInitialDocument()) {
+ aLoadState->SetInternalLoadFlag(INTERNAL_LOAD_FLAGS_FIRST_LOAD);
+ }
+ }
+
+ if (newBC) {
+ targetContext = newBC;
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(targetContext, rv);
+
+ // If our target BrowsingContext is still pending initialization, ignore the
+ // navigation request targeting it.
+ if (NS_WARN_IF(targetContext->GetPendingInitialization())) {
+ return NS_OK;
+ }
+
+ aLoadState->SetTargetBrowsingContext(targetContext);
+ if (aLoadState->IsFormSubmission()) {
+ aLoadState->SetLoadType(
+ GetLoadTypeForFormSubmission(targetContext, aLoadState));
+ }
+
+ //
+ // Transfer the load to the target BrowsingContext... Clear the window target
+ // name to the empty string to prevent recursive retargeting!
+ //
+ // No window target
+ aLoadState->SetTarget(u""_ns);
+ // No forced download
+ aLoadState->SetFileName(VoidString());
+ return targetContext->InternalLoad(aLoadState);
+}
+
+static nsAutoCString RefMaybeNull(nsIURI* aURI) {
+ nsAutoCString result;
+ if (NS_FAILED(aURI->GetRef(result))) {
+ result.SetIsVoid(true);
+ }
+ return result;
+}
+
+uint32_t nsDocShell::GetSameDocumentNavigationFlags(nsIURI* aNewURI) {
+ uint32_t flags = LOCATION_CHANGE_SAME_DOCUMENT;
+
+ bool equal = false;
+ if (mCurrentURI &&
+ NS_SUCCEEDED(mCurrentURI->EqualsExceptRef(aNewURI, &equal)) && equal &&
+ RefMaybeNull(mCurrentURI) != RefMaybeNull(aNewURI)) {
+ flags |= LOCATION_CHANGE_HASHCHANGE;
+ }
+
+ return flags;
+}
+
+bool nsDocShell::IsSameDocumentNavigation(nsDocShellLoadState* aLoadState,
+ SameDocumentNavigationState& aState) {
+ MOZ_ASSERT(aLoadState);
+ if (!(aLoadState->LoadType() == LOAD_NORMAL ||
+ aLoadState->LoadType() == LOAD_STOP_CONTENT ||
+ LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(),
+ LOAD_FLAGS_REPLACE_HISTORY) ||
+ aLoadState->LoadType() == LOAD_HISTORY ||
+ aLoadState->LoadType() == LOAD_LINK)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> currentURI = mCurrentURI;
+
+ nsresult rvURINew = aLoadState->URI()->GetRef(aState.mNewHash);
+ if (NS_SUCCEEDED(rvURINew)) {
+ rvURINew = aLoadState->URI()->GetHasRef(&aState.mNewURIHasRef);
+ }
+
+ if (currentURI && NS_SUCCEEDED(rvURINew)) {
+ nsresult rvURIOld = currentURI->GetRef(aState.mCurrentHash);
+ if (NS_SUCCEEDED(rvURIOld)) {
+ rvURIOld = currentURI->GetHasRef(&aState.mCurrentURIHasRef);
+ }
+ if (NS_SUCCEEDED(rvURIOld)) {
+ if (NS_FAILED(currentURI->EqualsExceptRef(aLoadState->URI(),
+ &aState.mSameExceptHashes))) {
+ aState.mSameExceptHashes = false;
+ }
+ }
+ }
+
+ if (!aState.mSameExceptHashes && currentURI && NS_SUCCEEDED(rvURINew)) {
+ // Maybe aLoadState->URI() came from the exposable form of currentURI?
+ nsCOMPtr<nsIURI> currentExposableURI =
+ nsIOService::CreateExposableURI(currentURI);
+ nsresult rvURIOld = currentExposableURI->GetRef(aState.mCurrentHash);
+ if (NS_SUCCEEDED(rvURIOld)) {
+ rvURIOld = currentExposableURI->GetHasRef(&aState.mCurrentURIHasRef);
+ }
+ if (NS_SUCCEEDED(rvURIOld)) {
+ if (NS_FAILED(currentExposableURI->EqualsExceptRef(
+ aLoadState->URI(), &aState.mSameExceptHashes))) {
+ aState.mSameExceptHashes = false;
+ }
+ // HTTPS-Only Mode upgrades schemes from http to https in Necko, hence we
+ // have to perform a special check here to avoid an actual navigation. If
+ // HTTPS-Only Mode is enabled and the two URIs are same-origin (modulo the
+ // fact that the new URI is currently http), then set mSameExceptHashes to
+ // true and only perform a fragment navigation.
+ if (!aState.mSameExceptHashes) {
+ if (nsCOMPtr<nsIChannel> docChannel = GetCurrentDocChannel()) {
+ nsCOMPtr<nsILoadInfo> docLoadInfo = docChannel->LoadInfo();
+ if (!docLoadInfo->GetLoadErrorPage() &&
+ nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(
+ currentExposableURI, aLoadState->URI(), docLoadInfo)) {
+ uint32_t status = docLoadInfo->GetHttpsOnlyStatus();
+ if (status & (nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED |
+ nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) {
+ // At this point the requested URI is for sure a fragment
+ // navigation via HTTP and HTTPS-Only mode or HTTPS-First is
+ // enabled. Also it is not interfering the upgrade order of
+ // https://searchfox.org/mozilla-central/source/netwerk/base/nsNetUtil.cpp#2948-2953.
+ // Since we are on an HTTPS site the fragment
+ // navigation should also be an HTTPS.
+ // For that reason we should upgrade the URI to HTTPS.
+ aState.mSecureUpgradeURI = true;
+ aState.mSameExceptHashes = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (mozilla::SessionHistoryInParent()) {
+ if (mActiveEntry && aLoadState->LoadIsFromSessionHistory()) {
+ aState.mHistoryNavBetweenSameDoc = mActiveEntry->SharesDocumentWith(
+ aLoadState->GetLoadingSessionHistoryInfo()->mInfo);
+ }
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell::IsSameDocumentNavigation %p NavBetweenSameDoc=%d",
+ this, aState.mHistoryNavBetweenSameDoc));
+ } else {
+ if (mOSHE && aLoadState->LoadIsFromSessionHistory()) {
+ // We're doing a history load.
+
+ mOSHE->SharesDocumentWith(aLoadState->SHEntry(),
+ &aState.mHistoryNavBetweenSameDoc);
+ }
+ }
+
+ // A same document navigation happens when we navigate between two SHEntries
+ // for the same document. We do a same document navigation under two
+ // circumstances. Either
+ //
+ // a) we're navigating between two different SHEntries which share a
+ // document, or
+ //
+ // b) we're navigating to a new shentry whose URI differs from the
+ // current URI only in its hash, the new hash is non-empty, and
+ // we're not doing a POST.
+ //
+ // The restriction that the SHEntries in (a) must be different ensures
+ // that history.go(0) and the like trigger full refreshes, rather than
+ // same document navigations.
+ if (!mozilla::SessionHistoryInParent()) {
+ bool doSameDocumentNavigation =
+ (aState.mHistoryNavBetweenSameDoc && mOSHE != aLoadState->SHEntry()) ||
+ (!aLoadState->SHEntry() && !aLoadState->PostDataStream() &&
+ aState.mSameExceptHashes && aState.mNewURIHasRef);
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p NavBetweenSameDoc=%d is same doc = %d", this,
+ aState.mHistoryNavBetweenSameDoc, doSameDocumentNavigation));
+ return doSameDocumentNavigation;
+ }
+
+ if (aState.mHistoryNavBetweenSameDoc &&
+ !aLoadState->GetLoadingSessionHistoryInfo()->mLoadingCurrentEntry) {
+ return true;
+ }
+
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ ("nsDocShell::IsSameDocumentNavigation %p !LoadIsFromSessionHistory=%s "
+ "!PostDataStream: %s mSameExceptHashes: %s mNewURIHasRef: %s",
+ this, !aLoadState->LoadIsFromSessionHistory() ? "true" : "false",
+ !aLoadState->PostDataStream() ? "true" : "false",
+ aState.mSameExceptHashes ? "true" : "false",
+ aState.mNewURIHasRef ? "true" : "false"));
+ return !aLoadState->LoadIsFromSessionHistory() &&
+ !aLoadState->PostDataStream() && aState.mSameExceptHashes &&
+ aState.mNewURIHasRef;
+}
+
+nsresult nsDocShell::HandleSameDocumentNavigation(
+ nsDocShellLoadState* aLoadState, SameDocumentNavigationState& aState,
+ bool& aSameDocument) {
+ aSameDocument = true;
+#ifdef DEBUG
+ SameDocumentNavigationState state;
+ MOZ_ASSERT(IsSameDocumentNavigation(aLoadState, state));
+#endif
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell::HandleSameDocumentNavigation %p %s -> %s", this,
+ mCurrentURI->GetSpecOrDefault().get(),
+ aLoadState->URI()->GetSpecOrDefault().get()));
+
+ RefPtr<Document> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+ doc->DoNotifyPossibleTitleChange();
+
+ nsCOMPtr<nsIURI> currentURI = mCurrentURI;
+
+ // We need to upgrade the new URI from http: to https:
+ nsCOMPtr<nsIURI> newURI = aLoadState->URI();
+ if (aState.mSecureUpgradeURI) {
+ MOZ_TRY(NS_GetSecureUpgradedURI(aLoadState->URI(), getter_AddRefs(newURI)));
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Upgraded URI to %s", newURI->GetSpecOrDefault().get()));
+ }
+
+ if (StaticPrefs::dom_security_setdocumenturi()) {
+ // check if aLoadState->URI(), principalURI, mCurrentURI are same origin
+ // skip handling otherwise
+ nsCOMPtr<nsIPrincipal> origPrincipal = doc->NodePrincipal();
+ nsCOMPtr<nsIURI> principalURI = origPrincipal->GetURI();
+ if (origPrincipal->GetIsNullPrincipal()) {
+ nsCOMPtr<nsIPrincipal> precursor = origPrincipal->GetPrecursorPrincipal();
+ if (precursor) {
+ principalURI = precursor->GetURI();
+ }
+ }
+
+ auto isLoadableViaInternet = [](nsIURI* uri) {
+ return (uri && (net::SchemeIsHTTP(uri) || net::SchemeIsHTTPS(uri)));
+ };
+
+ if (isLoadableViaInternet(principalURI) &&
+ isLoadableViaInternet(mCurrentURI) && isLoadableViaInternet(newURI)) {
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ if (!NS_SUCCEEDED(
+ ssm->CheckSameOriginURI(newURI, principalURI, false, false)) ||
+ !NS_SUCCEEDED(ssm->CheckSameOriginURI(mCurrentURI, principalURI,
+ false, false))) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell[%p]: possible violation of the same origin policy "
+ "during same document navigation",
+ this));
+ aSameDocument = false;
+ return NS_OK;
+ }
+ }
+ }
+
+#ifdef DEBUG
+ if (aState.mSameExceptHashes) {
+ bool sameExceptHashes = false;
+ currentURI->EqualsExceptRef(newURI, &sameExceptHashes);
+ MOZ_ASSERT(sameExceptHashes);
+ }
+#endif
+
+ // Save the position of the scrollers.
+ nsPoint scrollPos = GetCurScrollPos();
+
+ // Reset mLoadType to its original value once we exit this block, because this
+ // same document navigation might have started after a normal, network load,
+ // and we don't want to clobber its load type. See bug 737307.
+ AutoRestore<uint32_t> loadTypeResetter(mLoadType);
+
+ // If a non-same-document-navigation (i.e., a network load) is pending, make
+ // this a replacement load, so that we don't add a SHEntry here and the
+ // network load goes into the SHEntry it expects to.
+ if (JustStartedNetworkLoad() && (aLoadState->LoadType() & LOAD_CMD_NORMAL)) {
+ mLoadType = LOAD_NORMAL_REPLACE;
+ } else {
+ mLoadType = aLoadState->LoadType();
+ }
+
+ mURIResultedInDocument = true;
+
+ nsCOMPtr<nsISHEntry> oldLSHE = mLSHE;
+
+ // we need to assign aLoadState->SHEntry() to mLSHE right here, so that on
+ // History loads, SetCurrentURI() called from OnNewURI() will send proper
+ // onLocationChange() notifications to the browser to update back/forward
+ // buttons.
+ SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(aLoadState->SHEntry()),
+ Nothing());
+ UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> oldLoadingEntry;
+ mLoadingEntry.swap(oldLoadingEntry);
+ if (aLoadState->GetLoadingSessionHistoryInfo()) {
+ mLoadingEntry = MakeUnique<LoadingSessionHistoryInfo>(
+ *aLoadState->GetLoadingSessionHistoryInfo());
+ mNeedToReportActiveAfterLoadingBecomesActive = false;
+ }
+
+ // Set the doc's URI according to the new history entry's URI.
+ doc->SetDocumentURI(newURI);
+
+ /* This is a anchor traversal within the same page.
+ * call OnNewURI() so that, this traversal will be
+ * recorded in session and global history.
+ */
+ nsCOMPtr<nsIPrincipal> newURITriggeringPrincipal, newURIPrincipalToInherit,
+ newURIPartitionedPrincipalToInherit;
+ nsCOMPtr<nsIContentSecurityPolicy> newCsp;
+ if (mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) {
+ if (mozilla::SessionHistoryInParent()) {
+ newURITriggeringPrincipal = mActiveEntry->GetTriggeringPrincipal();
+ newURIPrincipalToInherit = mActiveEntry->GetPrincipalToInherit();
+ newURIPartitionedPrincipalToInherit =
+ mActiveEntry->GetPartitionedPrincipalToInherit();
+ newCsp = mActiveEntry->GetCsp();
+ } else {
+ newURITriggeringPrincipal = mOSHE->GetTriggeringPrincipal();
+ newURIPrincipalToInherit = mOSHE->GetPrincipalToInherit();
+ newURIPartitionedPrincipalToInherit =
+ mOSHE->GetPartitionedPrincipalToInherit();
+ newCsp = mOSHE->GetCsp();
+ }
+ } else {
+ newURITriggeringPrincipal = aLoadState->TriggeringPrincipal();
+ newURIPrincipalToInherit = doc->NodePrincipal();
+ newURIPartitionedPrincipalToInherit = doc->PartitionedPrincipal();
+ newCsp = doc->GetCsp();
+ }
+
+ uint32_t locationChangeFlags = GetSameDocumentNavigationFlags(newURI);
+
+ // Pass true for aCloneSHChildren, since we're not
+ // changing documents here, so all of our subframes are
+ // still relevant to the new session history entry.
+ //
+ // It also makes OnNewURI(...) set LOCATION_CHANGE_SAME_DOCUMENT
+ // flag on firing onLocationChange(...).
+ // Anyway, aCloneSHChildren param is simply reflecting
+ // doSameDocumentNavigation in this scope.
+ //
+ // Note: we'll actually fire onLocationChange later, in order to preserve
+ // ordering of HistoryCommit() in the parent vs onLocationChange (bug
+ // 1668126)
+ bool locationChangeNeeded = OnNewURI(
+ newURI, nullptr, newURITriggeringPrincipal, newURIPrincipalToInherit,
+ newURIPartitionedPrincipalToInherit, newCsp, true, true);
+
+ nsCOMPtr<nsIInputStream> postData;
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ uint32_t cacheKey = 0;
+
+ bool scrollRestorationIsManual = false;
+ if (!mozilla::SessionHistoryInParent()) {
+ if (mOSHE) {
+ /* save current position of scroller(s) (bug 59774) */
+ mOSHE->SetScrollPosition(scrollPos.x, scrollPos.y);
+ scrollRestorationIsManual = mOSHE->GetScrollRestorationIsManual();
+ // Get the postdata, page ident and referrer info from the current page,
+ // if the new load is being done via normal means. Note that "normal
+ // means" can be checked for just by checking for LOAD_CMD_NORMAL, given
+ // the loadType and allowScroll check above -- it filters out some
+ // LOAD_CMD_NORMAL cases that we wouldn't want here.
+ if (aLoadState->LoadType() & LOAD_CMD_NORMAL) {
+ postData = mOSHE->GetPostData();
+ cacheKey = mOSHE->GetCacheKey();
+ referrerInfo = mOSHE->GetReferrerInfo();
+ }
+
+ // Link our new SHEntry to the old SHEntry's back/forward
+ // cache data, since the two SHEntries correspond to the
+ // same document.
+ if (mLSHE) {
+ if (!aLoadState->LoadIsFromSessionHistory()) {
+ // If we're not doing a history load, scroll restoration
+ // should be inherited from the previous session history entry.
+ SetScrollRestorationIsManualOnHistoryEntry(mLSHE,
+ scrollRestorationIsManual);
+ }
+ mLSHE->AdoptBFCacheEntry(mOSHE);
+ }
+ }
+ } else {
+ if (mActiveEntry) {
+ mActiveEntry->SetScrollPosition(scrollPos.x, scrollPos.y);
+ if (mBrowsingContext) {
+ CollectWireframe();
+ if (XRE_IsParentProcess()) {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ entry->SetScrollPosition(scrollPos.x, scrollPos.y);
+ }
+ } else {
+ mozilla::Unused << ContentChild::GetSingleton()
+ ->SendSessionHistoryEntryScrollPosition(
+ mBrowsingContext, scrollPos.x,
+ scrollPos.y);
+ }
+ }
+ }
+ if (mLoadingEntry) {
+ if (!mLoadingEntry->mLoadIsFromSessionHistory) {
+ // If we're not doing a history load, scroll restoration
+ // should be inherited from the previous session history entry.
+ // XXX This needs most probably tweaks once fragment navigation is
+ // fixed to work with session-history-in-parent.
+ SetScrollRestorationIsManualOnHistoryEntry(nullptr,
+ scrollRestorationIsManual);
+ }
+ }
+ }
+
+ // If we're doing a history load, use its scroll restoration state.
+ if (aLoadState->LoadIsFromSessionHistory()) {
+ if (mozilla::SessionHistoryInParent()) {
+ scrollRestorationIsManual = aLoadState->GetLoadingSessionHistoryInfo()
+ ->mInfo.GetScrollRestorationIsManual();
+ } else {
+ scrollRestorationIsManual =
+ aLoadState->SHEntry()->GetScrollRestorationIsManual();
+ }
+ }
+
+ /* Assign mLSHE to mOSHE. This will either be a new entry created
+ * by OnNewURI() for normal loads or aLoadState->SHEntry() for history
+ * loads.
+ */
+ if (!mozilla::SessionHistoryInParent()) {
+ if (mLSHE) {
+ SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE));
+ // Save the postData obtained from the previous page
+ // in to the session history entry created for the
+ // anchor page, so that any history load of the anchor
+ // page will restore the appropriate postData.
+ if (postData) {
+ mOSHE->SetPostData(postData);
+ }
+
+ // Make sure we won't just repost without hitting the
+ // cache first
+ if (cacheKey != 0) {
+ mOSHE->SetCacheKey(cacheKey);
+ }
+
+ // As the document has not changed, the referrer info hasn't changed too,
+ // so we can just copy it over.
+ if (referrerInfo) {
+ mOSHE->SetReferrerInfo(referrerInfo);
+ }
+ }
+
+ /* Set the title for the SH entry for this target url so that
+ * SH menus in go/back/forward buttons won't be empty for this.
+ * Note, this happens on mOSHE (and mActiveEntry in the future) because of
+ * the code above.
+ * Note, when session history lives in the parent process, this does not
+ * update the title there.
+ */
+ SetTitleOnHistoryEntry(false);
+ } else {
+ if (aLoadState->LoadIsFromSessionHistory()) {
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ ("Moving the loading entry to the active entry on nsDocShell %p to "
+ "%s",
+ this, mLoadingEntry->mInfo.GetURI()->GetSpecOrDefault().get()));
+
+ nsCOMPtr<nsILayoutHistoryState> currentLayoutHistoryState;
+ if (mActiveEntry) {
+ currentLayoutHistoryState = mActiveEntry->GetLayoutHistoryState();
+ }
+
+ UniquePtr<SessionHistoryInfo> previousActiveEntry(mActiveEntry.release());
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(mLoadingEntry->mInfo);
+ if (currentLayoutHistoryState) {
+ // Restore the existing nsILayoutHistoryState object, since it is
+ // possibly being used by the layout. When doing a new load, the
+ // shared state is copied from the existing active entry, so this
+ // special case is needed only with the history loads.
+ mActiveEntry->SetLayoutHistoryState(currentLayoutHistoryState);
+ }
+
+ if (cacheKey != 0) {
+ mActiveEntry->SetCacheKey(cacheKey);
+ }
+ // We're passing in mCurrentURI, which could be null. SessionHistoryCommit
+ // does require a non-null uri if this is for a refresh load of the same
+ // URI, but in that case mCurrentURI won't be null here.
+ mBrowsingContext->SessionHistoryCommit(
+ *mLoadingEntry, mLoadType, mCurrentURI, previousActiveEntry.get(),
+ true, true,
+ /* No expiration update on the same document loads*/
+ false, cacheKey);
+ // FIXME Need to set postdata.
+
+ // Set the title for the SH entry for this target url so that
+ // SH menus in go/back/forward buttons won't be empty for this.
+ // Note, when session history lives in the parent process, this does not
+ // update the title there.
+ SetTitleOnHistoryEntry(false);
+ } else {
+ Maybe<bool> scrollRestorationIsManual;
+ if (mActiveEntry) {
+ scrollRestorationIsManual.emplace(
+ mActiveEntry->GetScrollRestorationIsManual());
+
+ // Get the postdata, page ident and referrer info from the current page,
+ // if the new load is being done via normal means. Note that "normal
+ // means" can be checked for just by checking for LOAD_CMD_NORMAL, given
+ // the loadType and allowScroll check above -- it filters out some
+ // LOAD_CMD_NORMAL cases that we wouldn't want here.
+ if (aLoadState->LoadType() & LOAD_CMD_NORMAL) {
+ postData = mActiveEntry->GetPostData();
+ cacheKey = mActiveEntry->GetCacheKey();
+ referrerInfo = mActiveEntry->GetReferrerInfo();
+ }
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Creating an active entry on nsDocShell %p to %s", this,
+ newURI->GetSpecOrDefault().get()));
+ if (mActiveEntry) {
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(*mActiveEntry, newURI);
+ } else {
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(
+ newURI, newURITriggeringPrincipal, newURIPrincipalToInherit,
+ newURIPartitionedPrincipalToInherit, newCsp, mContentTypeHint);
+ }
+
+ // Save the postData obtained from the previous page in to the session
+ // history entry created for the anchor page, so that any history load of
+ // the anchor page will restore the appropriate postData.
+ if (postData) {
+ mActiveEntry->SetPostData(postData);
+ }
+
+ // Make sure we won't just repost without hitting the
+ // cache first
+ if (cacheKey != 0) {
+ mActiveEntry->SetCacheKey(cacheKey);
+ }
+
+ // As the document has not changed, the referrer info hasn't changed too,
+ // so we can just copy it over.
+ if (referrerInfo) {
+ mActiveEntry->SetReferrerInfo(referrerInfo);
+ }
+
+ // Set the title for the SH entry for this target url so that
+ // SH menus in go/back/forward buttons won't be empty for this.
+ mActiveEntry->SetTitle(mTitle);
+
+ if (scrollRestorationIsManual.isSome()) {
+ mActiveEntry->SetScrollRestorationIsManual(
+ scrollRestorationIsManual.value());
+ }
+
+ if (LOAD_TYPE_HAS_FLAGS(mLoadType, LOAD_FLAGS_REPLACE_HISTORY)) {
+ mBrowsingContext->ReplaceActiveSessionHistoryEntry(mActiveEntry.get());
+ } else {
+ mBrowsingContext->IncrementHistoryEntryCountForBrowsingContext();
+ // FIXME We should probably just compute mChildOffset in the parent
+ // instead of passing it over IPC here.
+ mBrowsingContext->SetActiveSessionHistoryEntry(
+ Some(scrollPos), mActiveEntry.get(), mLoadType, cacheKey);
+ // FIXME Do we need to update mPreviousEntryIndex and mLoadedEntryIndex?
+ }
+ }
+ }
+
+ if (locationChangeNeeded) {
+ FireOnLocationChange(this, nullptr, newURI, locationChangeFlags);
+ }
+
+ /* Restore the original LSHE if we were loading something
+ * while same document navigation was initiated.
+ */
+ SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(oldLSHE), Nothing());
+ mLoadingEntry.swap(oldLoadingEntry);
+
+ /* Set the title for the Global History entry for this anchor url.
+ */
+ UpdateGlobalHistoryTitle(newURI);
+
+ SetDocCurrentStateObj(mOSHE, mActiveEntry.get());
+
+ // Inform the favicon service that the favicon for oldURI also
+ // applies to newURI.
+ CopyFavicon(currentURI, newURI, UsePrivateBrowsing());
+
+ RefPtr<nsGlobalWindowOuter> scriptGlobal = mScriptGlobal;
+ nsCOMPtr<nsPIDOMWindowInner> win =
+ scriptGlobal ? scriptGlobal->GetCurrentInnerWindow() : nullptr;
+
+ // ScrollToAnchor doesn't necessarily cause us to scroll the window;
+ // the function decides whether a scroll is appropriate based on the
+ // arguments it receives. But even if we don't end up scrolling,
+ // ScrollToAnchor performs other important tasks, such as informing
+ // the presShell that we have a new hash. See bug 680257.
+ nsresult rv = ScrollToAnchor(aState.mCurrentURIHasRef, aState.mNewURIHasRef,
+ aState.mNewHash, aLoadState->LoadType());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* restore previous position of scroller(s), if we're moving
+ * back in history (bug 59774)
+ */
+ nscoord bx = 0;
+ nscoord by = 0;
+ bool needsScrollPosUpdate = false;
+ if ((mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) &&
+ (aLoadState->LoadType() == LOAD_HISTORY ||
+ aLoadState->LoadType() == LOAD_RELOAD_NORMAL) &&
+ !scrollRestorationIsManual) {
+ needsScrollPosUpdate = true;
+ if (mozilla::SessionHistoryInParent()) {
+ mActiveEntry->GetScrollPosition(&bx, &by);
+ } else {
+ mOSHE->GetScrollPosition(&bx, &by);
+ }
+ }
+
+ // Dispatch the popstate and hashchange events, as appropriate.
+ //
+ // The event dispatch below can cause us to re-enter script and
+ // destroy the docshell, nulling out mScriptGlobal. Hold a stack
+ // reference to avoid null derefs. See bug 914521.
+ if (win) {
+ // Fire a hashchange event URIs differ, and only in their hashes.
+ bool doHashchange = aState.mSameExceptHashes &&
+ (aState.mCurrentURIHasRef != aState.mNewURIHasRef ||
+ !aState.mCurrentHash.Equals(aState.mNewHash));
+
+ if (aState.mHistoryNavBetweenSameDoc || doHashchange) {
+ win->DispatchSyncPopState();
+ }
+
+ if (needsScrollPosUpdate && win->HasActiveDocument()) {
+ SetCurScrollPosEx(bx, by);
+ }
+
+ if (doHashchange) {
+ // Note that currentURI hasn't changed because it's on the
+ // stack, so we can just use it directly as the old URI.
+ win->DispatchAsyncHashchange(currentURI, newURI);
+ }
+ }
+
+ return NS_OK;
+}
+
+static bool NavigationShouldTakeFocus(nsDocShell* aDocShell,
+ nsDocShellLoadState* aLoadState) {
+ if (!aLoadState->AllowFocusMove()) {
+ return false;
+ }
+ if (!aLoadState->HasValidUserGestureActivation()) {
+ return false;
+ }
+ const auto& sourceBC = aLoadState->SourceBrowsingContext();
+ if (!sourceBC || !sourceBC->IsActive()) {
+ // If the navigation didn't come from a foreground tab, then we don't steal
+ // focus.
+ return false;
+ }
+ auto* bc = aDocShell->GetBrowsingContext();
+ if (sourceBC.get() == bc) {
+ // If it comes from the same tab / frame, don't steal focus either.
+ return false;
+ }
+ auto* fm = nsFocusManager::GetFocusManager();
+ if (fm && bc->IsActive() && fm->IsInActiveWindow(bc)) {
+ // If we're already on the foreground tab of the foreground window, then we
+ // don't need to do this. This helps to e.g. not steal focus from the
+ // browser chrome unnecessarily.
+ return false;
+ }
+ if (auto* doc = aDocShell->GetExtantDocument()) {
+ if (doc->IsInitialDocument()) {
+ // If we're the initial load for the browsing context, the browser
+ // chrome determines what to focus. This is important because the
+ // browser chrome may want to e.g focus the url-bar
+ return false;
+ }
+ }
+ // Take loadDivertedInBackground into account so the behavior would be the
+ // same as how the tab first opened.
+ return !Preferences::GetBool("browser.tabs.loadDivertedInBackground", false);
+}
+
+uint32_t nsDocShell::GetLoadTypeForFormSubmission(
+ BrowsingContext* aTargetBC, nsDocShellLoadState* aLoadState) {
+ MOZ_ASSERT(aLoadState->IsFormSubmission());
+
+ // https://html.spec.whatwg.org/#form-submission-algorithm
+ // 22. Let historyHandling be "push".
+ // 23. If form document equals targetNavigable's active document, and
+ // form document has not yet completely loaded, then set
+ // historyHandling to "replace".
+ return GetBrowsingContext() == aTargetBC && !mEODForCurrentDocument
+ ? LOAD_NORMAL_REPLACE
+ : LOAD_LINK;
+}
+
+nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
+ Maybe<uint32_t> aCacheKey) {
+ MOZ_ASSERT(aLoadState, "need a load state!");
+ MOZ_ASSERT(aLoadState->TriggeringPrincipal(),
+ "need a valid TriggeringPrincipal");
+
+ if (!aLoadState->TriggeringPrincipal()) {
+ MOZ_ASSERT(false, "InternalLoad needs a valid triggeringPrincipal");
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(mBrowsingContext->GetPendingInitialization())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ const bool shouldTakeFocus = NavigationShouldTakeFocus(this, aLoadState);
+
+ mOriginalUriString.Truncate();
+
+ MOZ_LOG(gDocShellLeakLog, LogLevel::Debug,
+ ("DOCSHELL %p InternalLoad %s\n", this,
+ aLoadState->URI()->GetSpecOrDefault().get()));
+
+ NS_ENSURE_TRUE(IsValidLoadType(aLoadState->LoadType()), NS_ERROR_INVALID_ARG);
+
+ // Cancel loads coming from Docshells that are being destroyed.
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv = EnsureScriptEnvironment();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // If we have a target to move to, do that now.
+ if (!aLoadState->Target().IsEmpty()) {
+ return PerformRetargeting(aLoadState);
+ }
+
+ // This is the non-retargeting load path, we've already set the right loadtype
+ // for form submissions in nsDocShell::OnLinkClickSync.
+ if (aLoadState->TargetBrowsingContext().IsNull()) {
+ aLoadState->SetTargetBrowsingContext(GetBrowsingContext());
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ aLoadState->TargetBrowsingContext() == GetBrowsingContext(),
+ "Load must be targeting this BrowsingContext");
+
+ MOZ_TRY(CheckDisallowedJavascriptLoad(aLoadState));
+
+ // If we don't have a target, we're loading into ourselves, and our load
+ // delegate may want to intercept that load.
+ SameDocumentNavigationState sameDocumentNavigationState;
+ bool sameDocument =
+ IsSameDocumentNavigation(aLoadState, sameDocumentNavigationState) &&
+ !aLoadState->GetPendingRedirectedChannel();
+
+ // Note: We do this check both here and in BrowsingContext::
+ // LoadURI/InternalLoad, since document-specific sandbox flags are only
+ // available in the process triggering the load, and we don't want the target
+ // process to have to trust the triggering process to do the appropriate
+ // checks for the BrowsingContext's sandbox flags.
+ MOZ_TRY(mBrowsingContext->CheckSandboxFlags(aLoadState));
+
+ NS_ENSURE_STATE(!HasUnloadedParent());
+
+ rv = CheckLoadingPermissions();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (mFiredUnloadEvent) {
+ if (IsOKToLoadURI(aLoadState->URI())) {
+ MOZ_ASSERT(aLoadState->Target().IsEmpty(),
+ "Shouldn't have a window target here!");
+
+ // If this is a replace load, make whatever load triggered
+ // the unload event also a replace load, so we don't
+ // create extra history entries.
+ if (LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(),
+ LOAD_FLAGS_REPLACE_HISTORY)) {
+ mLoadType = LOAD_NORMAL_REPLACE;
+ }
+
+ // Do this asynchronously
+ nsCOMPtr<nsIRunnable> ev = new InternalLoadEvent(this, aLoadState);
+ return Dispatch(ev.forget());
+ }
+
+ // Just ignore this load attempt
+ return NS_OK;
+ }
+
+ // If we are loading a URI that should inherit a security context (basically
+ // javascript: at this point), and the caller has said that principal
+ // inheritance is allowed, there are a few possible cases:
+ //
+ // 1) We are provided with the principal to inherit. In that case, we just use
+ // it.
+ //
+ // 2) The load is coming from some other application. In this case we don't
+ // want to inherit from whatever document we have loaded now, since the
+ // load is unrelated to it.
+ //
+ // 3) It's a load from our application, but does not provide an explicit
+ // principal to inherit. In that case, we want to inherit the principal of
+ // our current document, or of our parent document (if any) if we don't
+ // have a current document.
+ {
+ bool inherits;
+
+ if (!aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL) &&
+ !aLoadState->PrincipalToInherit() &&
+ (aLoadState->HasInternalLoadFlags(
+ INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL)) &&
+ NS_SUCCEEDED(nsContentUtils::URIInheritsSecurityContext(
+ aLoadState->URI(), &inherits)) &&
+ inherits) {
+ aLoadState->SetPrincipalToInherit(GetInheritedPrincipal(true));
+ }
+ // If principalToInherit is still null (e.g. if some of the conditions of
+ // were not satisfied), then no inheritance of any sort will happen: the
+ // load will just get a principal based on the URI being loaded.
+ }
+
+ // If this docshell is owned by a frameloader, make sure to cancel
+ // possible frameloader initialization before loading a new page.
+ nsCOMPtr<nsIDocShellTreeItem> parent = GetInProcessParentDocshell();
+ if (parent) {
+ RefPtr<Document> doc = parent->GetDocument();
+ if (doc) {
+ doc->TryCancelFrameLoaderInitialization(this);
+ }
+ }
+
+ // Before going any further vet loads initiated by external programs.
+ if (aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL)) {
+ MOZ_DIAGNOSTIC_ASSERT(aLoadState->LoadType() == LOAD_NORMAL);
+
+ // Disallow external chrome: loads targetted at content windows
+ if (SchemeIsChrome(aLoadState->URI())) {
+ NS_WARNING("blocked external chrome: url -- use '--chrome' option");
+ return NS_ERROR_FAILURE;
+ }
+
+ // clear the decks to prevent context bleed-through (bug 298255)
+ rv = CreateAboutBlankDocumentViewer(nullptr, nullptr, nullptr, nullptr,
+ /* aIsInitialDocument */ false);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ mAllowKeywordFixup = aLoadState->HasInternalLoadFlags(
+ INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP);
+ mURIResultedInDocument = false; // reset the clock...
+
+ // See if this is actually a load between two history entries for the same
+ // document. If the process fails, or if we successfully navigate within the
+ // same document, return.
+ if (sameDocument) {
+ nsresult rv = HandleSameDocumentNavigation(
+ aLoadState, sameDocumentNavigationState, sameDocument);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (shouldTakeFocus) {
+ mBrowsingContext->Focus(CallerType::System, IgnoreErrors());
+ }
+ if (sameDocument) {
+ return rv;
+ }
+ }
+
+ // mDocumentViewer->PermitUnload can destroy |this| docShell, which
+ // causes the next call of CanSavePresentation to crash.
+ // Hold onto |this| until we return, to prevent a crash from happening.
+ // (bug#331040)
+ nsCOMPtr<nsIDocShell> kungFuDeathGrip(this);
+
+ // Don't init timing for javascript:, since it generally doesn't
+ // actually start a load or anything. If it does, we'll init
+ // timing then, from OnStateChange.
+
+ // XXXbz mTiming should know what channel it's for, so we don't
+ // need this hackery.
+ bool toBeReset = false;
+ bool isJavaScript = SchemeIsJavascript(aLoadState->URI());
+
+ if (!isJavaScript) {
+ toBeReset = MaybeInitTiming();
+ }
+ bool isNotDownload = aLoadState->FileName().IsVoid();
+ if (mTiming && isNotDownload) {
+ mTiming->NotifyBeforeUnload();
+ }
+ // Check if the page doesn't want to be unloaded. The javascript:
+ // protocol handler deals with this for javascript: URLs.
+ if (!isJavaScript && isNotDownload &&
+ !aLoadState->NotifiedBeforeUnloadListeners() && mDocumentViewer) {
+ bool okToUnload;
+
+ // Check if request is exempted from HTTPSOnlyMode and if https-first is
+ // enabled, if so it means:
+ // * https-first failed to upgrade request to https
+ // * we already asked for permission to unload and the user accepted
+ // otherwise we wouldn't be here.
+ bool isPrivateWin = GetOriginAttributes().mPrivateBrowsingId > 0;
+ bool isHistoryOrReload = false;
+ uint32_t loadType = aLoadState->LoadType();
+
+ // Check if request is a reload.
+ if (loadType == LOAD_RELOAD_NORMAL ||
+ loadType == LOAD_RELOAD_BYPASS_CACHE ||
+ loadType == LOAD_RELOAD_BYPASS_PROXY ||
+ loadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE ||
+ loadType == LOAD_HISTORY) {
+ isHistoryOrReload = true;
+ }
+
+ // If it isn't a reload, the request already failed to be upgraded and
+ // https-first is enabled then don't ask the user again for permission to
+ // unload and just unload.
+ if (!isHistoryOrReload && aLoadState->IsExemptFromHTTPSFirstMode() &&
+ nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) {
+ rv = mDocumentViewer->PermitUnload(
+ nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload,
+ &okToUnload);
+ } else {
+ rv = mDocumentViewer->PermitUnload(&okToUnload);
+ }
+
+ if (NS_SUCCEEDED(rv) && !okToUnload) {
+ // The user chose not to unload the page, interrupt the
+ // load.
+ MaybeResetInitTiming(toBeReset);
+ return NS_OK;
+ }
+ }
+
+ if (mTiming && isNotDownload) {
+ mTiming->NotifyUnloadAccepted(mCurrentURI);
+ }
+
+ // In e10s, in the parent process, we refuse to load anything other than
+ // "safe" resources that we ship or trust enough to give "special" URLs.
+ // Similar check will be performed by the ParentProcessDocumentChannel if in
+ // use.
+ if (XRE_IsE10sParentProcess() &&
+ !DocumentChannel::CanUseDocumentChannel(aLoadState->URI()) &&
+ !CanLoadInParentProcess(aLoadState->URI())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Whenever a top-level browsing context is navigated, the user agent MUST
+ // lock the orientation of the document to the document's default
+ // orientation. We don't explicitly check for a top-level browsing context
+ // here because orientation is only set on top-level browsing contexts.
+ if (mBrowsingContext->GetOrientationLock() != hal::ScreenOrientation::None) {
+ MOZ_ASSERT(mBrowsingContext->IsTop());
+ MOZ_ALWAYS_SUCCEEDS(
+ mBrowsingContext->SetOrientationLock(hal::ScreenOrientation::None));
+ if (mBrowsingContext->IsActive()) {
+ ScreenOrientation::UpdateActiveOrientationLock(
+ hal::ScreenOrientation::None);
+ }
+ }
+
+ // Check for saving the presentation here, before calling Stop().
+ // This is necessary so that we can catch any pending requests.
+ // Since the new request has not been created yet, we pass null for the
+ // new request parameter.
+ // Also pass nullptr for the document, since it doesn't affect the return
+ // value for our purposes here.
+ bool savePresentation =
+ CanSavePresentation(aLoadState->LoadType(), nullptr, nullptr,
+ /* aReportBFCacheComboTelemetry */ true);
+
+ // nsDocShell::CanSavePresentation is for non-SHIP version only. Do a
+ // separate check for SHIP so that we know if there are ongoing requests
+ // before calling Stop() below.
+ if (mozilla::SessionHistoryInParent()) {
+ Document* document = GetDocument();
+ uint32_t flags = 0;
+ if (document && !document->CanSavePresentation(nullptr, flags, true)) {
+ // This forces some flags into the WindowGlobalParent's mBFCacheStatus,
+ // which we'll then use in CanonicalBrowsingContext::AllowedInBFCache,
+ // and in particular we'll store BFCacheStatus::REQUEST if needed.
+ // Also, we want to report all the flags to the parent process here (and
+ // not just BFCacheStatus::NOT_ALLOWED), so that it can update the
+ // telemetry data correctly.
+ document->DisallowBFCaching(flags);
+ }
+ }
+
+ // Don't stop current network activity for javascript: URL's since
+ // they might not result in any data, and thus nothing should be
+ // stopped in those cases. In the case where they do result in
+ // data, the javascript: URL channel takes care of stopping
+ // current network activity.
+ if (!isJavaScript && isNotDownload) {
+ // Stop any current network activity.
+ // Also stop content if this is a zombie doc. otherwise
+ // the onload will be delayed by other loads initiated in the
+ // background by the first document that
+ // didn't fully load before the next load was initiated.
+ // If not a zombie, don't stop content until data
+ // starts arriving from the new URI...
+
+ if ((mDocumentViewer && mDocumentViewer->GetPreviousViewer()) ||
+ LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(), LOAD_FLAGS_STOP_CONTENT)) {
+ rv = Stop(nsIWebNavigation::STOP_ALL);
+ } else {
+ rv = Stop(nsIWebNavigation::STOP_NETWORK);
+ }
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ mLoadType = aLoadState->LoadType();
+
+ // aLoadState->SHEntry() should be assigned to mLSHE, only after Stop() has
+ // been called. But when loading an error page, do not clear the
+ // mLSHE for the real page.
+ if (mLoadType != LOAD_ERROR_PAGE) {
+ SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(aLoadState->SHEntry()),
+ Nothing());
+ if (aLoadState->LoadIsFromSessionHistory() &&
+ !mozilla::SessionHistoryInParent()) {
+ // We're making history navigation or a reload. Make sure our history ID
+ // points to the same ID as SHEntry's docshell ID.
+ nsID historyID = {};
+ aLoadState->SHEntry()->GetDocshellID(historyID);
+
+ Unused << mBrowsingContext->SetHistoryID(historyID);
+ }
+ }
+
+ mSavingOldViewer = savePresentation;
+
+ // If we have a saved content viewer in history, restore and show it now.
+ if (aLoadState->LoadIsFromSessionHistory() &&
+ (mLoadType & LOAD_CMD_HISTORY)) {
+ // https://html.spec.whatwg.org/#history-traversal:
+ // To traverse the history
+ // "If entry has a different Document object than the current entry, then
+ // run the following substeps: Remove any tasks queued by the history
+ // traversal task source..."
+ // Same document object case was handled already above with
+ // HandleSameDocumentNavigation call.
+ RefPtr<ChildSHistory> shistory = GetRootSessionHistory();
+ if (shistory) {
+ shistory->RemovePendingHistoryNavigations();
+ }
+ if (!mozilla::SessionHistoryInParent()) {
+ // It's possible that the previous viewer of mDocumentViewer is the
+ // viewer that will end up in aLoadState->SHEntry() when it gets closed.
+ // If that's the case, we need to go ahead and force it into its shentry
+ // so we can restore it.
+ if (mDocumentViewer) {
+ nsCOMPtr<nsIDocumentViewer> prevViewer =
+ mDocumentViewer->GetPreviousViewer();
+ if (prevViewer) {
+#ifdef DEBUG
+ nsCOMPtr<nsIDocumentViewer> prevPrevViewer =
+ prevViewer->GetPreviousViewer();
+ NS_ASSERTION(!prevPrevViewer, "Should never have viewer chain here");
+#endif
+ nsCOMPtr<nsISHEntry> viewerEntry;
+ prevViewer->GetHistoryEntry(getter_AddRefs(viewerEntry));
+ if (viewerEntry == aLoadState->SHEntry()) {
+ // Make sure this viewer ends up in the right place
+ mDocumentViewer->SetPreviousViewer(nullptr);
+ prevViewer->Destroy();
+ }
+ }
+ }
+ nsCOMPtr<nsISHEntry> oldEntry = mOSHE;
+ bool restoring;
+ rv = RestorePresentation(aLoadState->SHEntry(), &restoring);
+ if (restoring) {
+ Telemetry::Accumulate(Telemetry::BFCACHE_PAGE_RESTORED, true);
+ return rv;
+ }
+ Telemetry::Accumulate(Telemetry::BFCACHE_PAGE_RESTORED, false);
+
+ // We failed to restore the presentation, so clean up.
+ // Both the old and new history entries could potentially be in
+ // an inconsistent state.
+ if (NS_FAILED(rv)) {
+ if (oldEntry) {
+ oldEntry->SyncPresentationState();
+ }
+
+ aLoadState->SHEntry()->SyncPresentationState();
+ }
+ }
+ }
+
+ bool isTopLevelDoc = mBrowsingContext->IsTopContent();
+
+ OriginAttributes attrs = GetOriginAttributes();
+ attrs.SetFirstPartyDomain(isTopLevelDoc, aLoadState->URI());
+
+ PredictorLearn(aLoadState->URI(), nullptr,
+ nsINetworkPredictor::LEARN_LOAD_TOPLEVEL, attrs);
+ PredictorPredict(aLoadState->URI(), nullptr,
+ nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr);
+
+ nsCOMPtr<nsIRequest> req;
+ rv = DoURILoad(aLoadState, aCacheKey, getter_AddRefs(req));
+
+ if (NS_SUCCEEDED(rv)) {
+ if (shouldTakeFocus) {
+ mBrowsingContext->Focus(CallerType::System, IgnoreErrors());
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ nsCOMPtr<nsIChannel> chan(do_QueryInterface(req));
+ UnblockEmbedderLoadEventForFailure();
+ nsCOMPtr<nsIURI> uri = aLoadState->URI();
+ if (DisplayLoadError(rv, uri, nullptr, chan) &&
+ // FIXME: At this point code was using internal load flags, but checking
+ // non-internal load flags?
+ aLoadState->HasLoadFlags(LOAD_FLAGS_ERROR_LOAD_CHANGES_RV)) {
+ return NS_ERROR_LOAD_SHOWED_ERRORPAGE;
+ }
+
+ // We won't report any error if this is an unknown protocol error. The
+ // reason behind this is that it will allow enumeration of external
+ // protocols if we report an error for each unknown protocol.
+ if (NS_ERROR_UNKNOWN_PROTOCOL == rv) {
+ return NS_OK;
+ }
+ }
+
+ return rv;
+}
+
+/* static */
+bool nsDocShell::CanLoadInParentProcess(nsIURI* aURI) {
+ nsCOMPtr<nsIURI> uri = aURI;
+ // In e10s, in the parent process, we refuse to load anything other than
+ // "safe" resources that we ship or trust enough to give "special" URLs.
+ bool canLoadInParent = false;
+ if (NS_SUCCEEDED(NS_URIChainHasFlags(
+ uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &canLoadInParent)) &&
+ canLoadInParent) {
+ // We allow UI resources.
+ return true;
+ }
+ // For about: and extension-based URIs, which don't get
+ // URI_IS_UI_RESOURCE, first remove layers of view-source:, if present.
+ while (uri && uri->SchemeIs("view-source")) {
+ nsCOMPtr<nsINestedURI> nested = do_QueryInterface(uri);
+ if (nested) {
+ nested->GetInnerURI(getter_AddRefs(uri));
+ } else {
+ break;
+ }
+ }
+ // Allow about: URIs, and allow moz-extension ones if we're running
+ // extension content in the parent process.
+ if (!uri || uri->SchemeIs("about") ||
+ (!StaticPrefs::extensions_webextensions_remote() &&
+ uri->SchemeIs("moz-extension"))) {
+ return true;
+ }
+#ifdef MOZ_THUNDERBIRD
+ if (uri->SchemeIs("imap") || uri->SchemeIs("mailbox") ||
+ uri->SchemeIs("news") || uri->SchemeIs("nntp") ||
+ uri->SchemeIs("snews")) {
+ return true;
+ }
+#endif
+ nsAutoCString scheme;
+ uri->GetScheme(scheme);
+ // Allow ext+foo URIs (extension-registered custom protocols). See
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/protocol_handlers
+ if (StringBeginsWith(scheme, "ext+"_ns) &&
+ !StaticPrefs::extensions_webextensions_remote()) {
+ return true;
+ }
+ // Final exception for some legacy automated tests:
+ if (xpc::IsInAutomation() &&
+ StaticPrefs::security_allow_unsafe_parent_loads()) {
+ return true;
+ }
+ return false;
+}
+
+nsIPrincipal* nsDocShell::GetInheritedPrincipal(
+ bool aConsiderCurrentDocument, bool aConsiderPartitionedPrincipal) {
+ RefPtr<Document> document;
+ bool inheritedFromCurrent = false;
+
+ if (aConsiderCurrentDocument && mDocumentViewer) {
+ document = mDocumentViewer->GetDocument();
+ inheritedFromCurrent = true;
+ }
+
+ if (!document) {
+ nsCOMPtr<nsIDocShellTreeItem> parentItem;
+ GetInProcessSameTypeParent(getter_AddRefs(parentItem));
+ if (parentItem) {
+ document = parentItem->GetDocument();
+ }
+ }
+
+ if (!document) {
+ if (!aConsiderCurrentDocument) {
+ return nullptr;
+ }
+
+ // Make sure we end up with _something_ as the principal no matter
+ // what.If this fails, we'll just get a null docViewer and bail.
+ EnsureDocumentViewer();
+ if (!mDocumentViewer) {
+ return nullptr;
+ }
+ document = mDocumentViewer->GetDocument();
+ }
+
+ //-- Get the document's principal
+ if (document) {
+ nsIPrincipal* docPrincipal = aConsiderPartitionedPrincipal
+ ? document->PartitionedPrincipal()
+ : document->NodePrincipal();
+
+ // Don't allow loads in typeContent docShells to inherit the system
+ // principal from existing documents.
+ if (inheritedFromCurrent && mItemType == typeContent &&
+ docPrincipal->IsSystemPrincipal()) {
+ return nullptr;
+ }
+
+ return docPrincipal;
+ }
+
+ return nullptr;
+}
+
+/* static */ nsresult nsDocShell::CreateRealChannelForDocument(
+ nsIChannel** aChannel, nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIInterfaceRequestor* aCallbacks, nsLoadFlags aLoadFlags,
+ const nsAString& aSrcdoc, nsIURI* aBaseURI) {
+ nsCOMPtr<nsIChannel> channel;
+ if (aSrcdoc.IsVoid()) {
+ MOZ_TRY(NS_NewChannelInternal(getter_AddRefs(channel), aURI, aLoadInfo,
+ nullptr, // PerformanceStorage
+ nullptr, // loadGroup
+ aCallbacks, aLoadFlags));
+
+ if (aBaseURI) {
+ nsCOMPtr<nsIViewSourceChannel> vsc = do_QueryInterface(channel);
+ if (vsc) {
+ MOZ_ALWAYS_SUCCEEDS(vsc->SetBaseURI(aBaseURI));
+ }
+ }
+ } else if (SchemeIsViewSource(aURI)) {
+ // Instantiate view source handler protocol, if it doesn't exist already.
+ nsCOMPtr<nsIIOService> io(do_GetIOService());
+ MOZ_ASSERT(io);
+ nsCOMPtr<nsIProtocolHandler> handler;
+ nsresult rv =
+ io->GetProtocolHandler("view-source", getter_AddRefs(handler));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsViewSourceHandler* vsh = nsViewSourceHandler::GetInstance();
+ if (!vsh) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_TRY(vsh->NewSrcdocChannel(aURI, aBaseURI, aSrcdoc, aLoadInfo,
+ getter_AddRefs(channel)));
+ } else {
+ MOZ_TRY(NS_NewInputStreamChannelInternal(getter_AddRefs(channel), aURI,
+ aSrcdoc, "text/html"_ns, aLoadInfo,
+ true));
+ nsCOMPtr<nsIInputStreamChannel> isc = do_QueryInterface(channel);
+ MOZ_ASSERT(isc);
+ isc->SetBaseURI(aBaseURI);
+ }
+
+ if (aLoadFlags != nsIRequest::LOAD_NORMAL) {
+ nsresult rv = channel->SetLoadFlags(aLoadFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ channel.forget(aChannel);
+ return NS_OK;
+}
+
+/* static */ bool nsDocShell::CreateAndConfigureRealChannelForLoadState(
+ BrowsingContext* aBrowsingContext, nsDocShellLoadState* aLoadState,
+ LoadInfo* aLoadInfo, nsIInterfaceRequestor* aCallbacks,
+ nsDocShell* aDocShell, const OriginAttributes& aOriginAttributes,
+ nsLoadFlags aLoadFlags, uint32_t aCacheKey, nsresult& aRv,
+ nsIChannel** aChannel) {
+ MOZ_ASSERT(aLoadInfo);
+
+ nsString srcdoc = VoidString();
+ bool isSrcdoc =
+ aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_IS_SRCDOC);
+ if (isSrcdoc) {
+ srcdoc = aLoadState->SrcdocData();
+ }
+
+ aLoadInfo->SetTriggeringRemoteType(
+ aLoadState->GetEffectiveTriggeringRemoteType());
+
+ if (aLoadState->PrincipalToInherit()) {
+ aLoadInfo->SetPrincipalToInherit(aLoadState->PrincipalToInherit());
+ }
+ aLoadInfo->SetLoadTriggeredFromExternal(
+ aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL));
+ aLoadInfo->SetForceAllowDataURI(aLoadState->HasInternalLoadFlags(
+ INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI));
+ aLoadInfo->SetOriginalFrameSrcLoad(
+ aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC));
+
+ bool inheritAttrs = false;
+ if (aLoadState->PrincipalToInherit()) {
+ inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal(
+ aLoadState->PrincipalToInherit(), aLoadState->URI(),
+ true, // aInheritForAboutBlank
+ isSrcdoc);
+ }
+
+ // Strip the target query parameters before creating the channel.
+ aLoadState->MaybeStripTrackerQueryStrings(aBrowsingContext);
+
+ OriginAttributes attrs;
+
+ // Inherit origin attributes from PrincipalToInherit if inheritAttrs is
+ // true. Otherwise we just use the origin attributes from docshell.
+ if (inheritAttrs) {
+ MOZ_ASSERT(aLoadState->PrincipalToInherit(),
+ "We should have PrincipalToInherit here.");
+ attrs = aLoadState->PrincipalToInherit()->OriginAttributesRef();
+ // If firstPartyIsolation is not enabled, then PrincipalToInherit should
+ // have the same origin attributes with docshell.
+ MOZ_ASSERT_IF(!OriginAttributes::IsFirstPartyEnabled(),
+ attrs == aOriginAttributes);
+ } else {
+ attrs = aOriginAttributes;
+ attrs.SetFirstPartyDomain(IsTopLevelDoc(aBrowsingContext, aLoadInfo),
+ aLoadState->URI());
+ }
+
+ aRv = aLoadInfo->SetOriginAttributes(attrs);
+ if (NS_WARN_IF(NS_FAILED(aRv))) {
+ return false;
+ }
+
+ if (aLoadState->GetIsFromProcessingFrameAttributes()) {
+ aLoadInfo->SetIsFromProcessingFrameAttributes();
+ }
+
+ // Propagate the IsFormSubmission flag to the loadInfo.
+ if (aLoadState->IsFormSubmission()) {
+ aLoadInfo->SetIsFormSubmission(true);
+ }
+
+ aLoadInfo->SetUnstrippedURI(aLoadState->GetUnstrippedURI());
+
+ nsCOMPtr<nsIChannel> channel;
+ aRv = CreateRealChannelForDocument(getter_AddRefs(channel), aLoadState->URI(),
+ aLoadInfo, aCallbacks, aLoadFlags, srcdoc,
+ aLoadState->BaseURI());
+ NS_ENSURE_SUCCESS(aRv, false);
+
+ if (!channel) {
+ return false;
+ }
+
+ // If the HTTPS-Only mode is enabled, every insecure request gets upgraded to
+ // HTTPS by default. This behavior can be disabled through the loadinfo flag
+ // HTTPS_ONLY_EXEMPT.
+ nsHTTPSOnlyUtils::TestSitePermissionAndPotentiallyAddExemption(channel);
+
+ // hack
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
+ nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal(
+ do_QueryInterface(channel));
+ nsCOMPtr<nsIURI> referrer;
+ nsIReferrerInfo* referrerInfo = aLoadState->GetReferrerInfo();
+ if (referrerInfo) {
+ referrerInfo->GetOriginalReferrer(getter_AddRefs(referrer));
+ }
+ if (httpChannelInternal) {
+ if (aLoadState->HasInternalLoadFlags(
+ INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES)) {
+ aRv = httpChannelInternal->SetThirdPartyFlags(
+ nsIHttpChannelInternal::THIRD_PARTY_FORCE_ALLOW);
+ MOZ_ASSERT(NS_SUCCEEDED(aRv));
+ }
+ if (aLoadState->FirstParty()) {
+ aRv = httpChannelInternal->SetDocumentURI(aLoadState->URI());
+ MOZ_ASSERT(NS_SUCCEEDED(aRv));
+ } else {
+ aRv = httpChannelInternal->SetDocumentURI(referrer);
+ MOZ_ASSERT(NS_SUCCEEDED(aRv));
+ }
+ aRv = httpChannelInternal->SetRedirectMode(
+ nsIHttpChannelInternal::REDIRECT_MODE_MANUAL);
+ MOZ_ASSERT(NS_SUCCEEDED(aRv));
+ }
+
+ if (httpChannel) {
+ if (aLoadState->HeadersStream()) {
+ aRv = AddHeadersToChannel(aLoadState->HeadersStream(), httpChannel);
+ }
+ // Set the referrer explicitly
+ // Referrer is currenly only set for link clicks here.
+ if (referrerInfo) {
+ aRv = httpChannel->SetReferrerInfo(referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(aRv));
+ }
+
+ // Mark the http channel as UrgentStart for top level document loading in
+ // active tab.
+ if (IsUrgentStart(aBrowsingContext, aLoadInfo, aLoadState->LoadType())) {
+ nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel));
+ if (cos) {
+ cos->AddClassFlags(nsIClassOfService::UrgentStart);
+ }
+ }
+ }
+
+ channel->SetOriginalURI(aLoadState->OriginalURI() ? aLoadState->OriginalURI()
+ : aLoadState->URI());
+
+ const nsACString& typeHint = aLoadState->TypeHint();
+ if (!typeHint.IsVoid()) {
+ channel->SetContentType(typeHint);
+ }
+
+ const nsAString& fileName = aLoadState->FileName();
+ if (!fileName.IsVoid()) {
+ aRv = channel->SetContentDisposition(nsIChannel::DISPOSITION_ATTACHMENT);
+ NS_ENSURE_SUCCESS(aRv, false);
+ if (!fileName.IsEmpty()) {
+ aRv = channel->SetContentDispositionFilename(fileName);
+ NS_ENSURE_SUCCESS(aRv, false);
+ }
+ }
+
+ if (nsCOMPtr<nsIWritablePropertyBag2> props = do_QueryInterface(channel)) {
+ nsCOMPtr<nsIURI> referrer;
+ nsIReferrerInfo* referrerInfo = aLoadState->GetReferrerInfo();
+ if (referrerInfo) {
+ referrerInfo->GetOriginalReferrer(getter_AddRefs(referrer));
+ }
+ // save true referrer for those who need it (e.g. xpinstall whitelisting)
+ // Currently only http and ftp channels support this.
+ props->SetPropertyAsInterface(u"docshell.internalReferrer"_ns, referrer);
+
+ if (aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_FIRST_LOAD)) {
+ props->SetPropertyAsBool(u"docshell.newWindowTarget"_ns, true);
+ }
+ }
+
+ nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(channel));
+ auto loadType = aLoadState->LoadType();
+
+ if (loadType == LOAD_RELOAD_NORMAL &&
+ StaticPrefs::
+ browser_soft_reload_only_force_validate_top_level_document()) {
+ nsCOMPtr<nsICacheInfoChannel> cachingChannel = do_QueryInterface(channel);
+ if (cachingChannel) {
+ cachingChannel->SetForceValidateCacheContent(true);
+ }
+ }
+
+ // figure out if we need to set the post data stream on the channel...
+ if (aLoadState->PostDataStream()) {
+ if (nsCOMPtr<nsIFormPOSTActionChannel> postChannel =
+ do_QueryInterface(channel)) {
+ // XXX it's a bit of a hack to rewind the postdata stream here but
+ // it has to be done in case the post data is being reused multiple
+ // times.
+ nsCOMPtr<nsISeekableStream> postDataSeekable =
+ do_QueryInterface(aLoadState->PostDataStream());
+ if (postDataSeekable) {
+ aRv = postDataSeekable->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS(aRv, false);
+ }
+
+ // we really need to have a content type associated with this stream!!
+ postChannel->SetUploadStream(aLoadState->PostDataStream(), ""_ns, -1);
+
+ // Ownership of the stream has transferred to the channel, clear our
+ // reference.
+ aLoadState->SetPostDataStream(nullptr);
+ }
+
+ /* If there is a valid postdata *and* it is a History Load,
+ * set up the cache key on the channel, to retrieve the
+ * data *only* from the cache. If it is a normal reload, the
+ * cache is free to go to the server for updated postdata.
+ */
+ if (cacheChannel && aCacheKey != 0) {
+ if (loadType == LOAD_HISTORY || loadType == LOAD_RELOAD_CHARSET_CHANGE) {
+ cacheChannel->SetCacheKey(aCacheKey);
+ uint32_t loadFlags;
+ if (NS_SUCCEEDED(channel->GetLoadFlags(&loadFlags))) {
+ channel->SetLoadFlags(loadFlags |
+ nsICachingChannel::LOAD_ONLY_FROM_CACHE);
+ }
+ } else if (loadType == LOAD_RELOAD_NORMAL) {
+ cacheChannel->SetCacheKey(aCacheKey);
+ }
+ }
+ } else {
+ /* If there is no postdata, set the cache key on the channel, and
+ * do not set the LOAD_ONLY_FROM_CACHE flag, so that the channel
+ * will be free to get it from net if it is not found in cache.
+ * New cache may use it creatively on CGI pages with GET
+ * method and even on those that say "no-cache"
+ */
+ if (loadType == LOAD_HISTORY || loadType == LOAD_RELOAD_NORMAL ||
+ loadType == LOAD_RELOAD_CHARSET_CHANGE ||
+ loadType == LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE ||
+ loadType == LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE) {
+ if (cacheChannel && aCacheKey != 0) {
+ cacheChannel->SetCacheKey(aCacheKey);
+ }
+ }
+ }
+
+ if (nsCOMPtr<nsIScriptChannel> scriptChannel = do_QueryInterface(channel)) {
+ // Allow execution against our context if the principals match
+ scriptChannel->SetExecutionPolicy(nsIScriptChannel::EXECUTE_NORMAL);
+ }
+
+ if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) {
+ timedChannel->SetTimingEnabled(true);
+
+ nsString initiatorType;
+ switch (aLoadInfo->InternalContentPolicyType()) {
+ case nsIContentPolicy::TYPE_INTERNAL_EMBED:
+ initiatorType = u"embed"_ns;
+ break;
+ case nsIContentPolicy::TYPE_INTERNAL_OBJECT:
+ initiatorType = u"object"_ns;
+ break;
+ default: {
+ const auto& embedderElementType =
+ aBrowsingContext->GetEmbedderElementType();
+ if (embedderElementType) {
+ initiatorType = *embedderElementType;
+ }
+ break;
+ }
+ }
+
+ if (!initiatorType.IsEmpty()) {
+ timedChannel->SetInitiatorType(initiatorType);
+ }
+ }
+
+ nsCOMPtr<nsIURI> rpURI;
+ aLoadInfo->GetResultPrincipalURI(getter_AddRefs(rpURI));
+ Maybe<nsCOMPtr<nsIURI>> originalResultPrincipalURI;
+ aLoadState->GetMaybeResultPrincipalURI(originalResultPrincipalURI);
+ if (originalResultPrincipalURI &&
+ (!aLoadState->KeepResultPrincipalURIIfSet() || !rpURI)) {
+ // Unconditionally override, we want the replay to be equal to what has
+ // been captured.
+ aLoadInfo->SetResultPrincipalURI(originalResultPrincipalURI.ref());
+ }
+
+ if (aLoadState->OriginalURI() && aLoadState->LoadReplace()) {
+ // The LOAD_REPLACE flag and its handling here will be removed as part
+ // of bug 1319110. For now preserve its restoration here to not break
+ // any code expecting it being set specially on redirected channels.
+ // If the flag has originally been set to change result of
+ // NS_GetFinalChannelURI it won't have any effect and also won't cause
+ // any harm.
+ uint32_t loadFlags;
+ aRv = channel->GetLoadFlags(&loadFlags);
+ NS_ENSURE_SUCCESS(aRv, false);
+ channel->SetLoadFlags(loadFlags | nsIChannel::LOAD_REPLACE);
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadState->Csp();
+ if (csp) {
+ // Navigational requests that are same origin need to be upgraded in case
+ // upgrade-insecure-requests is present. Please note that for document
+ // navigations that bit is re-computed in case we encounter a server
+ // side redirect so the navigation is not same-origin anymore.
+ bool upgradeInsecureRequests = false;
+ csp->GetUpgradeInsecureRequests(&upgradeInsecureRequests);
+ if (upgradeInsecureRequests) {
+ // only upgrade if the navigation is same origin
+ nsCOMPtr<nsIPrincipal> resultPrincipal;
+ aRv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
+ channel, getter_AddRefs(resultPrincipal));
+ NS_ENSURE_SUCCESS(aRv, false);
+ if (nsContentSecurityUtils::IsConsideredSameOriginForUIR(
+ aLoadState->TriggeringPrincipal(), resultPrincipal)) {
+ aLoadInfo->SetUpgradeInsecureRequests(true);
+ }
+ }
+
+ // For document loads we store the CSP that potentially needs to
+ // be inherited by the new document, e.g. in case we are loading
+ // an opaque origin like a data: URI. The actual inheritance
+ // check happens within Document::InitCSP().
+ // Please create an actual copy of the CSP (do not share the same
+ // reference) otherwise a Meta CSP of an opaque origin will
+ // incorrectly be propagated to the embedding document.
+ RefPtr<nsCSPContext> cspToInherit = new nsCSPContext();
+ cspToInherit->InitFromOther(static_cast<nsCSPContext*>(csp.get()));
+ aLoadInfo->SetCSPToInherit(cspToInherit);
+ }
+
+ channel.forget(aChannel);
+ return true;
+}
+
+bool nsDocShell::IsAboutBlankLoadOntoInitialAboutBlank(
+ nsIURI* aURI, bool aInheritPrincipal, nsIPrincipal* aPrincipalToInherit) {
+ return NS_IsAboutBlank(aURI) && aInheritPrincipal &&
+ (aPrincipalToInherit == GetInheritedPrincipal(false)) &&
+ (!mDocumentViewer || !mDocumentViewer->GetDocument() ||
+ mDocumentViewer->GetDocument()->IsInitialDocument());
+}
+
+nsresult nsDocShell::DoURILoad(nsDocShellLoadState* aLoadState,
+ Maybe<uint32_t> aCacheKey,
+ nsIRequest** aRequest) {
+ // Double-check that we're still around to load this URI.
+ if (mIsBeingDestroyed) {
+ // Return NS_OK despite not doing anything to avoid throwing exceptions
+ // from nsLocation::SetHref if the unload handler of the existing page
+ // tears us down.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURILoader> uriLoader = components::URILoader::Service();
+ if (NS_WARN_IF(!uriLoader)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Persist and sync layout history state before we load a new uri, as this
+ // might be our last chance to do so, in the content process.
+ PersistLayoutHistoryState();
+ SynchronizeLayoutHistoryState();
+
+ nsresult rv;
+ nsContentPolicyType contentPolicyType = DetermineContentType();
+
+ if (IsSubframe()) {
+ MOZ_ASSERT(contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_IFRAME ||
+ contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_FRAME,
+ "DoURILoad thinks this is a frame and InternalLoad does not");
+
+ if (StaticPrefs::dom_block_external_protocol_in_iframes()) {
+ // Only allow URLs able to return data in iframes.
+ if (nsContentUtils::IsExternalProtocol(aLoadState->URI())) {
+ // The context to check user-interaction with for the purposes of
+ // popup-blocking.
+ //
+ // We generally want to check the context that initiated the navigation.
+ WindowContext* sourceWindowContext = [&] {
+ const MaybeDiscardedBrowsingContext& sourceBC =
+ aLoadState->SourceBrowsingContext();
+ if (!sourceBC.IsNullOrDiscarded()) {
+ if (WindowContext* wc = sourceBC.get()->GetCurrentWindowContext()) {
+ return wc;
+ }
+ }
+ return mBrowsingContext->GetParentWindowContext();
+ }();
+
+ MOZ_ASSERT(sourceWindowContext);
+ // FIXME: We can't check user-interaction against an OOP window. This is
+ // the next best thing we can really do. The load state keeps whether
+ // the navigation had a user interaction in process
+ // (aLoadState->HasValidUserGestureActivation()), but we can't really
+ // consume it, which we want to prevent popup-spamming from the same
+ // click event.
+ WindowContext* context =
+ sourceWindowContext->IsInProcess()
+ ? sourceWindowContext
+ : mBrowsingContext->GetCurrentWindowContext();
+ const bool popupBlocked = [&] {
+ const bool active = mBrowsingContext->IsActive();
+
+ // For same-origin-with-top windows, we grant a single free popup
+ // without user activation, see bug 1680721.
+ //
+ // We consume the flag now even if there's no user activation.
+ const bool hasFreePass = [&] {
+ if (!active ||
+ !(context->IsInProcess() && context->SameOriginWithTop())) {
+ return false;
+ }
+ nsGlobalWindowInner* win =
+ context->TopWindowContext()->GetInnerWindow();
+ return win && win->TryOpenExternalProtocolIframe();
+ }();
+
+ if (context->IsInProcess() &&
+ context->ConsumeTransientUserGestureActivation()) {
+ // If the user has interacted with the page, consume it.
+ return false;
+ }
+
+ // TODO(emilio): Can we remove this check? It seems like what prompted
+ // this code (bug 1514547) should be covered by transient user
+ // activation, see bug 1514547.
+ if (active &&
+ PopupBlocker::ConsumeTimerTokenForExternalProtocolIframe()) {
+ return false;
+ }
+
+ if (sourceWindowContext->CanShowPopup()) {
+ return false;
+ }
+
+ if (hasFreePass) {
+ return false;
+ }
+
+ return true;
+ }();
+
+ // No error must be returned when iframes are blocked.
+ if (popupBlocked) {
+ nsAutoString message;
+ nsresult rv = nsContentUtils::GetLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES,
+ "ExternalProtocolFrameBlockedNoUserActivation", message);
+ if (NS_SUCCEEDED(rv)) {
+ nsContentUtils::ReportToConsoleByWindowID(
+ message, nsIScriptError::warningFlag, "DOM"_ns,
+ context->InnerWindowId());
+ }
+ return NS_OK;
+ }
+ }
+ }
+
+ // Only allow view-source scheme in top-level docshells. view-source is
+ // the only scheme to which this applies at the moment due to potential
+ // timing attacks to read data from cross-origin iframes. If this widens
+ // we should add a protocol flag for whether the scheme is allowed in
+ // frames and use something like nsNetUtil::NS_URIChainHasFlags.
+ nsCOMPtr<nsIURI> tempURI = aLoadState->URI();
+ nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(tempURI);
+ while (nestedURI) {
+ // view-source should always be an nsINestedURI, loop and check the
+ // scheme on this and all inner URIs that are also nested URIs.
+ if (SchemeIsViewSource(tempURI)) {
+ return NS_ERROR_UNKNOWN_PROTOCOL;
+ }
+ nestedURI->GetInnerURI(getter_AddRefs(tempURI));
+ nestedURI = do_QueryInterface(tempURI);
+ }
+ } else {
+ MOZ_ASSERT(contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT,
+ "DoURILoad thinks this is a document and InternalLoad does not");
+ }
+
+ // We want to inherit aLoadState->PrincipalToInherit() when:
+ // 1. ChannelShouldInheritPrincipal returns true.
+ // 2. aLoadState->URI() is not data: URI, or data: URI is not
+ // configured as unique opaque origin.
+ bool inheritPrincipal = false;
+
+ if (aLoadState->PrincipalToInherit()) {
+ bool isSrcdoc =
+ aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_IS_SRCDOC);
+ bool inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal(
+ aLoadState->PrincipalToInherit(), aLoadState->URI(),
+ true, // aInheritForAboutBlank
+ isSrcdoc);
+
+ inheritPrincipal = inheritAttrs && !SchemeIsData(aLoadState->URI());
+ }
+
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1736570
+ const bool isAboutBlankLoadOntoInitialAboutBlank =
+ IsAboutBlankLoadOntoInitialAboutBlank(aLoadState->URI(), inheritPrincipal,
+ aLoadState->PrincipalToInherit());
+
+ // FIXME We still have a ton of codepaths that don't pass through
+ // DocumentLoadListener, so probably need to create session history info
+ // in more places.
+ if (aLoadState->GetLoadingSessionHistoryInfo()) {
+ SetLoadingSessionHistoryInfo(*aLoadState->GetLoadingSessionHistoryInfo());
+ } else if (isAboutBlankLoadOntoInitialAboutBlank &&
+ mozilla::SessionHistoryInParent()) {
+ // Materialize LoadingSessionHistoryInfo here, because DocumentChannel
+ // loads have it, and later history behavior depends on it existing.
+ UniquePtr<SessionHistoryInfo> entry = MakeUnique<SessionHistoryInfo>(
+ aLoadState->URI(), aLoadState->TriggeringPrincipal(),
+ aLoadState->PrincipalToInherit(),
+ aLoadState->PartitionedPrincipalToInherit(), aLoadState->Csp(),
+ mContentTypeHint);
+ mozilla::dom::LoadingSessionHistoryInfo info(*entry);
+ SetLoadingSessionHistoryInfo(info, true);
+ }
+
+ // open a channel for the url
+
+ // If we have a pending channel, use the channel we've already created here.
+ // We don't need to set up load flags for our channel, as it has already been
+ // created.
+
+ if (nsCOMPtr<nsIChannel> channel =
+ aLoadState->GetPendingRedirectedChannel()) {
+ // If we have a request outparameter, shove our channel into it.
+ if (aRequest) {
+ nsCOMPtr<nsIRequest> outRequest = channel;
+ outRequest.forget(aRequest);
+ }
+
+ return OpenRedirectedChannel(aLoadState);
+ }
+
+ // There are two cases we care about:
+ // * Top-level load: In this case, loadingNode is null, but loadingWindow
+ // is our mScriptGlobal. We pass null for loadingPrincipal in this case.
+ // * Subframe load: loadingWindow is null, but loadingNode is the frame
+ // element for the load. loadingPrincipal is the NodePrincipal of the
+ // frame element.
+ nsCOMPtr<nsINode> loadingNode;
+ nsCOMPtr<nsPIDOMWindowOuter> loadingWindow;
+ nsCOMPtr<nsIPrincipal> loadingPrincipal;
+ nsCOMPtr<nsISupports> topLevelLoadingContext;
+
+ if (contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT) {
+ loadingNode = nullptr;
+ loadingPrincipal = nullptr;
+ loadingWindow = mScriptGlobal;
+ if (XRE_IsContentProcess()) {
+ // In e10s the child process doesn't have access to the element that
+ // contains the browsing context (because that element is in the chrome
+ // process).
+ nsCOMPtr<nsIBrowserChild> browserChild = GetBrowserChild();
+ topLevelLoadingContext = ToSupports(browserChild);
+ } else {
+ // This is for loading non-e10s tabs and toplevel windows of various
+ // sorts.
+ // For the toplevel window cases, requestingElement will be null.
+ nsCOMPtr<Element> requestingElement =
+ loadingWindow->GetFrameElementInternal();
+ topLevelLoadingContext = requestingElement;
+ }
+ } else {
+ loadingWindow = nullptr;
+ loadingNode = mScriptGlobal->GetFrameElementInternal();
+ if (loadingNode) {
+ // If we have a loading node, then use that as our loadingPrincipal.
+ loadingPrincipal = loadingNode->NodePrincipal();
+#ifdef DEBUG
+ // Get the docshell type for requestingElement.
+ RefPtr<Document> requestingDoc = loadingNode->OwnerDoc();
+ nsCOMPtr<nsIDocShell> elementDocShell = requestingDoc->GetDocShell();
+ // requestingElement docshell type = current docshell type.
+ MOZ_ASSERT(
+ mItemType == elementDocShell->ItemType(),
+ "subframes should have the same docshell type as their parent");
+#endif
+ } else {
+ if (mIsBeingDestroyed) {
+ // If this isn't a top-level load and mScriptGlobal's frame element is
+ // null, then the element got removed from the DOM while we were trying
+ // to load this resource. This docshell is scheduled for destruction
+ // already, so bail out here.
+ return NS_OK;
+ }
+ // If we are not being destroyed and we do not have access to the loading
+ // node, then we are a remote subframe. Set the loading principal
+ // to be a null principal and then set it correctly in the parent.
+ loadingPrincipal = NullPrincipal::Create(GetOriginAttributes(), nullptr);
+ }
+ }
+
+ if (!aLoadState->TriggeringPrincipal()) {
+ MOZ_ASSERT(false, "DoURILoad needs a valid triggeringPrincipal");
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t sandboxFlags = mBrowsingContext->GetSandboxFlags();
+ nsSecurityFlags securityFlags =
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+
+ if (mLoadType == LOAD_ERROR_PAGE) {
+ securityFlags |= nsILoadInfo::SEC_LOAD_ERROR_PAGE;
+ }
+
+ if (inheritPrincipal) {
+ securityFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL;
+ }
+
+ // Must never have a parent for TYPE_DOCUMENT loads
+ MOZ_ASSERT_IF(contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT,
+ !mBrowsingContext->GetParent());
+ // Subdocuments must have a parent
+ MOZ_ASSERT_IF(contentPolicyType == nsIContentPolicy::TYPE_SUBDOCUMENT,
+ mBrowsingContext->GetParent());
+ mBrowsingContext->SetTriggeringAndInheritPrincipals(
+ aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(),
+ aLoadState->GetLoadIdentifier());
+ RefPtr<LoadInfo> loadInfo =
+ (contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT)
+ ? new LoadInfo(loadingWindow, aLoadState->URI(),
+ aLoadState->TriggeringPrincipal(),
+ topLevelLoadingContext, securityFlags, sandboxFlags)
+ : new LoadInfo(loadingPrincipal, aLoadState->TriggeringPrincipal(),
+ loadingNode, securityFlags, contentPolicyType,
+ Maybe<mozilla::dom::ClientInfo>(),
+ Maybe<mozilla::dom::ServiceWorkerDescriptor>(),
+ sandboxFlags);
+ RefPtr<WindowContext> context = mBrowsingContext->GetCurrentWindowContext();
+
+ if (isAboutBlankLoadOntoInitialAboutBlank) {
+ // Match the DocumentChannel case where the default for third-partiness
+ // differs from the default in LoadInfo construction here.
+ // toolkit/components/antitracking/test/browser/browser_aboutblank.js
+ // fails without this.
+ BrowsingContext* top = mBrowsingContext->Top();
+ if (top == mBrowsingContext) {
+ // If we're at the top, this must be a window.open()ed
+ // window, and we can't be third-party relative to ourselves.
+ loadInfo->SetIsThirdPartyContextToTopWindow(false);
+ } else {
+ if (Document* topDoc = top->GetDocument()) {
+ bool thirdParty = false;
+ mozilla::Unused << topDoc->GetPrincipal()->IsThirdPartyPrincipal(
+ aLoadState->PrincipalToInherit(), &thirdParty);
+ loadInfo->SetIsThirdPartyContextToTopWindow(thirdParty);
+ } else {
+ // If top is in a different process, we have to be third-party relative
+ // to it.
+ loadInfo->SetIsThirdPartyContextToTopWindow(true);
+ }
+ }
+ }
+
+ if (mLoadType != LOAD_ERROR_PAGE && context && context->IsInProcess()) {
+ if (context->HasValidTransientUserGestureActivation()) {
+ aLoadState->SetHasValidUserGestureActivation(true);
+ }
+ if (!aLoadState->TriggeringWindowId()) {
+ aLoadState->SetTriggeringWindowId(context->Id());
+ }
+ if (!aLoadState->TriggeringStorageAccess()) {
+ Document* contextDoc = context->GetExtantDoc();
+ if (contextDoc) {
+ aLoadState->SetTriggeringStorageAccess(
+ contextDoc->UsingStorageAccess());
+ }
+ }
+ }
+
+ // in case this docshell load was triggered by a valid transient user gesture,
+ // or also the load originates from external, then we pass that information on
+ // to the loadinfo, which allows e.g. setting Sec-Fetch-User request headers.
+ if (aLoadState->HasValidUserGestureActivation() ||
+ aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL)) {
+ loadInfo->SetHasValidUserGestureActivation(true);
+ }
+
+ loadInfo->SetTriggeringWindowId(aLoadState->TriggeringWindowId());
+ loadInfo->SetTriggeringStorageAccess(aLoadState->TriggeringStorageAccess());
+ loadInfo->SetTriggeringSandboxFlags(aLoadState->TriggeringSandboxFlags());
+ loadInfo->SetIsMetaRefresh(aLoadState->IsMetaRefresh());
+
+ uint32_t cacheKey = 0;
+ if (aCacheKey) {
+ cacheKey = *aCacheKey;
+ } else if (mozilla::SessionHistoryInParent()) {
+ if (mLoadingEntry) {
+ cacheKey = mLoadingEntry->mInfo.GetCacheKey();
+ } else if (mActiveEntry) { // for reload cases
+ cacheKey = mActiveEntry->GetCacheKey();
+ }
+ } else {
+ if (mLSHE) {
+ cacheKey = mLSHE->GetCacheKey();
+ } else if (mOSHE) { // for reload cases
+ cacheKey = mOSHE->GetCacheKey();
+ }
+ }
+
+ bool uriModified;
+ if (mLSHE || mLoadingEntry) {
+ if (mLoadingEntry) {
+ uriModified = mLoadingEntry->mInfo.GetURIWasModified();
+ } else {
+ uriModified = mLSHE->GetURIWasModified();
+ }
+ } else {
+ uriModified = false;
+ }
+
+ bool isEmbeddingBlockedError = false;
+ if (mFailedChannel) {
+ nsresult status;
+ mFailedChannel->GetStatus(&status);
+ isEmbeddingBlockedError = status == NS_ERROR_XFO_VIOLATION ||
+ status == NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION;
+ }
+
+ nsLoadFlags loadFlags = aLoadState->CalculateChannelLoadFlags(
+ mBrowsingContext, Some(uriModified), Some(isEmbeddingBlockedError));
+
+ nsCOMPtr<nsIChannel> channel;
+ if (DocumentChannel::CanUseDocumentChannel(aLoadState->URI()) &&
+ !isAboutBlankLoadOntoInitialAboutBlank) {
+ channel = DocumentChannel::CreateForDocument(
+ aLoadState, loadInfo, loadFlags, this, cacheKey, uriModified,
+ isEmbeddingBlockedError);
+ MOZ_ASSERT(channel);
+
+ // Disable keyword fixup when using DocumentChannel, since
+ // DocumentLoadListener will handle this for us (in the parent process).
+ mAllowKeywordFixup = false;
+ } else if (!CreateAndConfigureRealChannelForLoadState(
+ mBrowsingContext, aLoadState, loadInfo, this, this,
+ GetOriginAttributes(), loadFlags, cacheKey, rv,
+ getter_AddRefs(channel))) {
+ return rv;
+ }
+
+ // Make sure to give the caller a channel if we managed to create one
+ // This is important for correct error page/session history interaction
+ if (aRequest) {
+ NS_ADDREF(*aRequest = channel);
+ }
+
+ const nsACString& typeHint = aLoadState->TypeHint();
+ if (!typeHint.IsVoid()) {
+ mContentTypeHint = typeHint;
+ } else {
+ mContentTypeHint.Truncate();
+ }
+
+ // Load attributes depend on load type...
+ if (mLoadType == LOAD_RELOAD_CHARSET_CHANGE) {
+ // Use SetAllowStaleCacheContent (not LOAD_FROM_CACHE flag) since we
+ // only want to force cache load for this channel, not the whole
+ // loadGroup.
+ nsCOMPtr<nsICacheInfoChannel> cachingChannel = do_QueryInterface(channel);
+ if (cachingChannel) {
+ cachingChannel->SetAllowStaleCacheContent(true);
+ }
+ }
+
+ uint32_t openFlags =
+ nsDocShell::ComputeURILoaderFlags(mBrowsingContext, mLoadType);
+ return OpenInitializedChannel(channel, uriLoader, openFlags);
+}
+
+static nsresult AppendSegmentToString(nsIInputStream* aIn, void* aClosure,
+ const char* aFromRawSegment,
+ uint32_t aToOffset, uint32_t aCount,
+ uint32_t* aWriteCount) {
+ // aFromSegment now contains aCount bytes of data.
+
+ nsAutoCString* buf = static_cast<nsAutoCString*>(aClosure);
+ buf->Append(aFromRawSegment, aCount);
+
+ // Indicate that we have consumed all of aFromSegment
+ *aWriteCount = aCount;
+ return NS_OK;
+}
+
+/* static */ nsresult nsDocShell::AddHeadersToChannel(
+ nsIInputStream* aHeadersData, nsIChannel* aGenericChannel) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aGenericChannel);
+ NS_ENSURE_STATE(httpChannel);
+
+ uint32_t numRead;
+ nsAutoCString headersString;
+ nsresult rv = aHeadersData->ReadSegments(
+ AppendSegmentToString, &headersString, UINT32_MAX, &numRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // used during the manipulation of the String from the InputStream
+ nsAutoCString headerName;
+ nsAutoCString headerValue;
+ int32_t crlf;
+ int32_t colon;
+
+ //
+ // Iterate over the headersString: for each "\r\n" delimited chunk,
+ // add the value as a header to the nsIHttpChannel
+ //
+
+ static const char kWhitespace[] = "\b\t\r\n ";
+ while (true) {
+ crlf = headersString.Find("\r\n");
+ if (crlf == kNotFound) {
+ return NS_OK;
+ }
+
+ const nsACString& oneHeader = StringHead(headersString, crlf);
+
+ colon = oneHeader.FindChar(':');
+ if (colon == kNotFound) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ headerName = StringHead(oneHeader, colon);
+ headerValue = Substring(oneHeader, colon + 1);
+
+ headerName.Trim(kWhitespace);
+ headerValue.Trim(kWhitespace);
+
+ headersString.Cut(0, crlf + 2);
+
+ //
+ // FINALLY: we can set the header!
+ //
+
+ rv = httpChannel->SetRequestHeader(headerName, headerValue, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("oops");
+ return NS_ERROR_UNEXPECTED;
+}
+
+/* static */ uint32_t nsDocShell::ComputeURILoaderFlags(
+ BrowsingContext* aBrowsingContext, uint32_t aLoadType) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ uint32_t openFlags = 0;
+ if (aLoadType == LOAD_LINK) {
+ openFlags |= nsIURILoader::IS_CONTENT_PREFERRED;
+ }
+ if (!aBrowsingContext->GetAllowContentRetargeting()) {
+ openFlags |= nsIURILoader::DONT_RETARGET;
+ }
+
+ return openFlags;
+}
+
+nsresult nsDocShell::OpenInitializedChannel(nsIChannel* aChannel,
+ nsIURILoader* aURILoader,
+ uint32_t aOpenFlags) {
+ nsresult rv = NS_OK;
+
+ // If anything fails here, make sure to clear our initial ClientSource.
+ auto cleanupInitialClient =
+ MakeScopeExit([&] { mInitialClientSource.reset(); });
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow();
+ NS_ENSURE_TRUE(win, NS_ERROR_FAILURE);
+
+ MaybeCreateInitialClientSource();
+
+ // Let the client channel helper know if we are using DocumentChannel,
+ // since redirects get handled in the parent process in that case.
+ RefPtr<net::DocumentChannel> docChannel = do_QueryObject(aChannel);
+ if (docChannel && XRE_IsContentProcess()) {
+ // Tell the content process nsDocumentOpenInfo to not try to do
+ // any sort of targeting.
+ aOpenFlags |= nsIURILoader::DONT_RETARGET;
+ }
+
+ // Since we are loading a document we need to make sure the proper reserved
+ // and initial client data is stored on the nsILoadInfo. The
+ // ClientChannelHelper does this and ensures that it is propagated properly
+ // on redirects. We pass no reserved client here so that the helper will
+ // create the reserved ClientSource if necessary.
+ Maybe<ClientInfo> noReservedClient;
+ if (docChannel) {
+ // When using DocumentChannel, all redirect handling is done in the parent,
+ // so we just need the child variant to watch for the internal redirect
+ // to the final channel.
+ rv = AddClientChannelHelperInChild(aChannel,
+ GetMainThreadSerialEventTarget());
+ docChannel->SetInitialClientInfo(GetInitialClientInfo());
+ } else {
+ rv = AddClientChannelHelper(aChannel, std::move(noReservedClient),
+ GetInitialClientInfo(),
+ GetMainThreadSerialEventTarget());
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aURILoader->OpenURI(aChannel, aOpenFlags, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We're about to load a new page and it may take time before necko
+ // gives back any data, so main thread might have a chance to process a
+ // collector slice
+ nsJSContext::MaybeRunNextCollectorSlice(this, JS::GCReason::DOCSHELL);
+
+ // Success. Keep the initial ClientSource if it exists.
+ cleanupInitialClient.release();
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::OpenRedirectedChannel(nsDocShellLoadState* aLoadState) {
+ nsCOMPtr<nsIChannel> channel = aLoadState->GetPendingRedirectedChannel();
+ MOZ_ASSERT(channel);
+
+ // If anything fails here, make sure to clear our initial ClientSource.
+ auto cleanupInitialClient =
+ MakeScopeExit([&] { mInitialClientSource.reset(); });
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow();
+ NS_ENSURE_TRUE(win, NS_ERROR_FAILURE);
+
+ MaybeCreateInitialClientSource();
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+
+ LoadInfo* li = static_cast<LoadInfo*>(loadInfo.get());
+ if (loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ li->UpdateBrowsingContextID(mBrowsingContext->Id());
+ } else if (loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_SUBDOCUMENT) {
+ li->UpdateFrameBrowsingContextID(mBrowsingContext->Id());
+ }
+
+ // If we did a process switch, then we should have an existing allocated
+ // ClientInfo, so we just need to allocate a corresponding ClientSource.
+ CreateReservedSourceIfNeeded(channel, GetMainThreadSerialEventTarget());
+
+ RefPtr<nsDocumentOpenInfo> loader =
+ new nsDocumentOpenInfo(this, nsIURILoader::DONT_RETARGET, nullptr);
+ channel->SetLoadGroup(mLoadGroup);
+
+ MOZ_ALWAYS_SUCCEEDS(loader->Prepare());
+
+ nsresult rv = NS_OK;
+ if (XRE_IsParentProcess()) {
+ // If we're in the parent, the we don't have an nsIChildChannel, just
+ // the original channel, which is already open in this process.
+
+ // DocumentLoadListener expects to get an nsIParentChannel, so
+ // we create a wrapper around the channel and nsIStreamListener
+ // that forwards functionality as needed, and then we register
+ // it under the provided identifier.
+ RefPtr<ParentChannelWrapper> wrapper =
+ new ParentChannelWrapper(channel, loader);
+ wrapper->Register(aLoadState->GetPendingRedirectChannelRegistrarId());
+
+ mLoadGroup->AddRequest(channel, nullptr);
+ } else if (nsCOMPtr<nsIChildChannel> childChannel =
+ do_QueryInterface(channel)) {
+ // Our channel was redirected from another process, so doesn't need to
+ // be opened again. However, it does need its listener hooked up
+ // correctly.
+ rv = childChannel->CompleteRedirectSetup(loader);
+ } else {
+ // It's possible for the redirected channel to not implement
+ // nsIChildChannel and be entirely local (like srcdoc). In that case we
+ // can just open the local instance and it will work.
+ rv = channel->AsyncOpen(loader);
+ }
+ if (rv == NS_ERROR_NO_CONTENT) {
+ return NS_OK;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Success. Keep the initial ClientSource if it exists.
+ cleanupInitialClient.release();
+ return NS_OK;
+}
+
+// https://html.spec.whatwg.org/#scrolling-to-a-fragment
+nsresult nsDocShell::ScrollToAnchor(bool aCurHasRef, bool aNewHasRef,
+ nsACString& aNewHash, uint32_t aLoadType) {
+ if (!mCurrentURI) {
+ return NS_OK;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (!presShell) {
+ // If we failed to get the shell, or if there is no shell,
+ // nothing left to do here.
+ return NS_OK;
+ }
+
+ nsIScrollableFrame* rootScroll = presShell->GetRootScrollFrameAsScrollable();
+ if (rootScroll) {
+ rootScroll->ClearDidHistoryRestore();
+ }
+
+ // If we have no new anchor, we do not want to scroll, unless there is a
+ // current anchor and we are doing a history load. So return if we have no
+ // new anchor, and there is no current anchor or the load is not a history
+ // load.
+ if ((!aCurHasRef || aLoadType != LOAD_HISTORY) && !aNewHasRef) {
+ return NS_OK;
+ }
+
+ // Both the new and current URIs refer to the same page. We can now
+ // browse to the hash stored in the new URI.
+
+ // If it's a load from history, we don't have any anchor jumping to do.
+ // Scrollbar position will be restored by the caller based on positions stored
+ // in session history.
+ bool scroll = aLoadType != LOAD_HISTORY && aLoadType != LOAD_RELOAD_NORMAL;
+
+ if (aNewHash.IsEmpty()) {
+ // 2. If fragment is the empty string, then return the special value top of
+ // the document.
+ //
+ // Tell the shell it's at an anchor without scrolling.
+ presShell->GoToAnchor(u""_ns, false);
+
+ if (scroll) {
+ // Scroll to the top of the page. Ignore the return value; failure to
+ // scroll here (e.g. if there is no root scrollframe) is not grounds for
+ // canceling the load!
+ SetCurScrollPosEx(0, 0);
+ }
+
+ return NS_OK;
+ }
+
+ // 3. Let potentialIndicatedElement be the result of finding a potential
+ // indicated element given document and fragment.
+ NS_ConvertUTF8toUTF16 uStr(aNewHash);
+ auto rv = presShell->GoToAnchor(uStr, scroll, ScrollFlags::ScrollSmoothAuto);
+
+ // 4. If potentialIndicatedElement is not null, then return
+ // potentialIndicatedElement.
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+
+ // 5. Let fragmentBytes be the result of percent-decoding fragment.
+ nsAutoCString fragmentBytes;
+ const bool unescaped = NS_UnescapeURL(aNewHash.Data(), aNewHash.Length(),
+ /* aFlags = */ 0, fragmentBytes);
+
+ if (!unescaped) {
+ // Another attempt is only necessary if characters were unescaped.
+ return NS_OK;
+ }
+
+ if (fragmentBytes.IsEmpty()) {
+ // When aNewHash contains "%00", the unescaped string may be empty, and
+ // GoToAnchor asserts if we ask it to scroll to an empty ref.
+ presShell->GoToAnchor(u""_ns, false);
+ return NS_OK;
+ }
+
+ // 6. Let decodedFragment be the result of running UTF-8 decode without BOM on
+ // fragmentBytes.
+ nsAutoString decodedFragment;
+ rv = UTF_8_ENCODING->DecodeWithoutBOMHandling(fragmentBytes, decodedFragment);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 7. Set potentialIndicatedElement to the result of finding a potential
+ // indicated element given document and decodedFragment.
+ //
+ // Ignore the return value of GoToAnchor, since it will return an error if
+ // there is no such anchor in the document, which is actually a success
+ // condition for us (we want to update the session history with the new URI no
+ // matter whether we actually scrolled somewhere).
+ presShell->GoToAnchor(decodedFragment, scroll, ScrollFlags::ScrollSmoothAuto);
+
+ return NS_OK;
+}
+
+bool nsDocShell::OnNewURI(nsIURI* aURI, nsIChannel* aChannel,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ bool aAddToGlobalHistory, bool aCloneSHChildren) {
+ MOZ_ASSERT(aURI, "uri is null");
+ MOZ_ASSERT(!aChannel || !aTriggeringPrincipal, "Shouldn't have both set");
+
+ MOZ_ASSERT(!aPrincipalToInherit ||
+ (aPrincipalToInherit && aTriggeringPrincipal));
+
+#if defined(DEBUG)
+ if (MOZ_LOG_TEST(gDocShellLog, LogLevel::Debug)) {
+ nsAutoCString chanName;
+ if (aChannel) {
+ aChannel->GetName(chanName);
+ } else {
+ chanName.AssignLiteral("<no channel>");
+ }
+
+ MOZ_LOG(gDocShellLog, LogLevel::Debug,
+ ("nsDocShell[%p]::OnNewURI(\"%s\", [%s], 0x%x)\n", this,
+ aURI->GetSpecOrDefault().get(), chanName.get(), mLoadType));
+ }
+#endif
+
+ bool equalUri = false;
+
+ // Get the post data and the HTTP response code from the channel.
+ uint32_t responseStatus = 0;
+ nsCOMPtr<nsIInputStream> inputStream;
+ if (aChannel) {
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel));
+
+ // Check if the HTTPChannel is hiding under a multiPartChannel
+ if (!httpChannel) {
+ GetHttpChannel(aChannel, getter_AddRefs(httpChannel));
+ }
+
+ if (httpChannel) {
+ nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(httpChannel));
+ if (uploadChannel) {
+ uploadChannel->GetUploadStream(getter_AddRefs(inputStream));
+ }
+
+ // If the response status indicates an error, unlink this session
+ // history entry from any entries sharing its document.
+ nsresult rv = httpChannel->GetResponseStatus(&responseStatus);
+ if (mLSHE && NS_SUCCEEDED(rv) && responseStatus >= 400) {
+ mLSHE->AbandonBFCacheEntry();
+ // FIXME Do the same for mLoadingEntry
+ }
+ }
+ }
+
+ // Determine if this type of load should update history.
+ bool updateGHistory = ShouldUpdateGlobalHistory(mLoadType);
+
+ // We don't update session history on reload unless we're loading
+ // an iframe in shift-reload case.
+ bool updateSHistory = mBrowsingContext->ShouldUpdateSessionHistory(mLoadType);
+
+ // Create SH Entry (mLSHE) only if there is a SessionHistory object in the
+ // root browsing context.
+ // FIXME If session history in the parent is enabled then we only do this if
+ // the session history object is in process, otherwise we can't really
+ // use the mLSHE anyway. Once session history is only stored in the
+ // parent then this code will probably be removed anyway.
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (!rootSH) {
+ updateSHistory = false;
+ updateGHistory = false; // XXX Why global history too?
+ }
+
+ // Check if the url to be loaded is the same as the one already loaded.
+ if (mCurrentURI) {
+ aURI->Equals(mCurrentURI, &equalUri);
+ }
+
+#ifdef DEBUG
+ bool shAvailable = (rootSH != nullptr);
+
+ // XXX This log message is almost useless because |updateSHistory|
+ // and |updateGHistory| are not correct at this point.
+
+ MOZ_LOG(gDocShellLog, LogLevel::Debug,
+ (" shAvailable=%i updateSHistory=%i updateGHistory=%i"
+ " equalURI=%i\n",
+ shAvailable, updateSHistory, updateGHistory, equalUri));
+#endif
+
+ /* If the url to be loaded is the same as the one already there,
+ * and the original loadType is LOAD_NORMAL, LOAD_LINK, or
+ * LOAD_STOP_CONTENT, set loadType to LOAD_NORMAL_REPLACE so that
+ * AddToSessionHistory() won't mess with the current SHEntry and
+ * if this page has any frame children, it also will be handled
+ * properly. see bug 83684
+ *
+ * NB: If mOSHE is null but we have a current URI, then it probably
+ * means that we must be at the transient about:blank content viewer;
+ * we should let the normal load continue, since there's nothing to
+ * replace. Sometimes this happens after a session restore (eg process
+ * switch) and mCurrentURI is not about:blank; we assume we can let the load
+ * continue (Bug 1301399).
+ *
+ * XXX Hopefully changing the loadType at this time will not hurt
+ * anywhere. The other way to take care of sequentially repeating
+ * frameset pages is to add new methods to nsIDocShellTreeItem.
+ * Hopefully I don't have to do that.
+ */
+ if (equalUri &&
+ (mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) &&
+ (mLoadType == LOAD_NORMAL || mLoadType == LOAD_LINK ||
+ mLoadType == LOAD_STOP_CONTENT) &&
+ !inputStream) {
+ mLoadType = LOAD_NORMAL_REPLACE;
+ }
+
+ // If this is a refresh to the currently loaded url, we don't
+ // have to update session or global history.
+ if (mLoadType == LOAD_REFRESH && !inputStream && equalUri) {
+ SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(mOSHE), Nothing());
+ }
+
+ /* If the user pressed shift-reload, cache will create a new cache key
+ * for the page. Save the new cacheKey in Session History.
+ * see bug 90098
+ */
+ if (aChannel && IsForceReloadType(mLoadType)) {
+ MOZ_ASSERT(!updateSHistory || IsSubframe(),
+ "We shouldn't be updating session history for forced"
+ " reloads unless we're in a newly created iframe!");
+
+ nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(aChannel));
+ uint32_t cacheKey = 0;
+ // Get the Cache Key and store it in SH.
+ if (cacheChannel) {
+ cacheChannel->GetCacheKey(&cacheKey);
+ }
+ // If we already have a loading history entry, store the new cache key
+ // in it. Otherwise, since we're doing a reload and won't be updating
+ // our history entry, store the cache key in our current history entry.
+ SetCacheKeyOnHistoryEntry(mLSHE ? mLSHE : mOSHE, cacheKey);
+
+ if (!mozilla::SessionHistoryInParent()) {
+ // Since we're force-reloading, clear all the sub frame history.
+ ClearFrameHistory(mLSHE);
+ ClearFrameHistory(mOSHE);
+ }
+ }
+
+ if (!mozilla::SessionHistoryInParent()) {
+ // Clear subframe history on refresh.
+ // XXX: history.go(0) won't go this path as mLoadType is LOAD_HISTORY in
+ // this case. One should re-validate after bug 1331865 fixed.
+ if (mLoadType == LOAD_REFRESH) {
+ ClearFrameHistory(mLSHE);
+ ClearFrameHistory(mOSHE);
+ }
+
+ if (updateSHistory) {
+ // Update session history if necessary...
+ if (!mLSHE && (mItemType == typeContent) && mURIResultedInDocument) {
+ /* This is a fresh page getting loaded for the first time
+ *.Create a Entry for it and add it to SH, if this is the
+ * rootDocShell
+ */
+ (void)AddToSessionHistory(aURI, aChannel, aTriggeringPrincipal,
+ aPrincipalToInherit,
+ aPartitionedPrincipalToInherit, aCsp,
+ aCloneSHChildren, getter_AddRefs(mLSHE));
+ }
+ } else if (GetSessionHistory() && mLSHE && mURIResultedInDocument) {
+ // Even if we don't add anything to SHistory, ensure the current index
+ // points to the same SHEntry as our mLSHE.
+
+ GetSessionHistory()->LegacySHistory()->EnsureCorrectEntryAtCurrIndex(
+ mLSHE);
+ }
+ }
+
+ // If this is a POST request, we do not want to include this in global
+ // history.
+ if (ShouldAddURIVisit(aChannel) && updateGHistory && aAddToGlobalHistory &&
+ !net::ChannelIsPost(aChannel)) {
+ nsCOMPtr<nsIURI> previousURI;
+ uint32_t previousFlags = 0;
+
+ if (mLoadType & LOAD_CMD_RELOAD) {
+ // On a reload request, we don't set redirecting flags.
+ previousURI = aURI;
+ } else {
+ ExtractLastVisit(aChannel, getter_AddRefs(previousURI), &previousFlags);
+ }
+
+ AddURIVisit(aURI, previousURI, previousFlags, responseStatus);
+ }
+
+ // If this was a history load or a refresh, or it was a history load but
+ // later changed to LOAD_NORMAL_REPLACE due to redirection, update the index
+ // in session history.
+ if (!mozilla::SessionHistoryInParent() && rootSH &&
+ ((mLoadType & (LOAD_CMD_HISTORY | LOAD_CMD_RELOAD)) ||
+ mLoadType == LOAD_NORMAL_REPLACE || mLoadType == LOAD_REFRESH_REPLACE)) {
+ mPreviousEntryIndex = rootSH->Index();
+ if (!mozilla::SessionHistoryInParent()) {
+ rootSH->LegacySHistory()->UpdateIndex();
+ }
+ mLoadedEntryIndex = rootSH->Index();
+ MOZ_LOG(gPageCacheLog, LogLevel::Verbose,
+ ("Previous index: %d, Loaded index: %d", mPreviousEntryIndex,
+ mLoadedEntryIndex));
+ }
+
+ // aCloneSHChildren exactly means "we are not loading a new document".
+ uint32_t locationFlags =
+ aCloneSHChildren ? uint32_t(LOCATION_CHANGE_SAME_DOCUMENT) : 0;
+
+ bool onLocationChangeNeeded =
+ SetCurrentURI(aURI, aChannel, false,
+ /* aIsInitialAboutBlank */ false, locationFlags);
+ // Make sure to store the referrer from the channel, if any
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel));
+ if (httpChannel) {
+ mReferrerInfo = httpChannel->GetReferrerInfo();
+ }
+ return onLocationChangeNeeded;
+}
+
+Maybe<Wireframe> nsDocShell::GetWireframe() {
+ const bool collectWireFrame =
+ mozilla::SessionHistoryInParent() &&
+ StaticPrefs::browser_history_collectWireframes() &&
+ mBrowsingContext->IsTopContent() && mActiveEntry;
+
+ if (!collectWireFrame) {
+ return Nothing();
+ }
+
+ RefPtr<Document> doc = mDocumentViewer->GetDocument();
+ Nullable<Wireframe> wireframe;
+ doc->GetWireframeWithoutFlushing(false, wireframe);
+ if (wireframe.IsNull()) {
+ return Nothing();
+ }
+ return Some(wireframe.Value());
+}
+
+bool nsDocShell::CollectWireframe() {
+ Maybe<Wireframe> wireframe = GetWireframe();
+ if (wireframe.isNothing()) {
+ return false;
+ }
+
+ if (XRE_IsParentProcess()) {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ entry->SetWireframe(wireframe);
+ }
+ } else {
+ mozilla::Unused
+ << ContentChild::GetSingleton()->SendSessionHistoryEntryWireframe(
+ mBrowsingContext, wireframe.ref());
+ }
+
+ return true;
+}
+
+//*****************************************************************************
+// nsDocShell: Session History
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::AddState(JS::Handle<JS::Value> aData, const nsAString& aTitle,
+ const nsAString& aURL, bool aReplace, JSContext* aCx) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell[%p]: AddState(..., %s, %s, %d)", this,
+ NS_ConvertUTF16toUTF8(aTitle).get(),
+ NS_ConvertUTF16toUTF8(aURL).get(), aReplace));
+ // Implements History.pushState and History.replaceState
+
+ // Here's what we do, roughly in the order specified by HTML5. The specific
+ // steps we are executing are at
+ // <https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate>
+ // and
+ // <https://html.spec.whatwg.org/multipage/history.html#url-and-history-update-steps>.
+ // This function basically implements #dom-history-pushstate and
+ // UpdateURLAndHistory implements #url-and-history-update-steps.
+ //
+ // A. Serialize aData using structured clone. This is #dom-history-pushstate
+ // step 5.
+ // B. If the third argument is present, #dom-history-pushstate step 7.
+ // 7.1. Resolve the url, relative to our document.
+ // 7.2. If (a) fails, raise a SECURITY_ERR
+ // 7.4. Compare the resulting absolute URL to the document's address. If
+ // any part of the URLs difer other than the <path>, <query>, and
+ // <fragment> components, raise a SECURITY_ERR and abort.
+ // C. If !aReplace, #url-and-history-update-steps steps 2.1-2.3:
+ // Remove from the session history all entries after the current entry,
+ // as we would after a regular navigation, and save the current
+ // entry's scroll position (bug 590573).
+ // D. #url-and-history-update-steps step 2.4 or step 3. As apropriate,
+ // either add a state object entry to the session history after the
+ // current entry with the following properties, or modify the current
+ // session history entry to set
+ // a. cloned data as the state object,
+ // b. if the third argument was present, the absolute URL found in
+ // step 2
+ // Also clear the new history entry's POST data (see bug 580069).
+ // E. If aReplace is false (i.e. we're doing a pushState instead of a
+ // replaceState), notify bfcache that we've navigated to a new page.
+ // F. If the third argument is present, set the document's current address
+ // to the absolute URL found in step B. This is
+ // #url-and-history-update-steps step 4.
+ //
+ // It's important that this function not run arbitrary scripts after step A
+ // and before completing step E. For example, if a script called
+ // history.back() before we completed step E, bfcache might destroy an
+ // active content viewer. Since EvictOutOfRangeDocumentViewers at the end of
+ // step E might run script, we can't just put a script blocker around the
+ // critical section.
+ //
+ // Note that we completely ignore the aTitle parameter.
+
+ nsresult rv;
+
+ // Don't clobber the load type of an existing network load.
+ AutoRestore<uint32_t> loadTypeResetter(mLoadType);
+
+ // pushState effectively becomes replaceState when we've started a network
+ // load but haven't adopted its document yet. This mirrors what we do with
+ // changes to the hash at this stage of the game.
+ if (JustStartedNetworkLoad()) {
+ aReplace = true;
+ }
+
+ RefPtr<Document> document = GetDocument();
+ NS_ENSURE_TRUE(document, NS_ERROR_FAILURE);
+
+ // Step A: Serialize aData using structured clone.
+ // https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate
+ // step 5.
+ nsCOMPtr<nsIStructuredCloneContainer> scContainer;
+
+ // scContainer->Init might cause arbitrary JS to run, and this code might
+ // navigate the page we're on, potentially to a different origin! (bug
+ // 634834) To protect against this, we abort if our principal changes due
+ // to the InitFromJSVal() call.
+ {
+ RefPtr<Document> origDocument = GetDocument();
+ if (!origDocument) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ nsCOMPtr<nsIPrincipal> origPrincipal = origDocument->NodePrincipal();
+
+ scContainer = new nsStructuredCloneContainer();
+ rv = scContainer->InitFromJSVal(aData, aCx);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<Document> newDocument = GetDocument();
+ if (!newDocument) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ nsCOMPtr<nsIPrincipal> newPrincipal = newDocument->NodePrincipal();
+
+ bool principalsEqual = false;
+ origPrincipal->Equals(newPrincipal, &principalsEqual);
+ NS_ENSURE_TRUE(principalsEqual, NS_ERROR_DOM_SECURITY_ERR);
+ }
+
+ // Check that the state object isn't too long.
+ int32_t maxStateObjSize = StaticPrefs::browser_history_maxStateObjectSize();
+ if (maxStateObjSize < 0) {
+ maxStateObjSize = 0;
+ }
+
+ uint64_t scSize;
+ rv = scContainer->GetSerializedNBytes(&scSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(scSize <= (uint32_t)maxStateObjSize, NS_ERROR_ILLEGAL_VALUE);
+
+ // Step B: Resolve aURL.
+ // https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate
+ // step 7.
+ bool equalURIs = true;
+ nsCOMPtr<nsIURI> currentURI;
+ if (mCurrentURI) {
+ currentURI = nsIOService::CreateExposableURI(mCurrentURI);
+ } else {
+ currentURI = mCurrentURI;
+ }
+ nsCOMPtr<nsIURI> newURI;
+ if (aURL.Length() == 0) {
+ newURI = currentURI;
+ } else {
+ // 7.1: Resolve aURL relative to mURI
+
+ nsIURI* docBaseURI = document->GetDocBaseURI();
+ if (!docBaseURI) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString spec;
+ docBaseURI->GetSpec(spec);
+
+ rv = NS_NewURI(getter_AddRefs(newURI), aURL,
+ document->GetDocumentCharacterSet(), docBaseURI);
+
+ // 7.2: If 2a fails, raise a SECURITY_ERR
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ // 7.4 and 7.5: Same-origin check.
+ if (!nsContentUtils::URIIsLocalFile(newURI)) {
+ // In addition to checking that the security manager says that
+ // the new URI has the same origin as our current URI, we also
+ // check that the two URIs have the same userpass. (The
+ // security manager says that |http://foo.com| and
+ // |http://me@foo.com| have the same origin.) currentURI
+ // won't contain the password part of the userpass, so this
+ // means that it's never valid to specify a password in a
+ // pushState or replaceState URI.
+
+ nsCOMPtr<nsIScriptSecurityManager> secMan =
+ do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+ NS_ENSURE_TRUE(secMan, NS_ERROR_FAILURE);
+
+ // It's very important that we check that newURI is of the same
+ // origin as currentURI, not docBaseURI, because a page can
+ // set docBaseURI arbitrarily to any domain.
+ nsAutoCString currentUserPass, newUserPass;
+ NS_ENSURE_SUCCESS(currentURI->GetUserPass(currentUserPass),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_SUCCESS(newURI->GetUserPass(newUserPass), NS_ERROR_FAILURE);
+ bool isPrivateWin =
+ document->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId >
+ 0;
+ if (NS_FAILED(secMan->CheckSameOriginURI(currentURI, newURI, true,
+ isPrivateWin)) ||
+ !currentUserPass.Equals(newUserPass)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ } else {
+ // It's a file:// URI
+ nsCOMPtr<nsIPrincipal> principal = document->GetPrincipal();
+
+ if (!principal || NS_FAILED(principal->CheckMayLoadWithReporting(
+ newURI, false, document->InnerWindowID()))) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ }
+
+ if (currentURI) {
+ currentURI->Equals(newURI, &equalURIs);
+ } else {
+ equalURIs = false;
+ }
+
+ } // end of same-origin check
+
+ // Step 8: call "URL and history update steps"
+ rv = UpdateURLAndHistory(document, newURI, scContainer, aTitle, aReplace,
+ currentURI, equalURIs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsDocShell::UpdateURLAndHistory(Document* aDocument, nsIURI* aNewURI,
+ nsIStructuredCloneContainer* aData,
+ const nsAString& aTitle, bool aReplace,
+ nsIURI* aCurrentURI, bool aEqualURIs) {
+ // Implements
+ // https://html.spec.whatwg.org/multipage/history.html#url-and-history-update-steps
+
+ // If we have a pending title change, handle it before creating a new entry.
+ aDocument->DoNotifyPossibleTitleChange();
+
+ // Step 2, if aReplace is false: Create a new entry in the session
+ // history. This will erase all SHEntries after the new entry and make this
+ // entry the current one. This operation may modify mOSHE, which we need
+ // later, so we keep a reference here.
+ NS_ENSURE_TRUE(mOSHE || mActiveEntry || aReplace, NS_ERROR_FAILURE);
+ nsCOMPtr<nsISHEntry> oldOSHE = mOSHE;
+
+ // If this push/replaceState changed the document's current URI and the new
+ // URI differs from the old URI in more than the hash, or if the old
+ // SHEntry's URI was modified in this way by a push/replaceState call
+ // set URIWasModified to true for the current SHEntry (bug 669671).
+ bool sameExceptHashes = true;
+ aNewURI->EqualsExceptRef(aCurrentURI, &sameExceptHashes);
+ bool uriWasModified;
+ if (sameExceptHashes) {
+ if (mozilla::SessionHistoryInParent()) {
+ uriWasModified = mActiveEntry && mActiveEntry->GetURIWasModified();
+ } else {
+ uriWasModified = oldOSHE && oldOSHE->GetURIWasModified();
+ }
+ } else {
+ uriWasModified = true;
+ }
+
+ mLoadType = LOAD_PUSHSTATE;
+
+ nsCOMPtr<nsISHEntry> newSHEntry;
+ if (!aReplace) {
+ // Step 2.
+
+ // Step 2.2, "Remove any tasks queued by the history traversal task
+ // source that are associated with any Document objects in the
+ // top-level browsing context's document family." This is very hard in
+ // SessionHistoryInParent since we can't synchronously access the
+ // pending navigations that are already sent to the parent. We can
+ // abort any AsyncGo navigations that are waiting to be sent. If we
+ // send a message to the parent, it would be processed after any
+ // navigations previously sent. So long as we consider the "history
+ // traversal task source" to be the list in this process we match the
+ // spec. If we move the entire list to the parent, we can handle the
+ // aborting of loads there, but we don't have a way to synchronously
+ // remove entries as we do here for non-SHIP.
+ RefPtr<ChildSHistory> shistory = GetRootSessionHistory();
+ if (shistory) {
+ shistory->RemovePendingHistoryNavigations();
+ }
+
+ nsPoint scrollPos = GetCurScrollPos();
+
+ bool scrollRestorationIsManual;
+ if (mozilla::SessionHistoryInParent()) {
+ // FIXME Need to save the current scroll position on mActiveEntry.
+ scrollRestorationIsManual = mActiveEntry->GetScrollRestorationIsManual();
+ } else {
+ // Save the current scroll position (bug 590573). Step 2.3.
+ mOSHE->SetScrollPosition(scrollPos.x, scrollPos.y);
+
+ scrollRestorationIsManual = mOSHE->GetScrollRestorationIsManual();
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
+
+ if (mozilla::SessionHistoryInParent()) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p UpdateActiveEntry (not replacing)", this));
+ nsString title(mActiveEntry->GetTitle());
+ UpdateActiveEntry(false,
+ /* aPreviousScrollPos = */ Some(scrollPos), aNewURI,
+ /* aOriginalURI = */ nullptr,
+ /* aReferrerInfo = */ nullptr,
+ /* aTriggeringPrincipal = */ aDocument->NodePrincipal(),
+ csp, title, scrollRestorationIsManual, aData,
+ uriWasModified);
+ } else {
+ // Since we're not changing which page we have loaded, pass
+ // true for aCloneChildren.
+ nsresult rv = AddToSessionHistory(
+ aNewURI, nullptr,
+ aDocument->NodePrincipal(), // triggeringPrincipal
+ nullptr, nullptr, csp, true, getter_AddRefs(newSHEntry));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(newSHEntry, NS_ERROR_FAILURE);
+
+ // Session history entries created by pushState inherit scroll restoration
+ // mode from the current entry.
+ newSHEntry->SetScrollRestorationIsManual(scrollRestorationIsManual);
+
+ nsString title;
+ mOSHE->GetTitle(title);
+
+ // Set the new SHEntry's title (bug 655273).
+ newSHEntry->SetTitle(title);
+
+ // Link the new SHEntry to the old SHEntry's BFCache entry, since the
+ // two entries correspond to the same document.
+ NS_ENSURE_SUCCESS(newSHEntry->AdoptBFCacheEntry(oldOSHE),
+ NS_ERROR_FAILURE);
+
+ // AddToSessionHistory may not modify mOSHE. In case it doesn't,
+ // we'll just set mOSHE here.
+ mOSHE = newSHEntry;
+ }
+ } else if (mozilla::SessionHistoryInParent()) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p UpdateActiveEntry (replacing) mActiveEntry %p",
+ this, mActiveEntry.get()));
+ // Setting the resultPrincipalURI to nullptr is fine here: it will cause
+ // NS_GetFinalChannelURI to use the originalURI as the URI, which is aNewURI
+ // in our case. We could also set it to aNewURI, with the same result.
+ // We don't use aTitle here, see bug 544535.
+ nsString title;
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ if (mActiveEntry) {
+ title = mActiveEntry->GetTitle();
+ referrerInfo = mActiveEntry->GetReferrerInfo();
+ } else {
+ referrerInfo = nullptr;
+ }
+ UpdateActiveEntry(
+ true, /* aPreviousScrollPos = */ Nothing(), aNewURI, aNewURI,
+ /* aReferrerInfo = */ referrerInfo, aDocument->NodePrincipal(),
+ aDocument->GetCsp(), title,
+ mActiveEntry && mActiveEntry->GetScrollRestorationIsManual(), aData,
+ uriWasModified);
+ } else {
+ // Step 3.
+ newSHEntry = mOSHE;
+
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("nsDocShell %p step 3", this));
+ // Since we're not changing which page we have loaded, pass
+ // true for aCloneChildren.
+ if (!newSHEntry) {
+ nsresult rv = AddToSessionHistory(
+ aNewURI, nullptr,
+ aDocument->NodePrincipal(), // triggeringPrincipal
+ nullptr, nullptr, aDocument->GetCsp(), true,
+ getter_AddRefs(newSHEntry));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mOSHE = newSHEntry;
+ }
+
+ newSHEntry->SetURI(aNewURI);
+ newSHEntry->SetOriginalURI(aNewURI);
+ // We replaced the URI of the entry, clear the unstripped URI as it
+ // shouldn't be used for reloads anymore.
+ newSHEntry->SetUnstrippedURI(nullptr);
+ // Setting the resultPrincipalURI to nullptr is fine here: it will cause
+ // NS_GetFinalChannelURI to use the originalURI as the URI, which is aNewURI
+ // in our case. We could also set it to aNewURI, with the same result.
+ newSHEntry->SetResultPrincipalURI(nullptr);
+ newSHEntry->SetLoadReplace(false);
+ }
+
+ if (!mozilla::SessionHistoryInParent()) {
+ // Step 2.4 and 3: Modify new/original session history entry and clear its
+ // POST data, if there is any.
+ newSHEntry->SetStateData(aData);
+ newSHEntry->SetPostData(nullptr);
+
+ newSHEntry->SetURIWasModified(uriWasModified);
+
+ // Step E as described at the top of AddState: If aReplace is false,
+ // indicating that we're doing a pushState rather than a replaceState,
+ // notify bfcache that we've added a page to the history so it can evict
+ // content viewers if appropriate. Otherwise call ReplaceEntry so that we
+ // notify nsIHistoryListeners that an entry was replaced. We may not have a
+ // root session history if this call is coming from a document.open() in a
+ // docshell subtree that disables session history.
+ RefPtr<ChildSHistory> rootSH = GetRootSessionHistory();
+ if (rootSH) {
+ rootSH->LegacySHistory()->EvictDocumentViewersOrReplaceEntry(newSHEntry,
+ aReplace);
+ }
+ }
+
+ // Step 4: If the document's URI changed, update document's URI and update
+ // global history.
+ //
+ // We need to call FireOnLocationChange so that the browser's address bar
+ // gets updated and the back button is enabled, but we only need to
+ // explicitly call FireOnLocationChange if we're not calling SetCurrentURI,
+ // since SetCurrentURI will call FireOnLocationChange for us.
+ //
+ // Both SetCurrentURI(...) and FireDummyOnLocationChange() pass
+ // nullptr for aRequest param to FireOnLocationChange(...). Such an update
+ // notification is allowed only when we know docshell is not loading a new
+ // document and it requires LOCATION_CHANGE_SAME_DOCUMENT flag. Otherwise,
+ // FireOnLocationChange(...) breaks security UI.
+ //
+ // If the docshell is shutting down, don't update the document URI, as we
+ // can't load into a docshell that is being destroyed.
+ if (!aEqualURIs && !mIsBeingDestroyed) {
+ aDocument->SetDocumentURI(aNewURI);
+ SetCurrentURI(aNewURI, nullptr, /* aFireLocationChange */ true,
+ /* aIsInitialAboutBlank */ false,
+ GetSameDocumentNavigationFlags(aNewURI));
+
+ AddURIVisit(aNewURI, aCurrentURI, 0);
+
+ // AddURIVisit doesn't set the title for the new URI in global history,
+ // so do that here.
+ UpdateGlobalHistoryTitle(aNewURI);
+
+ // Inform the favicon service that our old favicon applies to this new
+ // URI.
+ CopyFavicon(aCurrentURI, aNewURI, UsePrivateBrowsing());
+ } else {
+ FireDummyOnLocationChange();
+ }
+ aDocument->SetStateObject(aData);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetCurrentScrollRestorationIsManual(bool* aIsManual) {
+ if (mozilla::SessionHistoryInParent()) {
+ *aIsManual = mActiveEntry && mActiveEntry->GetScrollRestorationIsManual();
+ return NS_OK;
+ }
+
+ *aIsManual = false;
+ if (mOSHE) {
+ return mOSHE->GetScrollRestorationIsManual(aIsManual);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetCurrentScrollRestorationIsManual(bool aIsManual) {
+ SetScrollRestorationIsManualOnHistoryEntry(mOSHE, aIsManual);
+
+ return NS_OK;
+}
+
+void nsDocShell::SetScrollRestorationIsManualOnHistoryEntry(
+ nsISHEntry* aSHEntry, bool aIsManual) {
+ if (aSHEntry) {
+ aSHEntry->SetScrollRestorationIsManual(aIsManual);
+ }
+
+ if (mActiveEntry && mBrowsingContext) {
+ mActiveEntry->SetScrollRestorationIsManual(aIsManual);
+ if (XRE_IsParentProcess()) {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ entry->SetScrollRestorationIsManual(aIsManual);
+ }
+ } else {
+ mozilla::Unused << ContentChild::GetSingleton()
+ ->SendSessionHistoryEntryScrollRestorationIsManual(
+ mBrowsingContext, aIsManual);
+ }
+ }
+}
+
+void nsDocShell::SetCacheKeyOnHistoryEntry(nsISHEntry* aSHEntry,
+ uint32_t aCacheKey) {
+ if (aSHEntry) {
+ aSHEntry->SetCacheKey(aCacheKey);
+ }
+
+ if (mActiveEntry && mBrowsingContext) {
+ mActiveEntry->SetCacheKey(aCacheKey);
+ if (XRE_IsParentProcess()) {
+ SessionHistoryEntry* entry =
+ mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry();
+ if (entry) {
+ entry->SetCacheKey(aCacheKey);
+ }
+ } else {
+ mozilla::Unused
+ << ContentChild::GetSingleton()->SendSessionHistoryEntryCacheKey(
+ mBrowsingContext, aCacheKey);
+ }
+ }
+}
+
+/* static */
+bool nsDocShell::ShouldAddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel) {
+ // I believe none of the about: urls should go in the history. But then
+ // that could just be me... If the intent is only deny about:blank then we
+ // should just do a spec compare, rather than two gets of the scheme and
+ // then the path. -Gagan
+ nsresult rv;
+ nsAutoCString buf;
+
+ rv = aURI->GetScheme(buf);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ if (buf.EqualsLiteral("about")) {
+ rv = aURI->GetPathQueryRef(buf);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ if (buf.EqualsLiteral("blank")) {
+ return false;
+ }
+ // We only want to add about:newtab if it's not privileged, and
+ // if it is not configured to show the blank page.
+ if (buf.EqualsLiteral("newtab")) {
+ if (!StaticPrefs::browser_newtabpage_enabled()) {
+ return false;
+ }
+
+ NS_ENSURE_TRUE(aChannel, false);
+ nsCOMPtr<nsIPrincipal> resultPrincipal;
+ rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
+ aChannel, getter_AddRefs(resultPrincipal));
+ NS_ENSURE_SUCCESS(rv, false);
+ return !resultPrincipal->IsSystemPrincipal();
+ }
+ }
+
+ return true;
+}
+
+nsresult nsDocShell::AddToSessionHistory(
+ nsIURI* aURI, nsIChannel* aChannel, nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, bool aCloneChildren,
+ nsISHEntry** aNewEntry) {
+ MOZ_ASSERT(aURI, "uri is null");
+ MOZ_ASSERT(!aChannel || !aTriggeringPrincipal, "Shouldn't have both set");
+ MOZ_DIAGNOSTIC_ASSERT(!mozilla::SessionHistoryInParent());
+
+#if defined(DEBUG)
+ if (MOZ_LOG_TEST(gDocShellLog, LogLevel::Debug)) {
+ nsAutoCString chanName;
+ if (aChannel) {
+ aChannel->GetName(chanName);
+ } else {
+ chanName.AssignLiteral("<no channel>");
+ }
+
+ MOZ_LOG(gDocShellLog, LogLevel::Debug,
+ ("nsDocShell[%p]::AddToSessionHistory(\"%s\", [%s])\n", this,
+ aURI->GetSpecOrDefault().get(), chanName.get()));
+ }
+#endif
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsISHEntry> entry;
+
+ /*
+ * If this is a LOAD_FLAGS_REPLACE_HISTORY in a subframe, we use
+ * the existing SH entry in the page and replace the url and
+ * other vitalities.
+ */
+ if (LOAD_TYPE_HAS_FLAGS(mLoadType, LOAD_FLAGS_REPLACE_HISTORY) &&
+ !mBrowsingContext->IsTop()) {
+ // This is a subframe
+ entry = mOSHE;
+ if (entry) {
+ entry->ClearEntry();
+ }
+ }
+
+ // Create a new entry if necessary.
+ if (!entry) {
+ entry = new nsSHEntry();
+ }
+
+ // Get the post data & referrer
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIURI> originalURI;
+ nsCOMPtr<nsIURI> resultPrincipalURI;
+ nsCOMPtr<nsIURI> unstrippedURI;
+ bool loadReplace = false;
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ uint32_t cacheKey = 0;
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal = aTriggeringPrincipal;
+ nsCOMPtr<nsIPrincipal> principalToInherit = aPrincipalToInherit;
+ nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit =
+ aPartitionedPrincipalToInherit;
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aCsp;
+ bool expired = false; // by default the page is not expired
+ bool discardLayoutState = false;
+ nsCOMPtr<nsICacheInfoChannel> cacheChannel;
+ bool userActivation = false;
+
+ if (aChannel) {
+ cacheChannel = do_QueryInterface(aChannel);
+
+ /* If there is a caching channel, get the Cache Key and store it
+ * in SH.
+ */
+ if (cacheChannel) {
+ cacheChannel->GetCacheKey(&cacheKey);
+ }
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel));
+
+ // Check if the httpChannel is hiding under a multipartChannel
+ if (!httpChannel) {
+ GetHttpChannel(aChannel, getter_AddRefs(httpChannel));
+ }
+ if (httpChannel) {
+ nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(httpChannel));
+ if (uploadChannel) {
+ uploadChannel->GetUploadStream(getter_AddRefs(inputStream));
+ }
+ httpChannel->GetOriginalURI(getter_AddRefs(originalURI));
+ uint32_t loadFlags;
+ aChannel->GetLoadFlags(&loadFlags);
+ loadReplace = loadFlags & nsIChannel::LOAD_REPLACE;
+ rv = httpChannel->GetReferrerInfo(getter_AddRefs(referrerInfo));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ discardLayoutState = ShouldDiscardLayoutState(httpChannel);
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ if (!triggeringPrincipal) {
+ triggeringPrincipal = loadInfo->TriggeringPrincipal();
+ }
+ if (!csp) {
+ csp = loadInfo->GetCspToInherit();
+ }
+
+ loadInfo->GetResultPrincipalURI(getter_AddRefs(resultPrincipalURI));
+
+ loadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI));
+
+ userActivation = loadInfo->GetHasValidUserGestureActivation();
+
+ // For now keep storing just the principal in the SHEntry.
+ if (!principalToInherit) {
+ if (loadInfo->GetLoadingSandboxed()) {
+ if (loadInfo->GetLoadingPrincipal()) {
+ principalToInherit = NullPrincipal::CreateWithInheritedAttributes(
+ loadInfo->GetLoadingPrincipal());
+ } else {
+ // get the OriginAttributes
+ OriginAttributes attrs;
+ loadInfo->GetOriginAttributes(&attrs);
+ principalToInherit = NullPrincipal::Create(attrs);
+ }
+ } else {
+ principalToInherit = loadInfo->PrincipalToInherit();
+ }
+ }
+
+ if (!partitionedPrincipalToInherit) {
+ // XXXehsan is it correct to fall back to the principal to inherit in all
+ // cases? For example, what about the cases where we are using the load
+ // info's principal to inherit? Do we need to add a similar concept to
+ // load info for partitioned principal?
+ partitionedPrincipalToInherit = principalToInherit;
+ }
+ }
+
+ nsAutoString srcdoc;
+ bool srcdocEntry = false;
+ nsCOMPtr<nsIURI> baseURI;
+
+ nsCOMPtr<nsIInputStreamChannel> inStrmChan = do_QueryInterface(aChannel);
+ if (inStrmChan) {
+ bool isSrcdocChannel;
+ inStrmChan->GetIsSrcdocChannel(&isSrcdocChannel);
+ if (isSrcdocChannel) {
+ inStrmChan->GetSrcdocData(srcdoc);
+ srcdocEntry = true;
+ inStrmChan->GetBaseURI(getter_AddRefs(baseURI));
+ } else {
+ srcdoc.SetIsVoid(true);
+ }
+ }
+ /* If cache got a 'no-store', ask SH not to store
+ * HistoryLayoutState. By default, SH will set this
+ * flag to true and save HistoryLayoutState.
+ */
+ bool saveLayoutState = !discardLayoutState;
+
+ if (cacheChannel) {
+ // Check if the page has expired from cache
+ uint32_t expTime = 0;
+ cacheChannel->GetCacheTokenExpirationTime(&expTime);
+ uint32_t now = PRTimeToSeconds(PR_Now());
+ if (expTime <= now) {
+ expired = true;
+ }
+ }
+
+ // Title is set in nsDocShell::SetTitle()
+ entry->Create(aURI, // uri
+ u""_ns, // Title
+ inputStream, // Post data stream
+ cacheKey, // CacheKey
+ mContentTypeHint, // Content-type
+ triggeringPrincipal, // Channel or provided principal
+ principalToInherit, partitionedPrincipalToInherit, csp,
+ HistoryID(), GetCreatedDynamically(), originalURI,
+ resultPrincipalURI, unstrippedURI, loadReplace, referrerInfo,
+ srcdoc, srcdocEntry, baseURI, saveLayoutState, expired,
+ userActivation);
+
+ if (mBrowsingContext->IsTop() && GetSessionHistory()) {
+ bool shouldPersist = ShouldAddToSessionHistory(aURI, aChannel);
+ Maybe<int32_t> previousEntryIndex;
+ Maybe<int32_t> loadedEntryIndex;
+ rv = GetSessionHistory()->LegacySHistory()->AddToRootSessionHistory(
+ aCloneChildren, mOSHE, mBrowsingContext, entry, mLoadType,
+ shouldPersist, &previousEntryIndex, &loadedEntryIndex);
+
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Could not add entry to root session history");
+ if (previousEntryIndex.isSome()) {
+ mPreviousEntryIndex = previousEntryIndex.value();
+ }
+ if (loadedEntryIndex.isSome()) {
+ mLoadedEntryIndex = loadedEntryIndex.value();
+ }
+
+ // aCloneChildren implies that we are retaining the same document, thus we
+ // need to signal to the top WC that the new SHEntry may receive a fresh
+ // user interaction flag.
+ if (aCloneChildren) {
+ WindowContext* topWc = mBrowsingContext->GetTopWindowContext();
+ if (topWc && !topWc->IsDiscarded()) {
+ MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(false));
+ }
+ }
+ } else {
+ // This is a subframe, make sure that this new SHEntry will be
+ // marked with user interaction.
+ WindowContext* topWc = mBrowsingContext->GetTopWindowContext();
+ if (topWc && !topWc->IsDiscarded()) {
+ MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(false));
+ }
+ if (!mOSHE || !LOAD_TYPE_HAS_FLAGS(mLoadType, LOAD_FLAGS_REPLACE_HISTORY)) {
+ rv = AddChildSHEntryToParent(entry, mBrowsingContext->ChildOffset(),
+ aCloneChildren);
+ }
+ }
+
+ // Return the new SH entry...
+ if (aNewEntry) {
+ *aNewEntry = nullptr;
+ if (NS_SUCCEEDED(rv)) {
+ entry.forget(aNewEntry);
+ }
+ }
+
+ return rv;
+}
+
+void nsDocShell::UpdateActiveEntry(
+ bool aReplace, const Maybe<nsPoint>& aPreviousScrollPos, nsIURI* aURI,
+ nsIURI* aOriginalURI, nsIReferrerInfo* aReferrerInfo,
+ nsIPrincipal* aTriggeringPrincipal, nsIContentSecurityPolicy* aCsp,
+ const nsAString& aTitle, bool aScrollRestorationIsManual,
+ nsIStructuredCloneContainer* aData, bool aURIWasModified) {
+ MOZ_ASSERT(mozilla::SessionHistoryInParent());
+ MOZ_ASSERT(aURI, "uri is null");
+ MOZ_ASSERT(mLoadType == LOAD_PUSHSTATE,
+ "This code only deals with pushState");
+ MOZ_ASSERT_IF(aPreviousScrollPos.isSome(), !aReplace);
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Creating an active entry on nsDocShell %p to %s", this,
+ aURI->GetSpecOrDefault().get()));
+
+ // Even if we're replacing an existing entry we create new a
+ // SessionHistoryInfo. In the parent process we'll keep the existing
+ // SessionHistoryEntry, but just replace its SessionHistoryInfo, that way the
+ // entry keeps identity but its data is replaced.
+ bool replace = aReplace && mActiveEntry;
+
+ if (!replace) {
+ CollectWireframe();
+ }
+
+ if (mActiveEntry) {
+ // Link this entry to the previous active entry.
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(*mActiveEntry, aURI);
+ } else {
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(
+ aURI, aTriggeringPrincipal, nullptr, nullptr, aCsp, mContentTypeHint);
+ }
+ mActiveEntry->SetOriginalURI(aOriginalURI);
+ mActiveEntry->SetUnstrippedURI(nullptr);
+ mActiveEntry->SetReferrerInfo(aReferrerInfo);
+ mActiveEntry->SetTitle(aTitle);
+ mActiveEntry->SetStateData(static_cast<nsStructuredCloneContainer*>(aData));
+ mActiveEntry->SetURIWasModified(aURIWasModified);
+ mActiveEntry->SetScrollRestorationIsManual(aScrollRestorationIsManual);
+
+ if (replace) {
+ mBrowsingContext->ReplaceActiveSessionHistoryEntry(mActiveEntry.get());
+ } else {
+ mBrowsingContext->IncrementHistoryEntryCountForBrowsingContext();
+ // FIXME We should probably just compute mChildOffset in the parent
+ // instead of passing it over IPC here.
+ mBrowsingContext->SetActiveSessionHistoryEntry(
+ aPreviousScrollPos, mActiveEntry.get(), mLoadType,
+ /* aCacheKey = */ 0);
+ // FIXME Do we need to update mPreviousEntryIndex and mLoadedEntryIndex?
+ }
+}
+
+nsresult nsDocShell::LoadHistoryEntry(nsISHEntry* aEntry, uint32_t aLoadType,
+ bool aUserActivation) {
+ NS_ENSURE_TRUE(aEntry, NS_ERROR_FAILURE);
+
+ nsresult rv;
+ RefPtr<nsDocShellLoadState> loadState;
+ rv = aEntry->CreateLoadInfo(getter_AddRefs(loadState));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Calling CreateAboutBlankDocumentViewer can set mOSHE to null, and if
+ // that's the only thing holding a ref to aEntry that will cause aEntry to
+ // die while we're loading it. So hold a strong ref to aEntry here, just
+ // in case.
+ nsCOMPtr<nsISHEntry> kungFuDeathGrip(aEntry);
+
+ loadState->SetHasValidUserGestureActivation(
+ loadState->HasValidUserGestureActivation() || aUserActivation);
+
+ return LoadHistoryEntry(loadState, aLoadType, aEntry == mOSHE);
+}
+
+nsresult nsDocShell::LoadHistoryEntry(const LoadingSessionHistoryInfo& aEntry,
+ uint32_t aLoadType,
+ bool aUserActivation) {
+ RefPtr<nsDocShellLoadState> loadState = aEntry.CreateLoadInfo();
+ loadState->SetHasValidUserGestureActivation(
+ loadState->HasValidUserGestureActivation() || aUserActivation);
+
+ return LoadHistoryEntry(loadState, aLoadType, aEntry.mLoadingCurrentEntry);
+}
+
+nsresult nsDocShell::LoadHistoryEntry(nsDocShellLoadState* aLoadState,
+ uint32_t aLoadType,
+ bool aLoadingCurrentEntry) {
+ if (!IsNavigationAllowed()) {
+ return NS_OK;
+ }
+
+ // We are setting load type afterwards so we don't have to
+ // send it in an IPC message
+ aLoadState->SetLoadType(aLoadType);
+
+ nsresult rv;
+ if (SchemeIsJavascript(aLoadState->URI())) {
+ // We're loading a URL that will execute script from inside asyncOpen.
+ // Replace the current document with about:blank now to prevent
+ // anything from the current document from leaking into any JavaScript
+ // code in the URL.
+ // Don't cache the presentation if we're going to just reload the
+ // current entry. Caching would lead to trying to save the different
+ // content viewers in the same nsISHEntry object.
+ rv = CreateAboutBlankDocumentViewer(
+ aLoadState->PrincipalToInherit(),
+ aLoadState->PartitionedPrincipalToInherit(), nullptr, nullptr,
+ /* aIsInitialDocument */ false, Nothing(), !aLoadingCurrentEntry);
+
+ if (NS_FAILED(rv)) {
+ // The creation of the intermittent about:blank content
+ // viewer failed for some reason (potentially because the
+ // user prevented it). Interrupt the history load.
+ return NS_OK;
+ }
+
+ if (!aLoadState->TriggeringPrincipal()) {
+ // Ensure that we have a triggeringPrincipal. Otherwise javascript:
+ // URIs will pick it up from the about:blank page we just loaded,
+ // and we don't really want even that in this case.
+ nsCOMPtr<nsIPrincipal> principal =
+ NullPrincipal::Create(GetOriginAttributes());
+ aLoadState->SetTriggeringPrincipal(principal);
+ }
+ }
+
+ /* If there is a valid postdata *and* the user pressed
+ * reload or shift-reload, take user's permission before we
+ * repost the data to the server.
+ */
+ if ((aLoadType & LOAD_CMD_RELOAD) && aLoadState->PostDataStream()) {
+ bool repost;
+ rv = ConfirmRepost(&repost);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // If the user pressed cancel in the dialog, return. We're done here.
+ if (!repost) {
+ return NS_BINDING_ABORTED;
+ }
+ }
+
+ // If there is no valid triggeringPrincipal, we deny the load
+ MOZ_ASSERT(aLoadState->TriggeringPrincipal(),
+ "need a valid triggeringPrincipal to load from history");
+ if (!aLoadState->TriggeringPrincipal()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return InternalLoad(aLoadState); // No nsIRequest
+}
+
+NS_IMETHODIMP
+nsDocShell::PersistLayoutHistoryState() {
+ nsresult rv = NS_OK;
+
+ if (mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) {
+ bool scrollRestorationIsManual;
+ if (mozilla::SessionHistoryInParent()) {
+ scrollRestorationIsManual = mActiveEntry->GetScrollRestorationIsManual();
+ } else {
+ scrollRestorationIsManual = mOSHE->GetScrollRestorationIsManual();
+ }
+ nsCOMPtr<nsILayoutHistoryState> layoutState;
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ rv = presShell->CaptureHistoryState(getter_AddRefs(layoutState));
+ } else if (scrollRestorationIsManual) {
+ // Even if we don't have layout anymore, we may want to reset the
+ // current scroll state in layout history.
+ GetLayoutHistoryState(getter_AddRefs(layoutState));
+ }
+
+ if (scrollRestorationIsManual && layoutState) {
+ layoutState->ResetScrollState();
+ }
+ }
+
+ return rv;
+}
+
+void nsDocShell::SwapHistoryEntries(nsISHEntry* aOldEntry,
+ nsISHEntry* aNewEntry) {
+ if (aOldEntry == mOSHE) {
+ mOSHE = aNewEntry;
+ }
+
+ if (aOldEntry == mLSHE) {
+ mLSHE = aNewEntry;
+ }
+}
+
+void nsDocShell::SetHistoryEntryAndUpdateBC(const Maybe<nsISHEntry*>& aLSHE,
+ const Maybe<nsISHEntry*>& aOSHE) {
+ // We want to hold on to the reference in mLSHE before we update it.
+ // Otherwise, SetHistoryEntry could release the last reference to
+ // the entry while aOSHE is pointing to it.
+ nsCOMPtr<nsISHEntry> deathGripOldLSHE;
+ if (aLSHE.isSome()) {
+ deathGripOldLSHE = SetHistoryEntry(&mLSHE, aLSHE.value());
+ MOZ_ASSERT(mLSHE.get() == aLSHE.value());
+ }
+ nsCOMPtr<nsISHEntry> deathGripOldOSHE;
+ if (aOSHE.isSome()) {
+ deathGripOldOSHE = SetHistoryEntry(&mOSHE, aOSHE.value());
+ MOZ_ASSERT(mOSHE.get() == aOSHE.value());
+ }
+}
+
+already_AddRefed<nsISHEntry> nsDocShell::SetHistoryEntry(
+ nsCOMPtr<nsISHEntry>* aPtr, nsISHEntry* aEntry) {
+ // We need to sync up the docshell and session history trees for
+ // subframe navigation. If the load was in a subframe, we forward up to
+ // the root docshell, which will then recursively sync up all docshells
+ // to their corresponding entries in the new session history tree.
+ // If we don't do this, then we can cache a content viewer on the wrong
+ // cloned entry, and subsequently restore it at the wrong time.
+ RefPtr<BrowsingContext> topBC = mBrowsingContext->Top();
+ if (topBC->IsDiscarded()) {
+ topBC = nullptr;
+ }
+ RefPtr<BrowsingContext> currBC =
+ mBrowsingContext->IsDiscarded() ? nullptr : mBrowsingContext;
+ if (topBC && *aPtr) {
+ (*aPtr)->SyncTreesForSubframeNavigation(aEntry, topBC, currBC);
+ }
+ nsCOMPtr<nsISHEntry> entry(aEntry);
+ entry.swap(*aPtr);
+ return entry.forget();
+}
+
+already_AddRefed<ChildSHistory> nsDocShell::GetRootSessionHistory() {
+ RefPtr<ChildSHistory> childSHistory =
+ mBrowsingContext->Top()->GetChildSessionHistory();
+ return childSHistory.forget();
+}
+
+nsresult nsDocShell::GetHttpChannel(nsIChannel* aChannel,
+ nsIHttpChannel** aReturn) {
+ NS_ENSURE_ARG_POINTER(aReturn);
+ if (!aChannel) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIMultiPartChannel> multiPartChannel(do_QueryInterface(aChannel));
+ if (multiPartChannel) {
+ nsCOMPtr<nsIChannel> baseChannel;
+ multiPartChannel->GetBaseChannel(getter_AddRefs(baseChannel));
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(baseChannel));
+ *aReturn = httpChannel;
+ NS_IF_ADDREF(*aReturn);
+ }
+ return NS_OK;
+}
+
+bool nsDocShell::ShouldDiscardLayoutState(nsIHttpChannel* aChannel) {
+ // By default layout State will be saved.
+ if (!aChannel) {
+ return false;
+ }
+
+ // figure out if SH should be saving layout state
+ bool noStore = false;
+ Unused << aChannel->IsNoStoreResponse(&noStore);
+ return noStore;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetEditor(nsIEditor** aEditor) {
+ NS_ENSURE_ARG_POINTER(aEditor);
+ RefPtr<HTMLEditor> htmlEditor = GetHTMLEditorInternal();
+ htmlEditor.forget(aEditor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetEditor(nsIEditor* aEditor) {
+ HTMLEditor* htmlEditor = aEditor ? aEditor->GetAsHTMLEditor() : nullptr;
+ // If TextEditor comes, throw an error.
+ if (aEditor && !htmlEditor) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ return SetHTMLEditorInternal(htmlEditor);
+}
+
+HTMLEditor* nsDocShell::GetHTMLEditorInternal() {
+ return mEditorData ? mEditorData->GetHTMLEditor() : nullptr;
+}
+
+nsresult nsDocShell::SetHTMLEditorInternal(HTMLEditor* aHTMLEditor) {
+ if (!aHTMLEditor && !mEditorData) {
+ return NS_OK;
+ }
+
+ nsresult rv = EnsureEditorData();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return mEditorData->SetHTMLEditor(aHTMLEditor);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetEditable(bool* aEditable) {
+ NS_ENSURE_ARG_POINTER(aEditable);
+ *aEditable = mEditorData && mEditorData->GetEditable();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetHasEditingSession(bool* aHasEditingSession) {
+ NS_ENSURE_ARG_POINTER(aHasEditingSession);
+
+ if (mEditorData) {
+ *aHasEditingSession = !!mEditorData->GetEditingSession();
+ } else {
+ *aHasEditingSession = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::MakeEditable(bool aInWaitForUriLoad) {
+ nsresult rv = EnsureEditorData();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return mEditorData->MakeEditable(aInWaitForUriLoad);
+}
+
+/* static */ bool nsDocShell::ShouldAddURIVisit(nsIChannel* aChannel) {
+ bool needToAddURIVisit = true;
+ nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aChannel));
+ if (props) {
+ mozilla::Unused << props->GetPropertyAsBool(
+ u"docshell.needToAddURIVisit"_ns, &needToAddURIVisit);
+ }
+
+ return needToAddURIVisit;
+}
+
+/* static */ void nsDocShell::ExtractLastVisit(
+ nsIChannel* aChannel, nsIURI** aURI, uint32_t* aChannelRedirectFlags) {
+ nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aChannel));
+ if (!props) {
+ return;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIURI> uri(do_GetProperty(props, u"docshell.previousURI"_ns, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ uri.forget(aURI);
+
+ rv = props->GetPropertyAsUint32(u"docshell.previousFlags"_ns,
+ aChannelRedirectFlags);
+
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Could not fetch previous flags, URI will be treated like referrer");
+
+ } else {
+ // There is no last visit for this channel, so this must be the first
+ // link. Link the visit to the referrer of this request, if any.
+ // Treat referrer as null if there is an error getting it.
+ NS_GetReferrerFromChannel(aChannel, aURI);
+ }
+}
+
+void nsDocShell::SaveLastVisit(nsIChannel* aChannel, nsIURI* aURI,
+ uint32_t aChannelRedirectFlags) {
+ nsCOMPtr<nsIWritablePropertyBag2> props(do_QueryInterface(aChannel));
+ if (!props || !aURI) {
+ return;
+ }
+
+ props->SetPropertyAsInterface(u"docshell.previousURI"_ns, aURI);
+ props->SetPropertyAsUint32(u"docshell.previousFlags"_ns,
+ aChannelRedirectFlags);
+}
+
+/* static */ void nsDocShell::InternalAddURIVisit(
+ nsIURI* aURI, nsIURI* aPreviousURI, uint32_t aChannelRedirectFlags,
+ uint32_t aResponseStatus, BrowsingContext* aBrowsingContext,
+ nsIWidget* aWidget, uint32_t aLoadType, bool aWasUpgraded) {
+ MOZ_ASSERT(aURI, "Visited URI is null!");
+ MOZ_ASSERT(aLoadType != LOAD_ERROR_PAGE && aLoadType != LOAD_BYPASS_HISTORY,
+ "Do not add error or bypass pages to global history");
+
+ bool usePrivateBrowsing = false;
+ aBrowsingContext->GetUsePrivateBrowsing(&usePrivateBrowsing);
+
+ // Only content-type docshells save URI visits. Also don't do
+ // anything here if we're not supposed to use global history.
+ if (!aBrowsingContext->IsContent() ||
+ !aBrowsingContext->GetUseGlobalHistory() || usePrivateBrowsing) {
+ return;
+ }
+
+ nsCOMPtr<IHistory> history = components::History::Service();
+
+ if (history) {
+ uint32_t visitURIFlags = 0;
+
+ if (aBrowsingContext->IsTop()) {
+ visitURIFlags |= IHistory::TOP_LEVEL;
+ }
+
+ if (aChannelRedirectFlags & nsIChannelEventSink::REDIRECT_TEMPORARY) {
+ visitURIFlags |= IHistory::REDIRECT_TEMPORARY;
+ } else if (aChannelRedirectFlags &
+ nsIChannelEventSink::REDIRECT_PERMANENT) {
+ visitURIFlags |= IHistory::REDIRECT_PERMANENT;
+ } else {
+ MOZ_ASSERT(!aChannelRedirectFlags,
+ "One of REDIRECT_TEMPORARY or REDIRECT_PERMANENT must be set "
+ "if any flags in aChannelRedirectFlags is set.");
+ }
+
+ if (aResponseStatus >= 300 && aResponseStatus < 400) {
+ visitURIFlags |= IHistory::REDIRECT_SOURCE;
+ if (aResponseStatus == 301 || aResponseStatus == 308) {
+ visitURIFlags |= IHistory::REDIRECT_SOURCE_PERMANENT;
+ }
+ }
+ // Errors 400-501 and 505 are considered unrecoverable, in the sense a
+ // simple retry attempt by the user is unlikely to solve them.
+ // 408 is special cased, since may actually indicate a temporary
+ // connection problem.
+ else if (aResponseStatus != 408 &&
+ ((aResponseStatus >= 400 && aResponseStatus <= 501) ||
+ aResponseStatus == 505)) {
+ visitURIFlags |= IHistory::UNRECOVERABLE_ERROR;
+ }
+
+ if (aWasUpgraded) {
+ visitURIFlags |=
+ IHistory::REDIRECT_SOURCE | IHistory::REDIRECT_SOURCE_UPGRADED;
+ }
+
+ mozilla::Unused << history->VisitURI(aWidget, aURI, aPreviousURI,
+ visitURIFlags,
+ aBrowsingContext->BrowserId());
+ }
+}
+
+void nsDocShell::AddURIVisit(nsIURI* aURI, nsIURI* aPreviousURI,
+ uint32_t aChannelRedirectFlags,
+ uint32_t aResponseStatus) {
+ nsPIDOMWindowOuter* outer = GetWindow();
+ nsCOMPtr<nsIWidget> widget = widget::WidgetUtils::DOMWindowToWidget(outer);
+
+ InternalAddURIVisit(aURI, aPreviousURI, aChannelRedirectFlags,
+ aResponseStatus, mBrowsingContext, widget, mLoadType,
+ false);
+}
+
+//*****************************************************************************
+// nsDocShell: Helper Routines
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::SetLoadType(uint32_t aLoadType) {
+ mLoadType = aLoadType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetLoadType(uint32_t* aLoadType) {
+ *aLoadType = mLoadType;
+ return NS_OK;
+}
+
+nsresult nsDocShell::ConfirmRepost(bool* aRepost) {
+ if (StaticPrefs::dom_confirm_repost_testing_always_accept()) {
+ *aRepost = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPromptCollection> prompter =
+ do_GetService("@mozilla.org/embedcomp/prompt-collection;1");
+ if (!prompter) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return prompter->ConfirmRepost(mBrowsingContext, aRepost);
+}
+
+nsresult nsDocShell::GetPromptAndStringBundle(nsIPrompt** aPrompt,
+ nsIStringBundle** aStringBundle) {
+ NS_ENSURE_SUCCESS(GetInterface(NS_GET_IID(nsIPrompt), (void**)aPrompt),
+ NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(stringBundleService, NS_ERROR_FAILURE);
+
+ NS_ENSURE_SUCCESS(
+ stringBundleService->CreateBundle(kAppstringsBundleURL, aStringBundle),
+ NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsIScrollableFrame* nsDocShell::GetRootScrollFrame() {
+ PresShell* presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, nullptr);
+
+ return presShell->GetRootScrollFrameAsScrollable();
+}
+
+nsresult nsDocShell::EnsureScriptEnvironment() {
+ if (mScriptGlobal) {
+ return NS_OK;
+ }
+
+ if (mIsBeingDestroyed) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+#ifdef DEBUG
+ NS_ASSERTION(!mInEnsureScriptEnv,
+ "Infinite loop! Calling EnsureScriptEnvironment() from "
+ "within EnsureScriptEnvironment()!");
+
+ // Yeah, this isn't re-entrant safe, but that's ok since if we
+ // re-enter this method, we'll infinitely loop...
+ AutoRestore<bool> boolSetter(mInEnsureScriptEnv);
+ mInEnsureScriptEnv = true;
+#endif
+
+ nsCOMPtr<nsIWebBrowserChrome> browserChrome(do_GetInterface(mTreeOwner));
+ NS_ENSURE_TRUE(browserChrome, NS_ERROR_NOT_AVAILABLE);
+
+ uint32_t chromeFlags;
+ browserChrome->GetChromeFlags(&chromeFlags);
+
+ // If our window is modal and we're not opened as chrome, make
+ // this window a modal content window.
+ mScriptGlobal = nsGlobalWindowOuter::Create(this, mItemType == typeChrome);
+ MOZ_ASSERT(mScriptGlobal);
+
+ // Ensure the script object is set up to run script.
+ return mScriptGlobal->EnsureScriptEnvironment();
+}
+
+nsresult nsDocShell::EnsureEditorData() {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+
+ bool openDocHasDetachedEditor = mOSHE && mOSHE->HasDetachedEditor();
+ if (!mEditorData && !mIsBeingDestroyed && !openDocHasDetachedEditor) {
+ // We shouldn't recreate the editor data if it already exists, or
+ // we're shutting down, or we already have a detached editor data
+ // stored in the session history. We should only have one editordata
+ // per docshell.
+ mEditorData = MakeUnique<nsDocShellEditorData>(this);
+ }
+
+ return mEditorData ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsDocShell::EnsureFind() {
+ if (!mFind) {
+ mFind = new nsWebBrowserFind();
+ }
+
+ // we promise that the nsIWebBrowserFind that we return has been set
+ // up to point to the focused, or content window, so we have to
+ // set that up each time.
+
+ nsIScriptGlobalObject* scriptGO = GetScriptGlobalObject();
+ NS_ENSURE_TRUE(scriptGO, NS_ERROR_UNEXPECTED);
+
+ // default to our window
+ nsCOMPtr<nsPIDOMWindowOuter> ourWindow = do_QueryInterface(scriptGO);
+ nsCOMPtr<nsPIDOMWindowOuter> windowToSearch;
+ nsFocusManager::GetFocusedDescendant(ourWindow,
+ nsFocusManager::eIncludeAllDescendants,
+ getter_AddRefs(windowToSearch));
+
+ nsCOMPtr<nsIWebBrowserFindInFrames> findInFrames = do_QueryInterface(mFind);
+ if (!findInFrames) {
+ return NS_ERROR_NO_INTERFACE;
+ }
+
+ nsresult rv = findInFrames->SetRootSearchFrame(ourWindow);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ rv = findInFrames->SetCurrentSearchFrame(windowToSearch);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::IsBeingDestroyed(bool* aDoomed) {
+ NS_ENSURE_ARG(aDoomed);
+ *aDoomed = mIsBeingDestroyed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetIsExecutingOnLoadHandler(bool* aResult) {
+ NS_ENSURE_ARG(aResult);
+ *aResult = mIsExecutingOnLoadHandler;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetLayoutHistoryState(nsILayoutHistoryState** aLayoutHistoryState) {
+ nsCOMPtr<nsILayoutHistoryState> state;
+ if (mozilla::SessionHistoryInParent()) {
+ if (mActiveEntry) {
+ state = mActiveEntry->GetLayoutHistoryState();
+ }
+ } else {
+ if (mOSHE) {
+ state = mOSHE->GetLayoutHistoryState();
+ }
+ }
+ state.forget(aLayoutHistoryState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetLayoutHistoryState(nsILayoutHistoryState* aLayoutHistoryState) {
+ if (mOSHE) {
+ mOSHE->SetLayoutHistoryState(aLayoutHistoryState);
+ }
+ if (mActiveEntry) {
+ mActiveEntry->SetLayoutHistoryState(aLayoutHistoryState);
+ }
+ return NS_OK;
+}
+
+nsDocShell::InterfaceRequestorProxy::InterfaceRequestorProxy(
+ nsIInterfaceRequestor* aRequestor) {
+ if (aRequestor) {
+ mWeakPtr = do_GetWeakReference(aRequestor);
+ }
+}
+
+nsDocShell::InterfaceRequestorProxy::~InterfaceRequestorProxy() {
+ mWeakPtr = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsDocShell::InterfaceRequestorProxy, nsIInterfaceRequestor)
+
+NS_IMETHODIMP
+nsDocShell::InterfaceRequestorProxy::GetInterface(const nsIID& aIID,
+ void** aSink) {
+ NS_ENSURE_ARG_POINTER(aSink);
+ nsCOMPtr<nsIInterfaceRequestor> ifReq = do_QueryReferent(mWeakPtr);
+ if (ifReq) {
+ return ifReq->GetInterface(aIID, aSink);
+ }
+ *aSink = nullptr;
+ return NS_NOINTERFACE;
+}
+
+//*****************************************************************************
+// nsDocShell::nsIAuthPromptProvider
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::GetAuthPrompt(uint32_t aPromptReason, const nsIID& aIID,
+ void** aResult) {
+ // a priority prompt request will override a false mAllowAuth setting
+ bool priorityPrompt = (aPromptReason == PROMPT_PROXY);
+
+ if (!mAllowAuth && !priorityPrompt) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // we're either allowing auth, or it's a proxy request
+ nsresult rv;
+ nsCOMPtr<nsIPromptFactory> wwatch =
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnsureScriptEnvironment();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the an auth prompter for our window so that the parenting
+ // of the dialogs works as it should when using tabs.
+
+ return wwatch->GetPrompt(mScriptGlobal, aIID,
+ reinterpret_cast<void**>(aResult));
+}
+
+//*****************************************************************************
+// nsDocShell::nsILoadContext
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShell::GetAssociatedWindow(mozIDOMWindowProxy** aWindow) {
+ CallGetInterface(this, aWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetTopWindow(mozIDOMWindowProxy** aWindow) {
+ return mBrowsingContext->GetTopWindow(aWindow);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetTopFrameElement(Element** aElement) {
+ return mBrowsingContext->GetTopFrameElement(aElement);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetUseTrackingProtection(bool* aUseTrackingProtection) {
+ return mBrowsingContext->GetUseTrackingProtection(aUseTrackingProtection);
+}
+
+NS_IMETHODIMP
+nsDocShell::SetUseTrackingProtection(bool aUseTrackingProtection) {
+ return mBrowsingContext->SetUseTrackingProtection(aUseTrackingProtection);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetIsContent(bool* aIsContent) {
+ *aIsContent = (mItemType == typeContent);
+ return NS_OK;
+}
+
+bool nsDocShell::IsOKToLoadURI(nsIURI* aURI) {
+ MOZ_ASSERT(aURI, "Must have a URI!");
+
+ if (!mFiredUnloadEvent) {
+ return true;
+ }
+
+ if (!mLoadingURI) {
+ return false;
+ }
+
+ bool isPrivateWin = false;
+ Document* doc = GetDocument();
+ if (doc) {
+ isPrivateWin =
+ doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0;
+ }
+
+ nsCOMPtr<nsIScriptSecurityManager> secMan =
+ do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+ return secMan && NS_SUCCEEDED(secMan->CheckSameOriginURI(
+ aURI, mLoadingURI, false, isPrivateWin));
+}
+
+//
+// Routines for selection and clipboard
+//
+nsresult nsDocShell::GetControllerForCommand(const char* aCommand,
+ nsIController** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = nullptr;
+
+ NS_ENSURE_TRUE(mScriptGlobal, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsPIWindowRoot> root = mScriptGlobal->GetTopWindowRoot();
+ NS_ENSURE_TRUE(root, NS_ERROR_FAILURE);
+
+ return root->GetControllerForCommand(aCommand, false /* for any window */,
+ aResult);
+}
+
+NS_IMETHODIMP
+nsDocShell::IsCommandEnabled(const char* aCommand, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIController> controller;
+ rv = GetControllerForCommand(aCommand, getter_AddRefs(controller));
+ if (controller) {
+ rv = controller->IsCommandEnabled(aCommand, aResult);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocShell::DoCommand(const char* aCommand) {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIController> controller;
+ rv = GetControllerForCommand(aCommand, getter_AddRefs(controller));
+ if (controller) {
+ rv = controller->DoCommand(aCommand);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocShell::DoCommandWithParams(const char* aCommand,
+ nsICommandParams* aParams) {
+ nsCOMPtr<nsIController> controller;
+ nsresult rv = GetControllerForCommand(aCommand, getter_AddRefs(controller));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsICommandController> commandController =
+ do_QueryInterface(controller, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return commandController->DoCommandWithParams(aCommand, aParams);
+}
+
+nsresult nsDocShell::EnsureCommandHandler() {
+ if (!mCommandManager) {
+ if (nsCOMPtr<nsPIDOMWindowOuter> domWindow = GetWindow()) {
+ mCommandManager = new nsCommandManager(domWindow);
+ }
+ }
+ return mCommandManager ? NS_OK : NS_ERROR_FAILURE;
+}
+
+// link handling
+
+class OnLinkClickEvent : public Runnable {
+ public:
+ OnLinkClickEvent(nsDocShell* aHandler, nsIContent* aContent,
+ nsDocShellLoadState* aLoadState, bool aNoOpenerImplied,
+ bool aIsTrusted, nsIPrincipal* aTriggeringPrincipal);
+
+ NS_IMETHOD Run() override {
+ AutoPopupStatePusher popupStatePusher(mPopupState);
+
+ // We need to set up an AutoJSAPI here for the following reason: When we
+ // do OnLinkClickSync we'll eventually end up in
+ // nsGlobalWindow::OpenInternal which only does popup blocking if
+ // !LegacyIsCallerChromeOrNativeCode(). So we need to fake things so that
+ // we don't look like native code as far as LegacyIsCallerNativeCode() is
+ // concerned.
+ AutoJSAPI jsapi;
+ if (mIsTrusted || jsapi.Init(mContent->OwnerDoc()->GetScopeObject())) {
+ mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied,
+ mTriggeringPrincipal);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<nsDocShell> mHandler;
+ nsCOMPtr<nsIContent> mContent;
+ RefPtr<nsDocShellLoadState> mLoadState;
+ nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
+ PopupBlocker::PopupControlState mPopupState;
+ bool mNoOpenerImplied;
+ bool mIsTrusted;
+};
+
+OnLinkClickEvent::OnLinkClickEvent(nsDocShell* aHandler, nsIContent* aContent,
+ nsDocShellLoadState* aLoadState,
+ bool aNoOpenerImplied, bool aIsTrusted,
+ nsIPrincipal* aTriggeringPrincipal)
+ : mozilla::Runnable("OnLinkClickEvent"),
+ mHandler(aHandler),
+ mContent(aContent),
+ mLoadState(aLoadState),
+ mTriggeringPrincipal(aTriggeringPrincipal),
+ mPopupState(PopupBlocker::GetPopupControlState()),
+ mNoOpenerImplied(aNoOpenerImplied),
+ mIsTrusted(aIsTrusted) {}
+
+nsresult nsDocShell::OnLinkClick(
+ nsIContent* aContent, nsIURI* aURI, const nsAString& aTargetSpec,
+ const nsAString& aFileName, nsIInputStream* aPostDataStream,
+ nsIInputStream* aHeadersDataStream, bool aIsUserTriggered, bool aIsTrusted,
+ nsIPrincipal* aTriggeringPrincipal, nsIContentSecurityPolicy* aCsp) {
+#ifndef ANDROID
+ MOZ_ASSERT(aTriggeringPrincipal, "Need a valid triggeringPrincipal");
+#endif
+ NS_ASSERTION(NS_IsMainThread(), "wrong thread");
+
+ if (!IsNavigationAllowed() || !IsOKToLoadURI(aURI)) {
+ return NS_OK;
+ }
+
+ // On history navigation through Back/Forward buttons, don't execute
+ // automatic JavaScript redirection such as |anchorElement.click()| or
+ // |formElement.submit()|.
+ //
+ // XXX |formElement.submit()| bypasses this checkpoint because it calls
+ // nsDocShell::OnLinkClickSync(...) instead.
+ if (ShouldBlockLoadingForBackButton()) {
+ return NS_OK;
+ }
+
+ if (aContent->IsEditable()) {
+ return NS_OK;
+ }
+
+ Document* ownerDoc = aContent->OwnerDoc();
+ if (nsContentUtils::IsExternalProtocol(aURI)) {
+ ownerDoc->EnsureNotEnteringAndExitFullscreen();
+ }
+
+ bool noOpenerImplied = false;
+ nsAutoString target(aTargetSpec);
+ if (aFileName.IsVoid() &&
+ ShouldOpenInBlankTarget(aTargetSpec, aURI, aContent, aIsUserTriggered)) {
+ target = u"_blank";
+ if (!aTargetSpec.Equals(target)) {
+ noOpenerImplied = true;
+ }
+ }
+
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI);
+ loadState->SetTarget(target);
+ loadState->SetFileName(aFileName);
+ loadState->SetPostDataStream(aPostDataStream);
+ loadState->SetHeadersStream(aHeadersDataStream);
+ loadState->SetFirstParty(true);
+ loadState->SetTriggeringPrincipal(
+ aTriggeringPrincipal ? aTriggeringPrincipal : aContent->NodePrincipal());
+ loadState->SetPrincipalToInherit(aContent->NodePrincipal());
+ loadState->SetCsp(aCsp ? aCsp : aContent->GetCsp());
+ loadState->SetAllowFocusMove(UserActivation::IsHandlingUserInput());
+
+ nsCOMPtr<nsIRunnable> ev =
+ new OnLinkClickEvent(this, aContent, loadState, noOpenerImplied,
+ aIsTrusted, aTriggeringPrincipal);
+ return Dispatch(ev.forget());
+}
+
+bool nsDocShell::ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
+ nsIURI* aLinkURI, nsIContent* aContent,
+ bool aIsUserTriggered) {
+ if (net::SchemeIsJavascript(aLinkURI)) {
+ return false;
+ }
+
+ // External links from within app tabs should always open in new tabs
+ // instead of replacing the app tab's page (Bug 575561)
+ // nsIURI.host can throw for non-nsStandardURL nsIURIs. If we fail to
+ // get either host, just return false to use the original target.
+ nsAutoCString linkHost;
+ if (NS_FAILED(aLinkURI->GetHost(linkHost))) {
+ return false;
+ }
+
+ // The targetTopLevelLinkClicksToBlank property on BrowsingContext allows
+ // privileged code to change the default targeting behaviour. In particular,
+ // if a user-initiated link click for the (or targetting the) top-level frame
+ // is detected, we default the target to "_blank" to give it a new
+ // top-level BrowsingContext.
+ if (mBrowsingContext->TargetTopLevelLinkClicksToBlank() && aIsUserTriggered &&
+ ((aOriginalTarget.IsEmpty() && mBrowsingContext->IsTop()) ||
+ aOriginalTarget == u"_top"_ns)) {
+ return true;
+ }
+
+ // Don't modify non-default targets.
+ if (!aOriginalTarget.IsEmpty()) {
+ return false;
+ }
+
+ // Only check targets that are in extension panels or app tabs.
+ // (isAppTab will be false for app tab subframes).
+ nsString mmGroup = mBrowsingContext->Top()->GetMessageManagerGroup();
+ if (!mmGroup.EqualsLiteral("webext-browsers") &&
+ !mBrowsingContext->IsAppTab()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> docURI = aContent->OwnerDoc()->GetDocumentURIObject();
+ if (!docURI) {
+ return false;
+ }
+
+ nsAutoCString docHost;
+ if (NS_FAILED(docURI->GetHost(docHost))) {
+ return false;
+ }
+
+ if (linkHost.Equals(docHost)) {
+ return false;
+ }
+
+ // Special case: ignore "www" prefix if it is part of host string
+ return linkHost.Length() < docHost.Length()
+ ? !docHost.Equals("www."_ns + linkHost)
+ : !linkHost.Equals("www."_ns + docHost);
+}
+
+static bool ElementCanHaveNoopener(nsIContent* aContent) {
+ // Make sure we are dealing with either an <A>, <AREA>, or <FORM> element in
+ // the HTML, XHTML, or SVG namespace.
+ return aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
+ nsGkAtoms::form) ||
+ aContent->IsSVGElement(nsGkAtoms::a);
+}
+
+nsresult nsDocShell::OnLinkClickSync(nsIContent* aContent,
+ nsDocShellLoadState* aLoadState,
+ bool aNoOpenerImplied,
+ nsIPrincipal* aTriggeringPrincipal) {
+ if (!IsNavigationAllowed() || !IsOKToLoadURI(aLoadState->URI())) {
+ return NS_OK;
+ }
+
+ // XXX When the linking node was HTMLFormElement, it is synchronous event.
+ // That is, the caller of this method is not |OnLinkClickEvent::Run()|
+ // but |HTMLFormElement::SubmitSubmission(...)|.
+ if (aContent->IsHTMLElement(nsGkAtoms::form) &&
+ ShouldBlockLoadingForBackButton()) {
+ return NS_OK;
+ }
+
+ if (aContent->IsEditable()) {
+ return NS_OK;
+ }
+
+ // if the triggeringPrincipal is not passed explicitly, then we
+ // fall back to using doc->NodePrincipal() as the triggeringPrincipal.
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal =
+ aTriggeringPrincipal ? aTriggeringPrincipal : aContent->NodePrincipal();
+
+ {
+ // defer to an external protocol handler if necessary...
+ nsCOMPtr<nsIExternalProtocolService> extProtService =
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
+ if (extProtService) {
+ nsAutoCString scheme;
+ aLoadState->URI()->GetScheme(scheme);
+ if (!scheme.IsEmpty()) {
+ // if the URL scheme does not correspond to an exposed protocol, then
+ // we need to hand this link click over to the external protocol
+ // handler.
+ bool isExposed;
+ nsresult rv =
+ extProtService->IsExposedProtocol(scheme.get(), &isExposed);
+ if (NS_SUCCEEDED(rv) && !isExposed) {
+ return extProtService->LoadURI(
+ aLoadState->URI(), triggeringPrincipal, nullptr, mBrowsingContext,
+ /* aTriggeredExternally */
+ false,
+ /* aHasValidUserGestureActivation */
+ aContent->OwnerDoc()->HasValidTransientUserGestureActivation());
+ }
+ }
+ }
+ }
+ uint32_t triggeringSandboxFlags = 0;
+ uint64_t triggeringWindowId = 0;
+ bool triggeringStorageAccess = false;
+ if (mBrowsingContext) {
+ triggeringSandboxFlags = aContent->OwnerDoc()->GetSandboxFlags();
+ triggeringWindowId = aContent->OwnerDoc()->InnerWindowID();
+ triggeringStorageAccess = aContent->OwnerDoc()->UsingStorageAccess();
+ }
+
+ uint32_t flags = INTERNAL_LOAD_FLAGS_NONE;
+ bool elementCanHaveNoopener = ElementCanHaveNoopener(aContent);
+ bool triggeringPrincipalIsSystemPrincipal =
+ aLoadState->TriggeringPrincipal()->IsSystemPrincipal();
+ if (elementCanHaveNoopener) {
+ MOZ_ASSERT(aContent->IsHTMLElement() || aContent->IsSVGElement());
+ nsAutoString relString;
+ aContent->AsElement()->GetAttr(nsGkAtoms::rel, relString);
+ nsWhitespaceTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok(
+ relString);
+
+ bool targetBlank = aLoadState->Target().LowerCaseEqualsLiteral("_blank");
+ bool explicitOpenerSet = false;
+
+ // The opener behaviour follows a hierarchy, such that if a higher
+ // priority behaviour is specified, it always takes priority. That
+ // priority is currently: norefrerer > noopener > opener > default
+
+ while (tok.hasMoreTokens()) {
+ const nsAString& token = tok.nextToken();
+ if (token.LowerCaseEqualsLiteral("noreferrer")) {
+ flags |= INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER |
+ INTERNAL_LOAD_FLAGS_NO_OPENER;
+ // noreferrer cannot be overwritten by a 'rel=opener'.
+ explicitOpenerSet = true;
+ break;
+ }
+
+ if (token.LowerCaseEqualsLiteral("noopener")) {
+ flags |= INTERNAL_LOAD_FLAGS_NO_OPENER;
+ explicitOpenerSet = true;
+ }
+
+ if (targetBlank && StaticPrefs::dom_targetBlankNoOpener_enabled() &&
+ token.LowerCaseEqualsLiteral("opener") && !explicitOpenerSet) {
+ explicitOpenerSet = true;
+ }
+ }
+
+ if (targetBlank && StaticPrefs::dom_targetBlankNoOpener_enabled() &&
+ !explicitOpenerSet && !triggeringPrincipalIsSystemPrincipal) {
+ flags |= INTERNAL_LOAD_FLAGS_NO_OPENER;
+ }
+
+ if (aNoOpenerImplied) {
+ flags |= INTERNAL_LOAD_FLAGS_NO_OPENER;
+ }
+ }
+
+ // Get the owner document of the link that was clicked, this will be
+ // the document that the link is in, or the last document that the
+ // link was in. From that document, we'll get the URI to use as the
+ // referrer, since the current URI in this docshell may be a
+ // new document that we're in the process of loading.
+ RefPtr<Document> referrerDoc = aContent->OwnerDoc();
+
+ // Now check that the referrerDoc's inner window is the current inner
+ // window for mScriptGlobal. If it's not, then we don't want to
+ // follow this link.
+ nsPIDOMWindowInner* referrerInner = referrerDoc->GetInnerWindow();
+ if (!mScriptGlobal || !referrerInner ||
+ mScriptGlobal->GetCurrentInnerWindow() != referrerInner) {
+ // We're no longer the current inner window
+ return NS_OK;
+ }
+
+ // referrer could be null here in some odd cases, but that's ok,
+ // we'll just load the link w/o sending a referrer in those cases.
+
+ // If this is an anchor element, grab its type property to use as a hint
+ nsAutoString typeHint;
+ RefPtr<HTMLAnchorElement> anchor = HTMLAnchorElement::FromNode(aContent);
+ if (anchor) {
+ anchor->GetType(typeHint);
+ NS_ConvertUTF16toUTF8 utf8Hint(typeHint);
+ nsAutoCString type, dummy;
+ NS_ParseRequestContentType(utf8Hint, type, dummy);
+ CopyUTF8toUTF16(type, typeHint);
+ }
+
+ uint32_t loadType = LOAD_LINK;
+ if (aLoadState->IsFormSubmission()) {
+ if (aLoadState->Target().IsEmpty()) {
+ // We set the right load type here for form submissions with an empty
+ // target. Form submission with a non-empty target are handled in
+ // nsDocShell::PerformRetargeting after we've selected the correct target
+ // BC.
+ loadType = GetLoadTypeForFormSubmission(GetBrowsingContext(), aLoadState);
+ }
+ } else {
+ // Link click can be triggered inside an onload handler, and we don't want
+ // to add history entry in this case.
+ bool inOnLoadHandler = false;
+ GetIsExecutingOnLoadHandler(&inOnLoadHandler);
+ if (inOnLoadHandler) {
+ loadType = LOAD_NORMAL_REPLACE;
+ }
+ }
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo =
+ elementCanHaveNoopener ? new ReferrerInfo(*aContent->AsElement())
+ : new ReferrerInfo(*referrerDoc);
+ RefPtr<WindowContext> context = mBrowsingContext->GetCurrentWindowContext();
+
+ aLoadState->SetTriggeringSandboxFlags(triggeringSandboxFlags);
+ aLoadState->SetTriggeringWindowId(triggeringWindowId);
+ aLoadState->SetTriggeringStorageAccess(triggeringStorageAccess);
+ aLoadState->SetReferrerInfo(referrerInfo);
+ aLoadState->SetInternalLoadFlags(flags);
+ aLoadState->SetTypeHint(NS_ConvertUTF16toUTF8(typeHint));
+ aLoadState->SetLoadType(loadType);
+ aLoadState->SetSourceBrowsingContext(mBrowsingContext);
+ aLoadState->SetHasValidUserGestureActivation(
+ context && context->HasValidTransientUserGestureActivation());
+
+ nsresult rv = InternalLoad(aLoadState);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsPingListener::DispatchPings(this, aContent, aLoadState->URI(),
+ referrerInfo);
+ }
+
+ return rv;
+}
+
+nsresult nsDocShell::OnOverLink(nsIContent* aContent, nsIURI* aURI,
+ const nsAString& aTargetSpec) {
+ if (aContent->IsEditable()) {
+ return NS_OK;
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIWebBrowserChrome> browserChrome = do_GetInterface(mTreeOwner);
+ if (!browserChrome) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIURI> exposableURI = nsIOService::CreateExposableURI(aURI);
+ nsAutoCString spec;
+ rv = exposableURI->GetDisplaySpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF8toUTF16 uStr(spec);
+
+ PredictorPredict(aURI, mCurrentURI, nsINetworkPredictor::PREDICT_LINK,
+ aContent->NodePrincipal()->OriginAttributesRef(), nullptr);
+
+ rv = browserChrome->SetLinkStatus(uStr);
+ return rv;
+}
+
+nsresult nsDocShell::OnLeaveLink() {
+ nsCOMPtr<nsIWebBrowserChrome> browserChrome(do_GetInterface(mTreeOwner));
+ nsresult rv = NS_ERROR_FAILURE;
+
+ if (browserChrome) {
+ rv = browserChrome->SetLinkStatus(u""_ns);
+ }
+ return rv;
+}
+
+bool nsDocShell::ShouldBlockLoadingForBackButton() {
+ if (!(mLoadType & LOAD_CMD_HISTORY) ||
+ UserActivation::IsHandlingUserInput() ||
+ !Preferences::GetBool("accessibility.blockjsredirection")) {
+ return false;
+ }
+
+ bool canGoForward = false;
+ GetCanGoForward(&canGoForward);
+ return canGoForward;
+}
+
+//----------------------------------------------------------------------
+// Web Shell Services API
+
+// This functions is only called when a new charset is detected in loading a
+// document.
+nsresult nsDocShell::CharsetChangeReloadDocument(
+ mozilla::NotNull<const mozilla::Encoding*> aEncoding, int32_t aSource) {
+ // XXX hack. keep the aCharset and aSource wait to pick it up
+ nsCOMPtr<nsIDocumentViewer> viewer;
+ NS_ENSURE_SUCCESS(GetDocViewer(getter_AddRefs(viewer)), NS_ERROR_FAILURE);
+ if (viewer) {
+ int32_t source;
+ Unused << viewer->GetReloadEncodingAndSource(&source);
+ if (aSource > source) {
+ viewer->SetReloadEncodingAndSource(aEncoding, aSource);
+ if (eCharsetReloadRequested != mCharsetReloadState) {
+ mCharsetReloadState = eCharsetReloadRequested;
+ switch (mLoadType) {
+ case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE:
+ return Reload(LOAD_FLAGS_CHARSET_CHANGE | LOAD_FLAGS_BYPASS_CACHE |
+ LOAD_FLAGS_BYPASS_PROXY);
+ case LOAD_RELOAD_BYPASS_CACHE:
+ return Reload(LOAD_FLAGS_CHARSET_CHANGE | LOAD_FLAGS_BYPASS_CACHE);
+ default:
+ return Reload(LOAD_FLAGS_CHARSET_CHANGE);
+ }
+ }
+ }
+ }
+ // return failure if this request is not accepted due to mCharsetReloadState
+ return NS_ERROR_DOCSHELL_REQUEST_REJECTED;
+}
+
+nsresult nsDocShell::CharsetChangeStopDocumentLoad() {
+ if (eCharsetReloadRequested != mCharsetReloadState) {
+ Stop(nsIWebNavigation::STOP_ALL);
+ return NS_OK;
+ }
+ // return failer if this request is not accepted due to mCharsetReloadState
+ return NS_ERROR_DOCSHELL_REQUEST_REJECTED;
+}
+
+NS_IMETHODIMP nsDocShell::ExitPrintPreview() {
+#if NS_PRINT_PREVIEW
+ nsCOMPtr<nsIWebBrowserPrint> viewer = do_QueryInterface(mDocumentViewer);
+ return viewer->ExitPrintPreview();
+#else
+ return NS_OK;
+#endif
+}
+
+/* [infallible] */
+NS_IMETHODIMP nsDocShell::GetIsTopLevelContentDocShell(
+ bool* aIsTopLevelContentDocShell) {
+ *aIsTopLevelContentDocShell = false;
+
+ if (mItemType == typeContent) {
+ *aIsTopLevelContentDocShell = mBrowsingContext->IsTopContent();
+ }
+
+ return NS_OK;
+}
+
+// Implements nsILoadContext.originAttributes
+NS_IMETHODIMP
+nsDocShell::GetScriptableOriginAttributes(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aVal) {
+ return mBrowsingContext->GetScriptableOriginAttributes(aCx, aVal);
+}
+
+// Implements nsIDocShell.GetOriginAttributes()
+NS_IMETHODIMP
+nsDocShell::GetOriginAttributes(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aVal) {
+ return mBrowsingContext->GetScriptableOriginAttributes(aCx, aVal);
+}
+
+bool nsDocShell::ServiceWorkerAllowedToControlWindow(nsIPrincipal* aPrincipal,
+ nsIURI* aURI) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aURI);
+
+ if (UsePrivateBrowsing() || mBrowsingContext->GetSandboxFlags()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> parent;
+ GetInProcessSameTypeParent(getter_AddRefs(parent));
+ nsPIDOMWindowOuter* parentOuter = parent ? parent->GetWindow() : nullptr;
+ nsPIDOMWindowInner* parentInner =
+ parentOuter ? parentOuter->GetCurrentInnerWindow() : nullptr;
+
+ StorageAccess storage =
+ StorageAllowedForNewWindow(aPrincipal, aURI, parentInner);
+
+ // If the partitioned service worker is enabled, service worker is allowed to
+ // control the window if partition is enabled.
+ if (StaticPrefs::privacy_partition_serviceWorkers() && parentInner) {
+ RefPtr<Document> doc = parentInner->GetExtantDoc();
+
+ if (doc && StoragePartitioningEnabled(storage, doc->CookieJarSettings())) {
+ return true;
+ }
+ }
+
+ return storage == StorageAccess::eAllow;
+}
+
+nsresult nsDocShell::SetOriginAttributes(const OriginAttributes& aAttrs) {
+ MOZ_ASSERT(!mIsBeingDestroyed);
+ return mBrowsingContext->SetOriginAttributes(aAttrs);
+}
+
+NS_IMETHODIMP
+nsDocShell::ResumeRedirectedLoad(uint64_t aIdentifier, int32_t aHistoryIndex) {
+ RefPtr<nsDocShell> self = this;
+ RefPtr<ChildProcessChannelListener> cpcl =
+ ChildProcessChannelListener::GetSingleton();
+
+ // Call into InternalLoad with the pending channel when it is received.
+ cpcl->RegisterCallback(
+ aIdentifier, [self, aHistoryIndex](
+ nsDocShellLoadState* aLoadState,
+ nsTArray<Endpoint<extensions::PStreamFilterParent>>&&
+ aStreamFilterEndpoints,
+ nsDOMNavigationTiming* aTiming) {
+ MOZ_ASSERT(aLoadState->GetPendingRedirectedChannel());
+ if (NS_WARN_IF(self->mIsBeingDestroyed)) {
+ aLoadState->GetPendingRedirectedChannel()->CancelWithReason(
+ NS_BINDING_ABORTED, "nsDocShell::mIsBeingDestroyed"_ns);
+ return NS_BINDING_ABORTED;
+ }
+
+ self->mLoadType = aLoadState->LoadType();
+ nsCOMPtr<nsIURI> previousURI;
+ uint32_t previousFlags = 0;
+ ExtractLastVisit(aLoadState->GetPendingRedirectedChannel(),
+ getter_AddRefs(previousURI), &previousFlags);
+ self->SaveLastVisit(aLoadState->GetPendingRedirectedChannel(),
+ previousURI, previousFlags);
+
+ if (aTiming) {
+ self->mTiming = new nsDOMNavigationTiming(self, aTiming);
+ self->mBlankTiming = false;
+ }
+
+ // If we're performing a history load, locate the correct history entry,
+ // and set the relevant bits on our loadState.
+ if (aHistoryIndex >= 0 && self->GetSessionHistory() &&
+ !mozilla::SessionHistoryInParent()) {
+ nsCOMPtr<nsISHistory> legacySHistory =
+ self->GetSessionHistory()->LegacySHistory();
+
+ nsCOMPtr<nsISHEntry> entry;
+ nsresult rv = legacySHistory->GetEntryAtIndex(aHistoryIndex,
+ getter_AddRefs(entry));
+ if (NS_SUCCEEDED(rv)) {
+ legacySHistory->InternalSetRequestedIndex(aHistoryIndex);
+ aLoadState->SetLoadType(LOAD_HISTORY);
+ aLoadState->SetSHEntry(entry);
+ }
+ }
+
+ self->InternalLoad(aLoadState);
+
+ if (aLoadState->GetOriginalURIString().isSome()) {
+ // Save URI string in case it's needed later when
+ // sending to search engine service in EndPageLoad()
+ self->mOriginalUriString = *aLoadState->GetOriginalURIString();
+ }
+
+ for (auto& endpoint : aStreamFilterEndpoints) {
+ extensions::StreamFilterParent::Attach(
+ aLoadState->GetPendingRedirectedChannel(), std::move(endpoint));
+ }
+
+ // If the channel isn't pending, then it means that InternalLoad
+ // never connected it, and we shouldn't try to continue. This
+ // can happen even if InternalLoad returned NS_OK.
+ bool pending = false;
+ aLoadState->GetPendingRedirectedChannel()->IsPending(&pending);
+ NS_ASSERTION(pending, "We should have connected the pending channel!");
+ if (!pending) {
+ return NS_BINDING_ABORTED;
+ }
+ return NS_OK;
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetOriginAttributes(JS::Handle<JS::Value> aOriginAttributes,
+ JSContext* aCx) {
+ OriginAttributes attrs;
+ if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return SetOriginAttributes(attrs);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetAsyncPanZoomEnabled(bool* aOut) {
+ if (PresShell* presShell = GetPresShell()) {
+ *aOut = presShell->AsyncPanZoomEnabled();
+ return NS_OK;
+ }
+
+ // If we don't have a presShell, fall back to the default platform value of
+ // whether or not APZ is enabled.
+ *aOut = gfxPlatform::AsyncPanZoomEnabled();
+ return NS_OK;
+}
+
+bool nsDocShell::HasUnloadedParent() {
+ for (WindowContext* wc = GetBrowsingContext()->GetParentWindowContext(); wc;
+ wc = wc->GetParentWindowContext()) {
+ if (!wc->IsCurrent() || wc->IsDiscarded() ||
+ wc->GetBrowsingContext()->IsDiscarded()) {
+ // If a parent is OOP and the parent WindowContext is no
+ // longer current, we can assume the parent was unloaded.
+ return true;
+ }
+
+ if (wc->GetBrowsingContext()->IsInProcess() &&
+ (!wc->GetBrowsingContext()->GetDocShell() ||
+ wc->GetBrowsingContext()->GetDocShell()->GetIsInUnload())) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* static */
+bool nsDocShell::ShouldUpdateGlobalHistory(uint32_t aLoadType) {
+ return !(aLoadType == LOAD_BYPASS_HISTORY || aLoadType == LOAD_ERROR_PAGE ||
+ aLoadType & LOAD_CMD_HISTORY);
+}
+
+void nsDocShell::UpdateGlobalHistoryTitle(nsIURI* aURI) {
+ if (!mBrowsingContext->GetUseGlobalHistory() || UsePrivateBrowsing()) {
+ return;
+ }
+
+ // Global history is interested into sub-frame visits only for link-coloring
+ // purposes, thus title updates are skipped for those.
+ //
+ // Moreover, some iframe documents (such as the ones created via
+ // document.open()) inherit the document uri of the caller, which would cause
+ // us to override a previously set page title with one from the subframe.
+ if (IsSubframe()) {
+ return;
+ }
+
+ if (nsCOMPtr<IHistory> history = components::History::Service()) {
+ history->SetURITitle(aURI, mTitle);
+ }
+}
+
+bool nsDocShell::IsInvisible() { return mInvisible; }
+
+void nsDocShell::SetInvisible(bool aInvisible) { mInvisible = aInvisible; }
+
+/* static */
+void nsDocShell::MaybeNotifyKeywordSearchLoading(const nsString& aProvider,
+ const nsString& aKeyword) {
+ if (aProvider.IsEmpty()) {
+ return;
+ }
+ nsresult rv;
+ nsCOMPtr<nsISupportsString> isupportsString =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = isupportsString->SetData(aProvider);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
+ if (obsSvc) {
+ // Note that "keyword-search" refers to a search via the url
+ // bar, not a bookmarks keyword search.
+ obsSvc->NotifyObservers(isupportsString, "keyword-search", aKeyword.get());
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::ShouldPrepareForIntercept(nsIURI* aURI, nsIChannel* aChannel,
+ bool* aShouldIntercept) {
+ return mInterceptController->ShouldPrepareForIntercept(aURI, aChannel,
+ aShouldIntercept);
+}
+
+NS_IMETHODIMP
+nsDocShell::ChannelIntercepted(nsIInterceptedChannel* aChannel) {
+ return mInterceptController->ChannelIntercepted(aChannel);
+}
+
+bool nsDocShell::InFrameSwap() {
+ RefPtr<nsDocShell> shell = this;
+ do {
+ if (shell->mInFrameSwap) {
+ return true;
+ }
+ shell = shell->GetInProcessParentDocshell();
+ } while (shell);
+ return false;
+}
+
+UniquePtr<ClientSource> nsDocShell::TakeInitialClientSource() {
+ return std::move(mInitialClientSource);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetEditingSession(nsIEditingSession** aEditSession) {
+ if (!NS_SUCCEEDED(EnsureEditorData())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aEditSession = do_AddRef(mEditorData->GetEditingSession()).take();
+ return *aEditSession ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetScriptableBrowserChild(nsIBrowserChild** aBrowserChild) {
+ *aBrowserChild = GetBrowserChild().take();
+ return *aBrowserChild ? NS_OK : NS_ERROR_FAILURE;
+}
+
+already_AddRefed<nsIBrowserChild> nsDocShell::GetBrowserChild() {
+ nsCOMPtr<nsIBrowserChild> tc = do_QueryReferent(mBrowserChild);
+ return tc.forget();
+}
+
+nsCommandManager* nsDocShell::GetCommandManager() {
+ NS_ENSURE_SUCCESS(EnsureCommandHandler(), nullptr);
+ return mCommandManager;
+}
+
+NS_IMETHODIMP_(void)
+nsDocShell::GetOriginAttributes(mozilla::OriginAttributes& aAttrs) {
+ mBrowsingContext->GetOriginAttributes(aAttrs);
+}
+
+HTMLEditor* nsIDocShell::GetHTMLEditor() {
+ nsDocShell* docShell = static_cast<nsDocShell*>(this);
+ return docShell->GetHTMLEditorInternal();
+}
+
+nsresult nsIDocShell::SetHTMLEditor(HTMLEditor* aHTMLEditor) {
+ nsDocShell* docShell = static_cast<nsDocShell*>(this);
+ return docShell->SetHTMLEditorInternal(aHTMLEditor);
+}
+
+#define MATRIX_LENGTH 20
+
+NS_IMETHODIMP
+nsDocShell::SetColorMatrix(const nsTArray<float>& aMatrix) {
+ if (aMatrix.Length() == MATRIX_LENGTH) {
+ mColorMatrix.reset(new gfx::Matrix5x4());
+ static_assert(
+ MATRIX_LENGTH * sizeof(float) == sizeof(mColorMatrix->components),
+ "Size mismatch for our memcpy");
+ memcpy(mColorMatrix->components, aMatrix.Elements(),
+ sizeof(mColorMatrix->components));
+ } else if (aMatrix.Length() == 0) {
+ mColorMatrix.reset();
+ } else {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ PresShell* presShell = GetPresShell();
+ if (!presShell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIFrame* frame = presShell->GetRootFrame();
+ if (!frame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ frame->SchedulePaint();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetColorMatrix(nsTArray<float>& aMatrix) {
+ if (mColorMatrix) {
+ aMatrix.SetLength(MATRIX_LENGTH);
+ static_assert(
+ MATRIX_LENGTH * sizeof(float) == sizeof(mColorMatrix->components),
+ "Size mismatch for our memcpy");
+ memcpy(aMatrix.Elements(), mColorMatrix->components,
+ MATRIX_LENGTH * sizeof(float));
+ }
+
+ return NS_OK;
+}
+
+#undef MATRIX_LENGTH
+
+NS_IMETHODIMP
+nsDocShell::GetIsForceReloading(bool* aForceReload) {
+ *aForceReload = IsForceReloading();
+ return NS_OK;
+}
+
+bool nsDocShell::IsForceReloading() { return IsForceReloadType(mLoadType); }
+
+NS_IMETHODIMP
+nsDocShell::GetBrowsingContextXPCOM(BrowsingContext** aBrowsingContext) {
+ *aBrowsingContext = do_AddRef(mBrowsingContext).take();
+ return NS_OK;
+}
+
+BrowsingContext* nsDocShell::GetBrowsingContext() { return mBrowsingContext; }
+
+bool nsDocShell::GetIsAttemptingToNavigate() {
+ // XXXbz the document.open spec says to abort even if there's just a
+ // queued navigation task, sort of. It's not clear whether browsers
+ // actually do that, and we didn't use to do it, so for now let's
+ // not do that.
+ // https://github.com/whatwg/html/issues/3447 tracks the spec side of this.
+ if (mDocumentRequest) {
+ // There's definitely a navigation in progress.
+ return true;
+ }
+
+ // javascript: channels have slightly weird behavior: they're LOAD_BACKGROUND
+ // until the script runs, which means they're not sending loadgroup
+ // notifications and hence not getting set as mDocumentRequest. Look through
+ // our loadgroup for document-level javascript: loads.
+ if (!mLoadGroup) {
+ return false;
+ }
+
+ nsCOMPtr<nsISimpleEnumerator> requests;
+ mLoadGroup->GetRequests(getter_AddRefs(requests));
+ bool hasMore = false;
+ while (NS_SUCCEEDED(requests->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> elem;
+ requests->GetNext(getter_AddRefs(elem));
+ nsCOMPtr<nsIScriptChannel> scriptChannel(do_QueryInterface(elem));
+ if (!scriptChannel) {
+ continue;
+ }
+
+ if (scriptChannel->GetIsDocumentLoad()) {
+ // This is a javascript: load that might lead to a new document,
+ // hence a navigation.
+ return true;
+ }
+ }
+
+ return mCheckingSessionHistory;
+}
+
+void nsDocShell::SetLoadingSessionHistoryInfo(
+ const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo,
+ bool aNeedToReportActiveAfterLoadingBecomesActive) {
+ // FIXME Would like to assert this, but can't yet.
+ // MOZ_ASSERT(!mLoadingEntry);
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Setting the loading entry on nsDocShell %p to %s", this,
+ aLoadingInfo.mInfo.GetURI()->GetSpecOrDefault().get()));
+ mLoadingEntry = MakeUnique<LoadingSessionHistoryInfo>(aLoadingInfo);
+ mNeedToReportActiveAfterLoadingBecomesActive =
+ aNeedToReportActiveAfterLoadingBecomesActive;
+}
+
+void nsDocShell::MoveLoadingToActiveEntry(bool aPersist, bool aExpired,
+ uint32_t aCacheKey,
+ nsIURI* aPreviousURI) {
+ MOZ_ASSERT(mozilla::SessionHistoryInParent());
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("nsDocShell %p MoveLoadingToActiveEntry", this));
+
+ UniquePtr<SessionHistoryInfo> previousActiveEntry(mActiveEntry.release());
+ mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> loadingEntry;
+ mActiveEntryIsLoadingFromSessionHistory =
+ mLoadingEntry && mLoadingEntry->mLoadIsFromSessionHistory;
+ if (mLoadingEntry) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("Moving the loading entry to the active entry on nsDocShell %p "
+ "to %s",
+ this, mLoadingEntry->mInfo.GetURI()->GetSpecOrDefault().get()));
+ mActiveEntry = MakeUnique<SessionHistoryInfo>(mLoadingEntry->mInfo);
+ mLoadingEntry.swap(loadingEntry);
+ if (!mActiveEntryIsLoadingFromSessionHistory) {
+ if (mNeedToReportActiveAfterLoadingBecomesActive) {
+ // Needed to pass various history length WPTs.
+ mBrowsingContext->SetActiveSessionHistoryEntry(
+ mozilla::Nothing(), mActiveEntry.get(), mLoadType,
+ /* aUpdatedCacheKey = */ 0, false);
+ }
+ mBrowsingContext->IncrementHistoryEntryCountForBrowsingContext();
+ }
+ }
+ mNeedToReportActiveAfterLoadingBecomesActive = false;
+
+ if (mActiveEntry) {
+ if (aCacheKey != 0) {
+ mActiveEntry->SetCacheKey(aCacheKey);
+ }
+ MOZ_ASSERT(loadingEntry);
+ uint32_t loadType =
+ mLoadType == LOAD_ERROR_PAGE ? mFailedLoadType : mLoadType;
+
+ if (loadingEntry->mLoadId != UINT64_MAX) {
+ // We're passing in mCurrentURI, which could be null. SessionHistoryCommit
+ // does require a non-null uri if this is for a refresh load of the same
+ // URI, but in that case mCurrentURI won't be null here.
+ mBrowsingContext->SessionHistoryCommit(
+ *loadingEntry, loadType, aPreviousURI, previousActiveEntry.get(),
+ aPersist, false, aExpired, aCacheKey);
+ }
+ }
+}
+
+static bool IsFaviconLoad(nsIRequest* aRequest) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (!channel) {
+ return false;
+ }
+
+ nsCOMPtr<nsILoadInfo> li = channel->LoadInfo();
+ return li && li->InternalContentPolicyType() ==
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON;
+}
+
+void nsDocShell::RecordSingleChannelId(bool aStartRequest,
+ nsIRequest* aRequest) {
+ // Ignore favicon loads, they don't need to block caching.
+ if (IsFaviconLoad(aRequest)) {
+ return;
+ }
+
+ MOZ_ASSERT_IF(!aStartRequest, mRequestForBlockingFromBFCacheCount > 0);
+
+ mRequestForBlockingFromBFCacheCount += aStartRequest ? 1 : -1;
+
+ if (mBrowsingContext->GetCurrentWindowContext()) {
+ // We have three states: no request, one request with an id and
+ // eiher one request without an id or multiple requests. Nothing() is no
+ // request, Some(non-zero) is one request with an id and Some(0) is one
+ // request without an id or multiple requests.
+ Maybe<uint64_t> singleChannelId;
+ if (mRequestForBlockingFromBFCacheCount > 1) {
+ singleChannelId = Some(0);
+ } else if (mRequestForBlockingFromBFCacheCount == 1) {
+ nsCOMPtr<nsIIdentChannel> identChannel;
+ if (aStartRequest) {
+ identChannel = do_QueryInterface(aRequest);
+ } else {
+ // aChannel is the channel that's being removed, but we need to check if
+ // the remaining channel in the loadgroup has an id.
+ nsCOMPtr<nsISimpleEnumerator> requests;
+ mLoadGroup->GetRequests(getter_AddRefs(requests));
+ for (const auto& request : SimpleEnumerator<nsIRequest>(requests)) {
+ if (!IsFaviconLoad(request) &&
+ !!(identChannel = do_QueryInterface(request))) {
+ break;
+ }
+ }
+ }
+
+ if (identChannel) {
+ singleChannelId = Some(identChannel->ChannelId());
+ } else {
+ singleChannelId = Some(0);
+ }
+ } else {
+ MOZ_ASSERT(mRequestForBlockingFromBFCacheCount == 0);
+ singleChannelId = Nothing();
+ }
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Verbose))) {
+ nsAutoCString uri("[no uri]");
+ if (mCurrentURI) {
+ uri = mCurrentURI->GetSpecOrDefault();
+ }
+ if (singleChannelId.isNothing()) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose,
+ ("Loadgroup for %s doesn't have any requests relevant for "
+ "blocking BFCache",
+ uri.get()));
+ } else if (singleChannelId.value() == 0) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose,
+ ("Loadgroup for %s has multiple requests relevant for blocking "
+ "BFCache",
+ uri.get()));
+ } else {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose,
+ ("Loadgroup for %s has one request with id %" PRIu64
+ " relevant for blocking BFCache",
+ uri.get(), singleChannelId.value()));
+ }
+ }
+
+ if (mSingleChannelId != singleChannelId) {
+ mSingleChannelId = singleChannelId;
+ WindowGlobalChild* wgc =
+ mBrowsingContext->GetCurrentWindowContext()->GetWindowGlobalChild();
+ if (wgc) {
+ wgc->SendSetSingleChannelId(singleChannelId);
+ }
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsDocShell::OnStartRequest(nsIRequest* aRequest) {
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Verbose))) {
+ nsAutoCString uri("[no uri]");
+ if (mCurrentURI) {
+ uri = mCurrentURI->GetSpecOrDefault();
+ }
+ nsAutoCString name;
+ aRequest->GetName(name);
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose,
+ ("Adding request %s to loadgroup for %s", name.get(), uri.get()));
+ }
+ RecordSingleChannelId(true, aRequest);
+ return nsDocLoader::OnStartRequest(aRequest);
+}
+
+NS_IMETHODIMP
+nsDocShell::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Verbose))) {
+ nsAutoCString uri("[no uri]");
+ if (mCurrentURI) {
+ uri = mCurrentURI->GetSpecOrDefault();
+ }
+ nsAutoCString name;
+ aRequest->GetName(name);
+ MOZ_LOG(
+ gSHIPBFCacheLog, LogLevel::Verbose,
+ ("Removing request %s from loadgroup for %s", name.get(), uri.get()));
+ }
+ RecordSingleChannelId(false, aRequest);
+ return nsDocLoader::OnStopRequest(aRequest, aStatusCode);
+}
+
+void nsDocShell::MaybeDisconnectChildListenersOnPageHide() {
+ MOZ_RELEASE_ASSERT(XRE_IsContentProcess());
+
+ if (mChannelToDisconnectOnPageHide != 0 && mLoadGroup) {
+ nsCOMPtr<nsISimpleEnumerator> requests;
+ mLoadGroup->GetRequests(getter_AddRefs(requests));
+ for (const auto& request : SimpleEnumerator<nsIRequest>(requests)) {
+ RefPtr<DocumentChannel> channel = do_QueryObject(request);
+ if (channel && channel->ChannelId() == mChannelToDisconnectOnPageHide) {
+ static_cast<DocumentChannelChild*>(channel.get())
+ ->DisconnectChildListeners(NS_BINDING_ABORTED, NS_BINDING_ABORTED);
+ }
+ }
+ mChannelToDisconnectOnPageHide = 0;
+ }
+}
diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h
new file mode 100644
index 0000000000..9f2d9a17dc
--- /dev/null
+++ b/docshell/base/nsDocShell.h
@@ -0,0 +1,1366 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShell_h__
+#define nsDocShell_h__
+
+#include "Units.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/ScrollbarPreferences.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/WindowProxyHolder.h"
+#include "nsCOMPtr.h"
+#include "nsCharsetSource.h"
+#include "nsDocLoader.h"
+#include "nsIAuthPromptProvider.h"
+#include "nsIBaseWindow.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsILoadContext.h"
+#include "nsINetworkInterceptController.h"
+#include "nsIRefreshURI.h"
+#include "nsIWebNavigation.h"
+#include "nsIWebPageDescriptor.h"
+#include "nsIWebProgressListener.h"
+#include "nsPoint.h" // mCurrent/mDefaultScrollbarPreferences
+#include "nsRect.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "prtime.h"
+
+// Interfaces Needed
+
+namespace mozilla {
+class Encoding;
+class HTMLEditor;
+class ObservedDocShell;
+enum class TaskCategory;
+namespace dom {
+class ClientInfo;
+class ClientSource;
+class EventTarget;
+class SessionHistoryInfo;
+struct LoadingSessionHistoryInfo;
+struct Wireframe;
+} // namespace dom
+namespace net {
+class LoadInfo;
+class DocumentLoadListener;
+} // namespace net
+} // namespace mozilla
+
+class nsIController;
+class nsIDocShellTreeOwner;
+class nsIDocumentViewer;
+class nsIHttpChannel;
+class nsIMutableArray;
+class nsIPrompt;
+class nsIScrollableFrame;
+class nsIStringBundle;
+class nsIURIFixup;
+class nsIURIFixupInfo;
+class nsIURILoader;
+class nsIWebBrowserFind;
+class nsIWidget;
+class nsIReferrerInfo;
+
+class nsCommandManager;
+class nsDocShellEditorData;
+class nsDOMNavigationTiming;
+class nsDSURIContentListener;
+class nsGlobalWindowOuter;
+
+class FramingChecker;
+class OnLinkClickEvent;
+
+/* internally used ViewMode types */
+enum ViewMode { viewNormal = 0x0, viewSource = 0x1 };
+
+enum eCharsetReloadState {
+ eCharsetReloadInit,
+ eCharsetReloadRequested,
+ eCharsetReloadStopOrigional
+};
+
+class nsDocShell final : public nsDocLoader,
+ public nsIDocShell,
+ public nsIWebNavigation,
+ public nsIBaseWindow,
+ public nsIRefreshURI,
+ public nsIWebProgressListener,
+ public nsIWebPageDescriptor,
+ public nsIAuthPromptProvider,
+ public nsILoadContext,
+ public nsINetworkInterceptController,
+ public mozilla::SupportsWeakPtr {
+ public:
+ enum InternalLoad : uint32_t {
+ INTERNAL_LOAD_FLAGS_NONE = 0x0,
+ INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL = 0x1,
+ INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER = 0x2,
+ INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP = 0x4,
+
+ // This flag marks the first load in this object
+ // @see nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD
+ INTERNAL_LOAD_FLAGS_FIRST_LOAD = 0x8,
+
+ // The set of flags that should not be set before calling into
+ // nsDocShell::LoadURI and other nsDocShell loading functions.
+ INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS = 0xf,
+
+ INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER = 0x10,
+ INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES = 0x20,
+
+ // Whether the load should be treated as srcdoc load, rather than a URI one.
+ INTERNAL_LOAD_FLAGS_IS_SRCDOC = 0x40,
+
+ // Whether this is the load of a frame's original src attribute
+ INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC = 0x80,
+
+ INTERNAL_LOAD_FLAGS_NO_OPENER = 0x100,
+
+ // Whether a top-level data URI navigation is allowed for that load
+ INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI = 0x200,
+
+ // Whether the load should go through LoadURIDelegate.
+ INTERNAL_LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 0x2000,
+ };
+
+ // Event type dispatched by RestorePresentation
+ class RestorePresentationEvent : public mozilla::Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+ explicit RestorePresentationEvent(nsDocShell* aDs)
+ : mozilla::Runnable("nsDocShell::RestorePresentationEvent"),
+ mDocShell(aDs) {}
+ void Revoke() { mDocShell = nullptr; }
+
+ private:
+ RefPtr<nsDocShell> mDocShell;
+ };
+
+ class InterfaceRequestorProxy : public nsIInterfaceRequestor {
+ public:
+ explicit InterfaceRequestorProxy(nsIInterfaceRequestor* aRequestor);
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ private:
+ virtual ~InterfaceRequestorProxy();
+ InterfaceRequestorProxy() = default;
+ nsWeakPtr mWeakPtr;
+ };
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsDocShell, nsDocLoader)
+ NS_DECL_NSIDOCSHELL
+ NS_DECL_NSIDOCSHELLTREEITEM
+ NS_DECL_NSIWEBNAVIGATION
+ NS_DECL_NSIBASEWINDOW
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIREFRESHURI
+ NS_DECL_NSIWEBPAGEDESCRIPTOR
+ NS_DECL_NSIAUTHPROMPTPROVIDER
+ NS_DECL_NSINETWORKINTERCEPTCONTROLLER
+
+ // Create a new nsDocShell object.
+ static already_AddRefed<nsDocShell> Create(
+ mozilla::dom::BrowsingContext* aBrowsingContext,
+ uint64_t aContentWindowID = 0);
+
+ bool Initialize();
+
+ NS_IMETHOD Stop() override {
+ // Need this here because otherwise nsIWebNavigation::Stop
+ // overrides the docloader's Stop()
+ return nsDocLoader::Stop();
+ }
+
+ mozilla::ScrollbarPreference ScrollbarPreference() const {
+ return mScrollbarPref;
+ }
+ void SetScrollbarPreference(mozilla::ScrollbarPreference);
+
+ /*
+ * The size, in CSS pixels, of the margins for the <body> of an HTML document
+ * in this docshell; used to implement the marginwidth attribute on HTML
+ * <frame>/<iframe> elements. A value smaller than zero indicates that the
+ * attribute was not set.
+ */
+ const mozilla::CSSIntSize& GetFrameMargins() const { return mFrameMargins; }
+
+ bool UpdateFrameMargins(const mozilla::CSSIntSize& aMargins) {
+ if (mFrameMargins == aMargins) {
+ return false;
+ }
+ mFrameMargins = aMargins;
+ return true;
+ }
+
+ /**
+ * Process a click on a link.
+ *
+ * @param aContent the content object used for triggering the link.
+ * @param aURI a URI object that defines the destination for the link
+ * @param aTargetSpec indicates where the link is targeted (may be an empty
+ * string)
+ * @param aFileName non-null when the link should be downloaded as the given
+ * file
+ * @param aPostDataStream the POST data to send
+ * @param aHeadersDataStream ??? (only used for plugins)
+ * @param aIsTrusted false if the triggerer is an untrusted DOM event.
+ * @param aTriggeringPrincipal, if not passed explicitly we fall back to
+ * the document's principal.
+ * @param aCsp, the CSP to be used for the load, that is the CSP of the
+ * entity responsible for causing the load to occur. Most likely
+ * this is the CSP of the document that started the load. In case
+ * aCsp was not passed explicitly we fall back to using
+ * aContent's document's CSP if that document holds any.
+ */
+ nsresult OnLinkClick(nsIContent* aContent, nsIURI* aURI,
+ const nsAString& aTargetSpec, const nsAString& aFileName,
+ nsIInputStream* aPostDataStream,
+ nsIInputStream* aHeadersDataStream,
+ bool aIsUserTriggered, bool aIsTrusted,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIContentSecurityPolicy* aCsp);
+ /**
+ * Process a click on a link.
+ *
+ * Works the same as OnLinkClick() except it happens immediately rather than
+ * through an event.
+ *
+ * @param aContent the content object used for triggering the link.
+ * @param aDocShellLoadState the extended load info for this load.
+ * @param aNoOpenerImplied if the link implies "noopener"
+ * @param aTriggeringPrincipal, if not passed explicitly we fall back to
+ * the document's principal.
+ */
+ nsresult OnLinkClickSync(nsIContent* aContent,
+ nsDocShellLoadState* aLoadState,
+ bool aNoOpenerImplied,
+ nsIPrincipal* aTriggeringPrincipal);
+
+ /**
+ * Process a mouse-over a link.
+ *
+ * @param aContent the linked content.
+ * @param aURI an URI object that defines the destination for the link
+ * @param aTargetSpec indicates where the link is targeted (it may be an empty
+ * string)
+ */
+ nsresult OnOverLink(nsIContent* aContent, nsIURI* aURI,
+ const nsAString& aTargetSpec);
+ /**
+ * Process the mouse leaving a link.
+ */
+ nsresult OnLeaveLink();
+
+ // Don't use NS_DECL_NSILOADCONTEXT because some of nsILoadContext's methods
+ // are shared with nsIDocShell and can't be declared twice.
+ NS_IMETHOD GetAssociatedWindow(mozIDOMWindowProxy**) override;
+ NS_IMETHOD GetTopWindow(mozIDOMWindowProxy**) override;
+ NS_IMETHOD GetTopFrameElement(mozilla::dom::Element**) override;
+ NS_IMETHOD GetIsContent(bool*) override;
+ NS_IMETHOD GetUsePrivateBrowsing(bool*) override;
+ NS_IMETHOD SetUsePrivateBrowsing(bool) override;
+ NS_IMETHOD SetPrivateBrowsing(bool) override;
+ NS_IMETHOD GetUseRemoteTabs(bool*) override;
+ NS_IMETHOD SetRemoteTabs(bool) override;
+ NS_IMETHOD GetUseRemoteSubframes(bool*) override;
+ NS_IMETHOD SetRemoteSubframes(bool) override;
+ NS_IMETHOD GetScriptableOriginAttributes(
+ JSContext*, JS::MutableHandle<JS::Value>) override;
+ NS_IMETHOD_(void)
+ GetOriginAttributes(mozilla::OriginAttributes& aAttrs) override;
+
+ // Restores a cached presentation from history (mLSHE).
+ // This method swaps out the content viewer and simulates loads for
+ // subframes. It then simulates the completion of the toplevel load.
+ nsresult RestoreFromHistory();
+
+ /**
+ * Parses the passed in header string and sets up a refreshURI if a "refresh"
+ * header is found. If docshell is busy loading a page currently, the request
+ * will be queued and executed when the current page finishes loading.
+ *
+ * @param aDocument document to which the refresh header applies.
+ * @param aHeader The meta refresh header string.
+ */
+ void SetupRefreshURIFromHeader(mozilla::dom::Document* aDocument,
+ const nsAString& aHeader);
+
+ // Perform a URI load from a refresh timer. This is just like the
+ // ForceRefreshURI method on nsIRefreshURI, but makes sure to take
+ // the timer involved out of mRefreshURIList if it's there.
+ // aTimer must not be null.
+ nsresult ForceRefreshURIFromTimer(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ uint32_t aDelay, nsITimer* aTimer);
+
+ // We need dummy OnLocationChange in some cases to update the UI without
+ // updating security info.
+ void FireDummyOnLocationChange() {
+ FireOnLocationChange(this, nullptr, mCurrentURI,
+ LOCATION_CHANGE_SAME_DOCUMENT);
+ }
+
+ nsresult HistoryEntryRemoved(int32_t aIndex);
+
+ // Notify Scroll observers when an async panning/zooming transform
+ // has started being applied
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void NotifyAsyncPanZoomStarted();
+
+ // Notify Scroll observers when an async panning/zooming transform
+ // is no longer applied
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void NotifyAsyncPanZoomStopped();
+
+ void SetInFrameSwap(bool aInSwap) { mInFrameSwap = aInSwap; }
+ bool InFrameSwap();
+
+ bool GetForcedAutodetection() { return mForcedAutodetection; }
+
+ void ResetForcedAutodetection() { mForcedAutodetection = false; }
+
+ mozilla::HTMLEditor* GetHTMLEditorInternal();
+ nsresult SetHTMLEditorInternal(mozilla::HTMLEditor* aHTMLEditor);
+
+ // Handle page navigation due to charset changes
+ nsresult CharsetChangeReloadDocument(
+ mozilla::NotNull<const mozilla::Encoding*> aEncoding, int32_t aSource);
+ nsresult CharsetChangeStopDocumentLoad();
+
+ nsDOMNavigationTiming* GetNavigationTiming() const;
+
+ nsresult SetOriginAttributes(const mozilla::OriginAttributes& aAttrs);
+
+ const mozilla::OriginAttributes& GetOriginAttributes() {
+ return mBrowsingContext->OriginAttributesRef();
+ }
+
+ // Determine whether this docshell corresponds to the given history entry,
+ // via having a pointer to it in mOSHE or mLSHE.
+ bool HasHistoryEntry(nsISHEntry* aEntry) const {
+ return aEntry && (aEntry == mOSHE || aEntry == mLSHE);
+ }
+
+ // Update any pointers (mOSHE or mLSHE) to aOldEntry to point to aNewEntry
+ void SwapHistoryEntries(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry);
+
+ bool GetCreatedDynamically() const {
+ return mBrowsingContext && mBrowsingContext->CreatedDynamically();
+ }
+
+ mozilla::gfx::Matrix5x4* GetColorMatrix() { return mColorMatrix.get(); }
+
+ static bool SandboxFlagsImplyCookies(const uint32_t& aSandboxFlags);
+
+ // Tell the favicon service that aNewURI has the same favicon as aOldURI.
+ static void CopyFavicon(nsIURI* aOldURI, nsIURI* aNewURI,
+ bool aInPrivateBrowsing);
+
+ static nsDocShell* Cast(nsIDocShell* aDocShell) {
+ return static_cast<nsDocShell*>(aDocShell);
+ }
+
+ static bool CanLoadInParentProcess(nsIURI* aURI);
+
+ // Returns true if the current load is a force reload (started by holding
+ // shift while triggering reload)
+ bool IsForceReloading();
+
+ mozilla::dom::WindowProxyHolder GetWindowProxy() {
+ EnsureScriptEnvironment();
+ return mozilla::dom::WindowProxyHolder(mBrowsingContext);
+ }
+
+ /**
+ * Loads the given URI. See comments on nsDocShellLoadState members for more
+ * information on information used.
+ * `aCacheKey` gets passed to DoURILoad call.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult InternalLoad(
+ nsDocShellLoadState* aLoadState,
+ mozilla::Maybe<uint32_t> aCacheKey = mozilla::Nothing());
+
+ void MaybeRestoreWindowName();
+
+ void StoreWindowNameToSHEntries();
+
+ void SetWillChangeProcess() { mWillChangeProcess = true; }
+ bool WillChangeProcess() { return mWillChangeProcess; }
+
+ // Create a content viewer within this nsDocShell for the given
+ // `WindowGlobalChild` actor.
+ nsresult CreateDocumentViewerForActor(
+ mozilla::dom::WindowGlobalChild* aWindowActor);
+
+ // Creates a real network channel (not a DocumentChannel) using the specified
+ // parameters.
+ // Used by nsDocShell when not using DocumentChannel, by DocumentLoadListener
+ // (parent-process DocumentChannel), and by DocumentChannelChild/ContentChild
+ // to transfer the resulting channel into the final process.
+ static nsresult CreateRealChannelForDocument(
+ nsIChannel** aChannel, nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIInterfaceRequestor* aCallbacks, nsLoadFlags aLoadFlags,
+ const nsAString& aSrcdoc, nsIURI* aBaseURI);
+
+ // Creates a real (not DocumentChannel) channel, and configures it using the
+ // supplied nsDocShellLoadState.
+ // Configuration options here are ones that should be applied to only the
+ // real channel, especially ones that need to QI to channel subclasses.
+ static bool CreateAndConfigureRealChannelForLoadState(
+ mozilla::dom::BrowsingContext* aBrowsingContext,
+ nsDocShellLoadState* aLoadState, mozilla::net::LoadInfo* aLoadInfo,
+ nsIInterfaceRequestor* aCallbacks, nsDocShell* aDocShell,
+ const mozilla::OriginAttributes& aOriginAttributes,
+ nsLoadFlags aLoadFlags, uint32_t aCacheKey, nsresult& rv,
+ nsIChannel** aChannel);
+
+ // This is used to deal with errors resulting from a failed page load.
+ // Errors are handled as follows:
+ // 1. Check to see if it's a file not found error or bad content
+ // encoding error.
+ // 2. Send the URI to a keyword server (if enabled)
+ // 3. If the error was DNS failure, then add www and .com to the URI
+ // (if appropriate).
+ // 4. If the www .com additions don't work, try those with an HTTPS scheme
+ // (if appropriate).
+ static already_AddRefed<nsIURI> AttemptURIFixup(
+ nsIChannel* aChannel, nsresult aStatus,
+ const mozilla::Maybe<nsCString>& aOriginalURIString, uint32_t aLoadType,
+ bool aIsTopFrame, bool aAllowKeywordFixup, bool aUsePrivateBrowsing,
+ bool aNotifyKeywordSearchLoading = false,
+ nsIInputStream** aNewPostData = nullptr,
+ bool* outWasSchemelessInput = nullptr);
+
+ static already_AddRefed<nsIURI> MaybeFixBadCertDomainErrorURI(
+ nsIChannel* aChannel, nsIURI* aUrl);
+
+ // Takes aStatus and filters out results that should not display
+ // an error page.
+ // If this returns a failed result, then we should display an error
+ // page with that result.
+ // aSkippedUnknownProtocolNavigation will be set to true if we chose
+ // to skip displaying an error page for an NS_ERROR_UNKNOWN_PROTOCOL
+ // navigation.
+ static nsresult FilterStatusForErrorPage(
+ nsresult aStatus, nsIChannel* aChannel, uint32_t aLoadType,
+ bool aIsTopFrame, bool aUseErrorPages, bool aIsInitialDocument,
+ bool* aSkippedUnknownProtocolNavigation = nullptr);
+
+ // Notify consumers of a search being loaded through the observer service:
+ static void MaybeNotifyKeywordSearchLoading(const nsString& aProvider,
+ const nsString& aKeyword);
+
+ nsDocShell* GetInProcessChildAt(int32_t aIndex);
+
+ static bool ShouldAddURIVisit(nsIChannel* aChannel);
+
+ /**
+ * Helper function that finds the last URI and its transition flags for a
+ * channel.
+ *
+ * This method first checks the channel's property bag to see if previous
+ * info has been saved. If not, it gives back the referrer of the channel.
+ *
+ * @param aChannel
+ * The channel we are transitioning to
+ * @param aURI
+ * Output parameter with the previous URI, not addref'd
+ * @param aChannelRedirectFlags
+ * If a redirect, output parameter with the previous redirect flags
+ * from nsIChannelEventSink
+ */
+ static void ExtractLastVisit(nsIChannel* aChannel, nsIURI** aURI,
+ uint32_t* aChannelRedirectFlags);
+
+ bool HasDocumentViewer() const { return !!mDocumentViewer; }
+
+ static uint32_t ComputeURILoaderFlags(
+ mozilla::dom::BrowsingContext* aBrowsingContext, uint32_t aLoadType);
+
+ void SetLoadingSessionHistoryInfo(
+ const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo,
+ bool aNeedToReportActiveAfterLoadingBecomesActive = false);
+ const mozilla::dom::LoadingSessionHistoryInfo*
+ GetLoadingSessionHistoryInfo() {
+ return mLoadingEntry.get();
+ }
+
+ already_AddRefed<nsIInputStream> GetPostDataFromCurrentEntry() const;
+ mozilla::Maybe<uint32_t> GetCacheKeyFromCurrentEntry() const;
+
+ // Loading and/or active entries are only set when session history
+ // in the parent is on.
+ bool FillLoadStateFromCurrentEntry(nsDocShellLoadState& aLoadState);
+
+ static bool ShouldAddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel);
+
+ bool IsOSHE(nsISHEntry* aEntry) const { return mOSHE == aEntry; }
+
+ mozilla::dom::ChildSHistory* GetSessionHistory() {
+ return mBrowsingContext->GetChildSessionHistory();
+ }
+
+ // This returns true only when using session history in parent.
+ bool IsLoadingFromSessionHistory();
+
+ NS_IMETHODIMP OnStartRequest(nsIRequest* aRequest) override;
+ NS_IMETHODIMP OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) override;
+
+ private: // member functions
+ friend class nsAppShellService;
+ friend class nsDSURIContentListener;
+ friend class FramingChecker;
+ friend class OnLinkClickEvent;
+ friend class nsIDocShell;
+ friend class mozilla::dom::BrowsingContext;
+ friend class mozilla::net::DocumentLoadListener;
+ friend class nsGlobalWindowOuter;
+
+ nsDocShell(mozilla::dom::BrowsingContext* aBrowsingContext,
+ uint64_t aContentWindowID);
+
+ static inline uint32_t PRTimeToSeconds(PRTime aTimeUsec) {
+ return uint32_t(aTimeUsec / PR_USEC_PER_SEC);
+ }
+
+ virtual ~nsDocShell();
+
+ //
+ // nsDocLoader
+ //
+
+ virtual void DestroyChildren() override;
+
+ // Overridden from nsDocLoader, this provides more information than the
+ // normal OnStateChange with flags STATE_REDIRECTING
+ virtual void OnRedirectStateChange(nsIChannel* aOldChannel,
+ nsIChannel* aNewChannel,
+ uint32_t aRedirectFlags,
+ uint32_t aStateFlags) override;
+
+ // Override the parent setter from nsDocLoader
+ virtual nsresult SetDocLoaderParent(nsDocLoader* aLoader) override;
+
+ //
+ // Content Viewer Management
+ //
+
+ nsresult EnsureDocumentViewer();
+
+ // aPrincipal can be passed in if the caller wants. If null is
+ // passed in, the about:blank principal will end up being used.
+ // aCSP, if any, will be used for the new about:blank load.
+ nsresult CreateAboutBlankDocumentViewer(
+ nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal,
+ nsIContentSecurityPolicy* aCSP, nsIURI* aBaseURI, bool aIsInitialDocument,
+ const mozilla::Maybe<nsILoadInfo::CrossOriginEmbedderPolicy>& aCOEP =
+ mozilla::Nothing(),
+ bool aTryToSaveOldPresentation = true, bool aCheckPermitUnload = true,
+ mozilla::dom::WindowGlobalChild* aActor = nullptr);
+
+ nsresult CreateDocumentViewer(const nsACString& aContentType,
+ nsIRequest* aRequest,
+ nsIStreamListener** aContentHandler);
+
+ nsresult NewDocumentViewerObj(const nsACString& aContentType,
+ nsIRequest* aRequest, nsILoadGroup* aLoadGroup,
+ nsIStreamListener** aContentHandler,
+ nsIDocumentViewer** aViewer);
+
+ already_AddRefed<nsILoadURIDelegate> GetLoadURIDelegate();
+
+ nsresult SetupNewViewer(
+ nsIDocumentViewer* aNewViewer,
+ mozilla::dom::WindowGlobalChild* aWindowActor = nullptr);
+
+ //
+ // Session History
+ //
+
+ // Either aChannel or aOwner must be null. If aChannel is
+ // present, the owner should be gotten from it.
+ // If aCloneChildren is true, then our current session history's
+ // children will be cloned onto the new entry. This should be
+ // used when we aren't actually changing the document while adding
+ // the new session history entry.
+ // aCsp is the CSP to be used for the load. That is *not* the CSP
+ // that will be applied to subresource loads within that document
+ // but the CSP for the document load itself. E.g. if that CSP
+ // includes upgrade-insecure-requests, then the new top-level load
+ // will be upgraded to HTTPS.
+ nsresult AddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ bool aCloneChildren, nsISHEntry** aNewEntry);
+
+ void UpdateActiveEntry(
+ bool aReplace, const mozilla::Maybe<nsPoint>& aPreviousScrollPos,
+ nsIURI* aURI, nsIURI* aOriginalURI, nsIReferrerInfo* aReferrerInfo,
+ nsIPrincipal* aTriggeringPrincipal, nsIContentSecurityPolicy* aCsp,
+ const nsAString& aTitle, bool aScrollRestorationIsManual,
+ nsIStructuredCloneContainer* aData, bool aURIWasModified);
+
+ nsresult AddChildSHEntry(nsISHEntry* aCloneRef, nsISHEntry* aNewEntry,
+ int32_t aChildOffset, uint32_t aLoadType,
+ bool aCloneChildren);
+
+ nsresult AddChildSHEntryToParent(nsISHEntry* aNewEntry, int32_t aChildOffset,
+ bool aCloneChildren);
+
+ // Call this method to swap in a new history entry to m[OL]SHE, rather than
+ // setting it directly. This completes the navigation in all docshells
+ // in the case of a subframe navigation.
+ // Returns old mOSHE/mLSHE.
+ already_AddRefed<nsISHEntry> SetHistoryEntry(nsCOMPtr<nsISHEntry>* aPtr,
+ nsISHEntry* aEntry);
+
+ // This method calls SetHistoryEntry and updates mOSHE and mLSHE in BC to be
+ // the same as in docshell
+ void SetHistoryEntryAndUpdateBC(const mozilla::Maybe<nsISHEntry*>& aLSHE,
+ const mozilla::Maybe<nsISHEntry*>& aOSHE);
+
+ // If aNotifiedBeforeUnloadListeners is true, "beforeunload" event listeners
+ // were notified by the caller and given the chance to abort the navigation,
+ // and should not be notified again.
+ static nsresult ReloadDocument(
+ nsDocShell* aDocShell, mozilla::dom::Document* aDocument,
+ uint32_t aLoadType, mozilla::dom::BrowsingContext* aBrowsingContext,
+ nsIURI* aCurrentURI, nsIReferrerInfo* aReferrerInfo,
+ bool aNotifiedBeforeUnloadListeners = false);
+
+ public:
+ bool IsAboutBlankLoadOntoInitialAboutBlank(nsIURI* aURI,
+ bool aInheritPrincipal,
+ nsIPrincipal* aPrincipalToInherit);
+
+ private:
+ //
+ // URI Load
+ //
+
+ // Actually open a channel and perform a URI load. Callers need to pass a
+ // non-null aLoadState->TriggeringPrincipal() which initiated the URI load.
+ // Please note that the TriggeringPrincipal will be used for performing
+ // security checks. If aLoadState->URI() is provided by the web, then please
+ // do not pass a SystemPrincipal as the triggeringPrincipal. If
+ // aLoadState()->PrincipalToInherit is null, then no inheritance of any sort
+ // will happen and the load will get a principal based on the URI being
+ // loaded. If the Srcdoc flag is set (INTERNAL_LOAD_FLAGS_IS_SRCDOC), the load
+ // will be considered as a srcdoc load, and the contents of Srcdoc will be
+ // loaded instead of the URI. aLoadState->OriginalURI() will be set as the
+ // originalURI on the channel that does the load. If OriginalURI is null, URI
+ // will be set as the originalURI. If LoadReplace is true, LOAD_REPLACE flag
+ // will be set on the nsIChannel.
+ // If `aCacheKey` is supplied, use it for the session history entry.
+ nsresult DoURILoad(nsDocShellLoadState* aLoadState,
+ mozilla::Maybe<uint32_t> aCacheKey, nsIRequest** aRequest);
+
+ static nsresult AddHeadersToChannel(nsIInputStream* aHeadersData,
+ nsIChannel* aChannel);
+
+ nsresult OpenInitializedChannel(nsIChannel* aChannel,
+ nsIURILoader* aURILoader,
+ uint32_t aOpenFlags);
+ nsresult OpenRedirectedChannel(nsDocShellLoadState* aLoadState);
+
+ void UpdateMixedContentChannelForNewLoad(nsIChannel* aChannel);
+
+ MOZ_CAN_RUN_SCRIPT
+ nsresult ScrollToAnchor(bool aCurHasRef, bool aNewHasRef,
+ nsACString& aNewHash, uint32_t aLoadType);
+
+ // This returns the load type for a form submission (see
+ // https://html.spec.whatwg.org/#form-submission-algorithm). The load type
+ // should be set as soon as the target BC has been determined.
+ uint32_t GetLoadTypeForFormSubmission(
+ mozilla::dom::BrowsingContext* aTargetBC,
+ nsDocShellLoadState* aLoadState);
+
+ private:
+ // Returns true if it is the caller's responsibility to ensure
+ // FireOnLocationChange is called.
+ // In all other cases false is returned.
+ // Either aChannel or aTriggeringPrincipal must be null. If aChannel is
+ // present, the owner should be gotten from it.
+ // If OnNewURI calls AddToSessionHistory, it will pass its
+ // aCloneSHChildren argument as aCloneChildren.
+ // aCsp is the CSP to be used for the load. That is *not* the CSP
+ // that will be applied to subresource loads within that document
+ // but the CSP for the document load itself. E.g. if that CSP
+ // includes upgrade-insecure-requests, then the new top-level load
+ // will be upgraded to HTTPS.
+ bool OnNewURI(nsIURI* aURI, nsIChannel* aChannel,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, bool aAddToGlobalHistory,
+ bool aCloneSHChildren);
+
+ public:
+ // If wireframe collection is enabled, will attempt to gather the
+ // wireframe for the document.
+ mozilla::Maybe<mozilla::dom::Wireframe> GetWireframe();
+
+ // If wireframe collection is enabled, will attempt to gather the
+ // wireframe for the document and stash it inside of the active history
+ // entry. Returns true if wireframes were collected.
+ bool CollectWireframe();
+
+ // Helper method that is called when a new document (including any
+ // sub-documents - ie. frames) has been completely loaded.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult EndPageLoad(nsIWebProgress* aProgress, nsIChannel* aChannel,
+ nsresult aResult);
+
+ // Builds an error page URI (e.g. about:neterror?etc) for the given aURI
+ // and displays it via the LoadErrorPage() overload below.
+ nsresult LoadErrorPage(nsIURI* aURI, const char16_t* aURL,
+ const char* aErrorPage, const char* aErrorType,
+ const char16_t* aDescription, const char* aCSSClass,
+ nsIChannel* aFailedChannel);
+
+ // This method directly loads aErrorURI as an error page. aFailedURI and
+ // aFailedChannel come from DisplayLoadError() or the LoadErrorPage() overload
+ // above.
+ nsresult LoadErrorPage(nsIURI* aErrorURI, nsIURI* aFailedURI,
+ nsIChannel* aFailedChannel);
+
+ bool DisplayLoadError(nsresult aError, nsIURI* aURI, const char16_t* aURL,
+ nsIChannel* aFailedChannel) {
+ bool didDisplayLoadError = false;
+ DisplayLoadError(aError, aURI, aURL, aFailedChannel, &didDisplayLoadError);
+ return didDisplayLoadError;
+ }
+
+ //
+ // Uncategorized
+ //
+
+ // Get the principal that we'll set on the channel if we're inheriting. If
+ // aConsiderCurrentDocument is true, we try to use the current document if
+ // at all possible. If that fails, we fall back on the parent document.
+ // If that fails too, we force creation of a content viewer and use the
+ // resulting principal. If aConsiderCurrentDocument is false, we just look
+ // at the parent.
+ // If aConsiderPartitionedPrincipal is true, we consider the partitioned
+ // principal instead of the node principal.
+ nsIPrincipal* GetInheritedPrincipal(
+ bool aConsiderCurrentDocument,
+ bool aConsiderPartitionedPrincipal = false);
+
+ /**
+ * Helper function that caches a URI and a transition for saving later.
+ *
+ * @param aChannel
+ * Channel that will have these properties saved
+ * @param aURI
+ * The URI to save for later
+ * @param aChannelRedirectFlags
+ * The nsIChannelEventSink redirect flags to save for later
+ */
+ static void SaveLastVisit(nsIChannel* aChannel, nsIURI* aURI,
+ uint32_t aChannelRedirectFlags);
+
+ /**
+ * Helper function for adding a URI visit using IHistory.
+ *
+ * The IHistory API maintains chains of visits, tracking both HTTP referrers
+ * and redirects for a user session. VisitURI requires the current URI and
+ * the previous URI in the chain.
+ *
+ * Visits can be saved either during a redirect or when the request has
+ * reached its final destination. The previous URI in the visit may be
+ * from another redirect.
+ *
+ * @pre aURI is not null.
+ *
+ * @param aURI
+ * The URI that was just visited
+ * @param aPreviousURI
+ * The previous URI of this visit
+ * @param aChannelRedirectFlags
+ * For redirects, the redirect flags from nsIChannelEventSink
+ * (0 otherwise)
+ * @param aResponseStatus
+ * For HTTP channels, the response code (0 otherwise).
+ */
+ void AddURIVisit(nsIURI* aURI, nsIURI* aPreviousURI,
+ uint32_t aChannelRedirectFlags,
+ uint32_t aResponseStatus = 0);
+
+ /**
+ * Internal helper funtion
+ */
+ static void InternalAddURIVisit(
+ nsIURI* aURI, nsIURI* aPreviousURI, uint32_t aChannelRedirectFlags,
+ uint32_t aResponseStatus, mozilla::dom::BrowsingContext* aBrowsingContext,
+ nsIWidget* aWidget, uint32_t aLoadType, bool aWasUpgraded);
+
+ static already_AddRefed<nsIURIFixupInfo> KeywordToURI(
+ const nsACString& aKeyword, bool aIsPrivateContext);
+
+ // Sets the current document's current state object to the given SHEntry's
+ // state object. The current state object is eventually given to the page
+ // in the PopState event.
+ void SetDocCurrentStateObj(nsISHEntry* aShEntry,
+ mozilla::dom::SessionHistoryInfo* aInfo);
+
+ // Returns true if would have called FireOnLocationChange,
+ // but did not because aFireOnLocationChange was false on entry.
+ // In this case it is the caller's responsibility to ensure
+ // FireOnLocationChange is called.
+ // In all other cases false is returned.
+ bool SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
+ bool aFireOnLocationChange, bool aIsInitialAboutBlank,
+ uint32_t aLocationFlags);
+
+ // The following methods deal with saving and restoring content viewers
+ // in session history.
+
+ // mDocumentViewer points to the current content viewer associated with
+ // this docshell. When loading a new document, the content viewer is
+ // either destroyed or stored into a session history entry. To make sure
+ // that destruction happens in a controlled fashion, a given content viewer
+ // is always owned in exactly one of these ways:
+ // 1) The content viewer is active and owned by a docshell's
+ // mDocumentViewer.
+ // 2) The content viewer is still being displayed while we begin loading
+ // a new document. The content viewer is owned by the _new_
+ // content viewer's mPreviousViewer, and has a pointer to the
+ // nsISHEntry where it will eventually be stored. The content viewer
+ // has been close()d by the docshell, which detaches the document from
+ // the window object.
+ // 3) The content viewer is cached in session history. The nsISHEntry
+ // has the only owning reference to the content viewer. The viewer
+ // has released its nsISHEntry pointer to prevent circular ownership.
+ //
+ // When restoring a content viewer from session history, open() is called
+ // to reattach the document to the window object. The content viewer is
+ // then placed into mDocumentViewer and removed from the history entry.
+ // (mDocumentViewer is put into session history as described above, if
+ // applicable).
+
+ // Determines whether we can safely cache the current mDocumentViewer in
+ // session history. This checks a number of factors such as cache policy,
+ // pending requests, and unload handlers.
+ // |aLoadType| should be the load type that will replace the current
+ // presentation. |aNewRequest| should be the request for the document to
+ // be loaded in place of the current document, or null if such a request
+ // has not been created yet. |aNewDocument| should be the document that will
+ // replace the current document.
+ bool CanSavePresentation(uint32_t aLoadType, nsIRequest* aNewRequest,
+ mozilla::dom::Document* aNewDocument,
+ bool aReportBFCacheComboTelemetry);
+
+ static void ReportBFCacheComboTelemetry(uint32_t aCombo);
+
+ // Captures the state of the supporting elements of the presentation
+ // (the "window" object, docshell tree, meta-refresh loads, and security
+ // state) and stores them on |mOSHE|.
+ nsresult CaptureState();
+
+ // Begin the toplevel restore process for |aSHEntry|.
+ // This simulates a channel open, and defers the real work until
+ // RestoreFromHistory is called from a PLEvent.
+ nsresult RestorePresentation(nsISHEntry* aSHEntry, bool* aRestoring);
+
+ // Call BeginRestore(nullptr, false) for each child of this shell.
+ nsresult BeginRestoreChildren();
+
+ // Method to get our current position and size without flushing
+ void DoGetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight);
+
+ // Call this when a URI load is handed to us (via OnLinkClick or
+ // InternalLoad). This makes sure that we're not inside unload, or that if
+ // we are it's still OK to load this URI.
+ bool IsOKToLoadURI(nsIURI* aURI);
+
+ // helpers for executing commands
+ nsresult GetControllerForCommand(const char* aCommand,
+ nsIController** aResult);
+
+ // Possibly create a ClientSource object to represent an initial about:blank
+ // window that has not been allocated yet. Normally we try not to create
+ // this about:blank window until something calls GetDocument(). We still need
+ // the ClientSource to exist for this conceptual window, though.
+ //
+ // The ClientSource is created with the given principal if specified. If
+ // the principal is not provided we will attempt to inherit it when we
+ // are sure it will match what the real about:blank window principal
+ // would have been. There are some corner cases where we cannot easily
+ // determine the correct principal and will not create the ClientSource.
+ // In these cases the initial about:blank will appear to not exist until
+ // its real document and window are created.
+ void MaybeCreateInitialClientSource(nsIPrincipal* aPrincipal = nullptr);
+
+ // Determine if a service worker is allowed to control a window in this
+ // docshell with the given URL. If there are any reasons it should not,
+ // this will return false. If true is returned then the window *may* be
+ // controlled. The caller must still consult either the parent controller
+ // or the ServiceWorkerManager to determine if a service worker should
+ // actually control the window.
+ bool ServiceWorkerAllowedToControlWindow(nsIPrincipal* aPrincipal,
+ nsIURI* aURI);
+
+ // Return the ClientInfo for the initial about:blank window, if it exists
+ // or we have speculatively created a ClientSource in
+ // MaybeCreateInitialClientSource(). This can return a ClientInfo object
+ // even if GetExtantDoc() returns nullptr.
+ mozilla::Maybe<mozilla::dom::ClientInfo> GetInitialClientInfo() const;
+
+ /**
+ * Initializes mTiming if it isn't yet.
+ * After calling this, mTiming is non-null. This method returns true if the
+ * initialization of the Timing can be reset (basically this is true if a new
+ * Timing object is created).
+ * In case the loading is aborted, MaybeResetInitTiming() can be called
+ * passing the return value of MaybeInitTiming(): if it's possible to reset
+ * the Timing, this method will do it.
+ */
+ [[nodiscard]] bool MaybeInitTiming();
+ void MaybeResetInitTiming(bool aReset);
+
+ // Convenience method for getting our parent docshell. Can return null
+ already_AddRefed<nsDocShell> GetInProcessParentDocshell();
+
+ // Internal implementation of nsIDocShell::FirePageHideNotification.
+ // If aSkipCheckingDynEntries is true, it will not try to remove dynamic
+ // subframe entries. This is to avoid redundant RemoveDynEntries calls in all
+ // children docshells.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FirePageHideNotificationInternal(
+ bool aIsUnload, bool aSkipCheckingDynEntries);
+
+ void ThawFreezeNonRecursive(bool aThaw);
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FirePageHideShowNonRecursive(bool aShow);
+
+ nsresult Dispatch(already_AddRefed<nsIRunnable>&& aRunnable);
+
+ void ReattachEditorToWindow(nsISHEntry* aSHEntry);
+ void ClearFrameHistory(nsISHEntry* aEntry);
+ // Determine if this type of load should update history.
+ static bool ShouldUpdateGlobalHistory(uint32_t aLoadType);
+ void UpdateGlobalHistoryTitle(nsIURI* aURI);
+ bool IsSubframe() { return mBrowsingContext->IsSubframe(); }
+ bool CanSetOriginAttributes();
+ bool ShouldBlockLoadingForBackButton();
+ static bool ShouldDiscardLayoutState(nsIHttpChannel* aChannel);
+ bool HasUnloadedParent();
+ bool JustStartedNetworkLoad();
+ bool NavigationBlockedByPrinting(bool aDisplayErrorDialog = true);
+ bool IsNavigationAllowed(bool aDisplayPrintErrorDialog = true,
+ bool aCheckIfUnloadFired = true);
+ nsIScrollableFrame* GetRootScrollFrame();
+ nsIChannel* GetCurrentDocChannel();
+ nsresult EnsureScriptEnvironment();
+ nsresult EnsureEditorData();
+ nsresult EnsureTransferableHookData();
+ nsresult EnsureFind();
+ nsresult EnsureCommandHandler();
+ nsresult RefreshURIFromQueue();
+ void RefreshURIToQueue();
+ nsresult Embed(nsIDocumentViewer* aDocumentViewer,
+ mozilla::dom::WindowGlobalChild* aWindowActor,
+ bool aIsTransientAboutBlank, bool aPersist,
+ nsIRequest* aRequest, nsIURI* aPreviousURI);
+ nsPresContext* GetEldestPresContext();
+ nsresult CheckLoadingPermissions();
+ nsresult LoadHistoryEntry(nsISHEntry* aEntry, uint32_t aLoadType,
+ bool aUserActivation);
+ nsresult LoadHistoryEntry(
+ const mozilla::dom::LoadingSessionHistoryInfo& aEntry, uint32_t aLoadType,
+ bool aUserActivation);
+ nsresult LoadHistoryEntry(nsDocShellLoadState* aLoadState, uint32_t aLoadType,
+ bool aLoadingCurrentEntry);
+ nsresult GetHttpChannel(nsIChannel* aChannel, nsIHttpChannel** aReturn);
+ nsresult ConfirmRepost(bool* aRepost);
+ nsresult GetPromptAndStringBundle(nsIPrompt** aPrompt,
+ nsIStringBundle** aStringBundle);
+ nsresult SetCurScrollPosEx(int32_t aCurHorizontalPos,
+ int32_t aCurVerticalPos);
+ nsPoint GetCurScrollPos();
+
+ already_AddRefed<mozilla::dom::ChildSHistory> GetRootSessionHistory();
+
+ bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; }
+
+ // Handles retrieval of subframe session history for nsDocShell::LoadURI. If a
+ // load is requested in a subframe of the current DocShell, the subframe
+ // loadType may need to reflect the loadType of the parent document, or in
+ // some cases (like reloads), the history load may need to be cancelled. See
+ // function comments for in-depth logic descriptions.
+ // Returns true if the method itself deals with the load.
+ bool MaybeHandleSubframeHistory(nsDocShellLoadState* aLoadState,
+ bool aContinueHandlingSubframeHistory);
+
+ // If we are passed a named target during InternalLoad, this method handles
+ // moving the load to the browsing context the target name resolves to.
+ nsresult PerformRetargeting(nsDocShellLoadState* aLoadState);
+
+ // Returns one of nsIContentPolicy::TYPE_DOCUMENT,
+ // nsIContentPolicy::TYPE_INTERNAL_IFRAME, or
+ // nsIContentPolicy::TYPE_INTERNAL_FRAME depending on who is responsible for
+ // this docshell.
+ nsContentPolicyType DetermineContentType();
+
+ // If this is an iframe, and the embedder is OOP, then notifes the
+ // embedder that loading has finished and we shouldn't be blocking
+ // load of the embedder. Only called when we fail to load, as we wait
+ // for the load event of our Document before notifying success.
+ //
+ // If aFireFrameErrorEvent is true, then fires an error event at the
+ // embedder element, for both in-process and OOP embedders.
+ void UnblockEmbedderLoadEventForFailure(bool aFireFrameErrorEvent = false);
+
+ struct SameDocumentNavigationState {
+ nsAutoCString mCurrentHash;
+ nsAutoCString mNewHash;
+ bool mCurrentURIHasRef = false;
+ bool mNewURIHasRef = false;
+ bool mSameExceptHashes = false;
+ bool mSecureUpgradeURI = false;
+ bool mHistoryNavBetweenSameDoc = false;
+ };
+
+ // Check to see if we're loading a prior history entry or doing a fragment
+ // navigation in the same document.
+ // NOTE: In case we are doing a fragment navigation, and HTTPS-Only/ -First
+ // mode is enabled and upgraded the underlying document, we update the URI of
+ // aLoadState from HTTP to HTTPS (if neccessary).
+ bool IsSameDocumentNavigation(nsDocShellLoadState* aLoadState,
+ SameDocumentNavigationState& aState);
+
+ // ... If so, handle the scrolling or other action required instead of
+ // continuing with new document navigation.
+ MOZ_CAN_RUN_SCRIPT
+ nsresult HandleSameDocumentNavigation(nsDocShellLoadState* aLoadState,
+ SameDocumentNavigationState& aState,
+ bool& aSameDocument);
+
+ uint32_t GetSameDocumentNavigationFlags(nsIURI* aNewURI);
+
+ // Called when the Private Browsing state of a nsDocShell changes.
+ void NotifyPrivateBrowsingChanged();
+
+ // Internal helpers for BrowsingContext to pass update values to nsIDocShell's
+ // LoadGroup.
+ void SetLoadGroupDefaultLoadFlags(nsLoadFlags aLoadFlags);
+
+ void SetTitleOnHistoryEntry(bool aUpdateEntryInSessionHistory);
+
+ void SetScrollRestorationIsManualOnHistoryEntry(nsISHEntry* aSHEntry,
+ bool aIsManual);
+
+ void SetCacheKeyOnHistoryEntry(nsISHEntry* aSHEntry, uint32_t aCacheKey);
+
+ // If the LoadState's URI is a javascript: URI, checks that the triggering
+ // principal subsumes the principal of the current document, and returns
+ // NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI if it does not.
+ nsresult CheckDisallowedJavascriptLoad(nsDocShellLoadState* aLoadState);
+
+ nsresult LoadURI(nsDocShellLoadState* aLoadState, bool aSetNavigating,
+ bool aContinueHandlingSubframeHistory);
+
+ // Sets the active entry to the current loading entry. aPersist is used in the
+ // case a new session history entry is added to the session history.
+ // aExpired is true if the relevant nsIChannel has its cache token expired.
+ // aCacheKey is the channel's cache key.
+ // aPreviousURI should be the URI that was previously loaded into the
+ // nsDocshell
+ void MoveLoadingToActiveEntry(bool aPersist, bool aExpired,
+ uint32_t aCacheKey, nsIURI* aPreviousURI);
+
+ void ActivenessMaybeChanged();
+
+ /**
+ * Returns true if `noopener` will be force-enabled by any attempt to create
+ * a popup window, even if rel="opener" is requested.
+ */
+ bool NoopenerForceEnabled();
+
+ bool ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
+ nsIURI* aLinkURI, nsIContent* aContent,
+ bool aIsUserTriggered);
+
+ void RecordSingleChannelId(bool aStartRequest, nsIRequest* aRequest);
+
+ void SetChannelToDisconnectOnPageHide(uint64_t aChannelId) {
+ MOZ_ASSERT(mChannelToDisconnectOnPageHide == 0);
+ mChannelToDisconnectOnPageHide = aChannelId;
+ }
+ void MaybeDisconnectChildListenersOnPageHide();
+
+ /**
+ * Helper for addState and document.open that does just the
+ * history-manipulation guts.
+ *
+ * Arguments the spec defines:
+ *
+ * @param aDocument the document we're manipulating. This will get the new
+ * URI.
+ * @param aNewURI the new URI.
+ * @param aData The serialized state data. May be null.
+ * @param aTitle The new title. May be empty.
+ * @param aReplace whether this should replace the exising SHEntry.
+ *
+ * Arguments we need internally because deriving them from the
+ * others is a bit complicated:
+ *
+ * @param aCurrentURI the current URI we're working with. Might be null.
+ * @param aEqualURIs whether the two URIs involved are equal.
+ */
+ nsresult UpdateURLAndHistory(mozilla::dom::Document* aDocument,
+ nsIURI* aNewURI,
+ nsIStructuredCloneContainer* aData,
+ const nsAString& aTitle, bool aReplace,
+ nsIURI* aCurrentURI, bool aEqualURIs);
+
+ private:
+ void SetCurrentURIInternal(nsIURI* aURI);
+
+ // data members
+ nsString mTitle;
+ nsCString mOriginalUriString;
+ nsTObserverArray<nsWeakPtr> mPrivacyObservers;
+ nsTObserverArray<nsWeakPtr> mReflowObservers;
+ nsTObserverArray<nsWeakPtr> mScrollObservers;
+ mozilla::UniquePtr<mozilla::dom::ClientSource> mInitialClientSource;
+ nsCOMPtr<nsINetworkInterceptController> mInterceptController;
+ RefPtr<nsDOMNavigationTiming> mTiming;
+ RefPtr<nsDSURIContentListener> mContentListener;
+ RefPtr<nsGlobalWindowOuter> mScriptGlobal;
+ nsCOMPtr<nsIPrincipal> mParentCharsetPrincipal;
+ // The following 3 lists contain either nsITimer or nsRefreshTimer objects.
+ // URIs to refresh are collected to mRefreshURIList.
+ nsCOMPtr<nsIMutableArray> mRefreshURIList;
+ // mSavedRefreshURIList is used to move the entries from mRefreshURIList to
+ // mOSHE.
+ nsCOMPtr<nsIMutableArray> mSavedRefreshURIList;
+ // BFCache-in-parent implementation caches the entries in
+ // mBFCachedRefreshURIList.
+ nsCOMPtr<nsIMutableArray> mBFCachedRefreshURIList;
+ uint64_t mContentWindowID;
+ nsCOMPtr<nsIDocumentViewer> mDocumentViewer;
+ nsCOMPtr<nsIWidget> mParentWidget;
+ RefPtr<mozilla::dom::ChildSHistory> mSessionHistory;
+ nsCOMPtr<nsIWebBrowserFind> mFind;
+ RefPtr<nsCommandManager> mCommandManager;
+ RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext;
+
+ // Weak reference to our BrowserChild actor.
+ nsWeakPtr mBrowserChild;
+
+ // Dimensions of the docshell
+ nsIntRect mBounds;
+
+ /**
+ * Content-Type Hint of the most-recently initiated load. Used for
+ * session history entries.
+ */
+ nsCString mContentTypeHint;
+
+ // mCurrentURI should be marked immutable on set if possible.
+ // Change mCurrentURI only through SetCurrentURIInternal method.
+ nsCOMPtr<nsIURI> mCurrentURI;
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+
+#ifdef DEBUG
+ // We're counting the number of |nsDocShells| to help find leaks
+ static unsigned long gNumberOfDocShells;
+
+ nsCOMPtr<nsIURI> mLastOpenedURI;
+#endif
+
+ // Reference to the SHEntry for this docshell until the page is destroyed.
+ // Somebody give me better name
+ // Only used when SHIP is disabled.
+ nsCOMPtr<nsISHEntry> mOSHE;
+
+ // Reference to the SHEntry for this docshell until the page is loaded
+ // Somebody give me better name.
+ // If mLSHE is non-null, non-pushState subframe loads don't create separate
+ // root history entries. That is, frames loaded during the parent page
+ // load don't generate history entries the way frame navigation after the
+ // parent has loaded does. (This isn't the only purpose of mLSHE.)
+ // Only used when SHIP is disabled.
+ nsCOMPtr<nsISHEntry> mLSHE;
+
+ // These are only set when fission.sessionHistoryInParent is set.
+ mozilla::UniquePtr<mozilla::dom::SessionHistoryInfo> mActiveEntry;
+ bool mActiveEntryIsLoadingFromSessionHistory = false;
+ // mLoadingEntry is set when we're about to start loading. Whenever
+ // setting mLoadingEntry, be sure to also set
+ // mNeedToReportActiveAfterLoadingBecomesActive.
+ mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> mLoadingEntry;
+
+ // Holds a weak pointer to a RestorePresentationEvent object if any that
+ // holds a weak pointer back to us. We use this pointer to possibly revoke
+ // the event whenever necessary.
+ nsRevocableEventPtr<RestorePresentationEvent> mRestorePresentationEvent;
+
+ // Editor data, if this document is designMode or contentEditable.
+ mozilla::UniquePtr<nsDocShellEditorData> mEditorData;
+
+ // The URI we're currently loading. This is only relevant during the
+ // firing of a pagehide/unload. The caller of FirePageHideNotification()
+ // is responsible for setting it and unsetting it. It may be null if the
+ // pagehide/unload is happening for some reason other than just loading a
+ // new URI.
+ nsCOMPtr<nsIURI> mLoadingURI;
+
+ // Set in LoadErrorPage from the method argument and used later
+ // in CreateDocumentViewer. We have to delay an shistory entry creation
+ // for which these objects are needed.
+ nsCOMPtr<nsIURI> mFailedURI;
+ nsCOMPtr<nsIChannel> mFailedChannel;
+
+ mozilla::UniquePtr<mozilla::gfx::Matrix5x4> mColorMatrix;
+
+ const mozilla::Encoding* mParentCharset;
+
+ // WEAK REFERENCES BELOW HERE.
+ // Note these are intentionally not addrefd. Doing so will create a cycle.
+ // For that reasons don't use nsCOMPtr.
+
+ nsIDocShellTreeOwner* mTreeOwner; // Weak Reference
+
+ RefPtr<mozilla::dom::EventTarget> mChromeEventHandler;
+
+ mozilla::ScrollbarPreference mScrollbarPref; // persistent across doc loads
+
+ eCharsetReloadState mCharsetReloadState;
+
+ int32_t mParentCharsetSource;
+ mozilla::CSSIntSize mFrameMargins;
+
+ // This can either be a content docshell or a chrome docshell.
+ const int32_t mItemType;
+
+ // Index into the nsISHEntry array, indicating the previous and current
+ // entry at the time that this DocShell begins to load. Consequently
+ // root docshell's indices can differ from child docshells'.
+ int32_t mPreviousEntryIndex;
+ int32_t mLoadedEntryIndex;
+
+ BusyFlags mBusyFlags;
+ AppType mAppType;
+ uint32_t mLoadType;
+ uint32_t mFailedLoadType;
+
+ // Whether or not handling of the <meta name="viewport"> tag is overridden.
+ // Possible values are defined as constants in nsIDocShell.idl.
+ MetaViewportOverride mMetaViewportOverride;
+
+ // See WindowGlobalParent::mSingleChannelId.
+ mozilla::Maybe<uint64_t> mSingleChannelId;
+ uint32_t mRequestForBlockingFromBFCacheCount = 0;
+
+ uint64_t mChannelToDisconnectOnPageHide;
+
+ uint32_t mPendingReloadCount = 0;
+
+ // The following two fields cannot be declared as bit fields
+ // because of uses with AutoRestore.
+ bool mCreatingDocument; // (should be) debugging only
+#ifdef DEBUG
+ bool mInEnsureScriptEnv;
+ uint64_t mDocShellID = 0;
+#endif
+
+ bool mInitialized : 1;
+ bool mAllowSubframes : 1;
+ bool mAllowMetaRedirects : 1;
+ bool mAllowImages : 1;
+ bool mAllowMedia : 1;
+ bool mAllowDNSPrefetch : 1;
+ bool mAllowWindowControl : 1;
+ bool mCSSErrorReportingEnabled : 1;
+ bool mAllowAuth : 1;
+ bool mAllowKeywordFixup : 1;
+ bool mDisableMetaRefreshWhenInactive : 1;
+ bool mIsAppTab : 1;
+ bool mWindowDraggingAllowed : 1;
+ bool mInFrameSwap : 1;
+
+ // This boolean is set to true right before we fire pagehide and generally
+ // unset when we embed a new content viewer. While it's true no navigation
+ // is allowed in this docshell.
+ bool mFiredUnloadEvent : 1;
+
+ // this flag is for bug #21358. a docshell may load many urls
+ // which don't result in new documents being created (i.e. a new
+ // content viewer) we want to make sure we don't call a on load
+ // event more than once for a given content viewer.
+ bool mEODForCurrentDocument : 1;
+ bool mURIResultedInDocument : 1;
+
+ bool mIsBeingDestroyed : 1;
+
+ bool mIsExecutingOnLoadHandler : 1;
+
+ // Indicates to CreateDocumentViewer() that it is safe to cache the old
+ // presentation of the page, and to SetupNewViewer() that the old viewer
+ // should be passed a SHEntry to save itself into.
+ // Only used with SHIP disabled.
+ bool mSavingOldViewer : 1;
+
+ bool mInvisible : 1;
+ bool mHasLoadedNonBlankURI : 1;
+
+ // This flag means that mTiming has been initialized but nulled out.
+ // We will check the innerWin's timing before creating a new one
+ // in MaybeInitTiming()
+ bool mBlankTiming : 1;
+
+ // This flag indicates when the title is valid for the current URI.
+ bool mTitleValidForCurrentURI : 1;
+
+ // If mWillChangeProcess is set to true, then when the docshell is destroyed,
+ // we prepare the browsing context to change process.
+ bool mWillChangeProcess : 1;
+
+ // This flag indicates whether or not the DocShell is currently executing an
+ // nsIWebNavigation navigation method.
+ bool mIsNavigating : 1;
+
+ // Whether we have a pending encoding autodetection request from the
+ // menu for all encodings.
+ bool mForcedAutodetection : 1;
+
+ /*
+ * Set to true if we're checking session history (in the parent process) for
+ * a possible history load. Used only with iframes.
+ */
+ bool mCheckingSessionHistory : 1;
+
+ // Whether mBrowsingContext->SetActiveSessionHistoryEntry() needs to be called
+ // when the loading entry becomes the active entry. This is used for the
+ // initial about:blank-replacing about:blank in order to make the history
+ // length WPTs pass.
+ bool mNeedToReportActiveAfterLoadingBecomesActive : 1;
+};
+
+inline nsISupports* ToSupports(nsDocShell* aDocShell) {
+ return static_cast<nsIDocumentLoader*>(aDocShell);
+}
+
+#endif /* nsDocShell_h__ */
diff --git a/docshell/base/nsDocShellEditorData.cpp b/docshell/base/nsDocShellEditorData.cpp
new file mode 100644
index 0000000000..6fe132a977
--- /dev/null
+++ b/docshell/base/nsDocShellEditorData.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDocShellEditorData.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/HTMLEditor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsEditingSession.h"
+#include "nsIDocShell.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsDocShellEditorData::nsDocShellEditorData(nsIDocShell* aOwningDocShell)
+ : mDocShell(aOwningDocShell),
+ mDetachedEditingState(Document::EditingState::eOff),
+ mMakeEditable(false),
+ mIsDetached(false),
+ mDetachedMakeEditable(false) {
+ NS_ASSERTION(mDocShell, "Where is my docShell?");
+}
+
+nsDocShellEditorData::~nsDocShellEditorData() { TearDownEditor(); }
+
+void nsDocShellEditorData::TearDownEditor() {
+ if (mHTMLEditor) {
+ RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor);
+ htmlEditor->PreDestroy();
+ }
+ mEditingSession = nullptr;
+ mIsDetached = false;
+}
+
+nsresult nsDocShellEditorData::MakeEditable(bool aInWaitForUriLoad) {
+ if (mMakeEditable) {
+ return NS_OK;
+ }
+
+ // if we are already editable, and are getting turned off,
+ // nuke the editor.
+ if (mHTMLEditor) {
+ NS_WARNING("Destroying existing editor on frame");
+
+ RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor);
+ htmlEditor->PreDestroy();
+ }
+
+ if (aInWaitForUriLoad) {
+ mMakeEditable = true;
+ }
+ return NS_OK;
+}
+
+bool nsDocShellEditorData::GetEditable() {
+ return mMakeEditable || (mHTMLEditor != nullptr);
+}
+
+nsEditingSession* nsDocShellEditorData::GetEditingSession() {
+ EnsureEditingSession();
+
+ return mEditingSession.get();
+}
+
+nsresult nsDocShellEditorData::SetHTMLEditor(HTMLEditor* aHTMLEditor) {
+ // destroy any editor that we have. Checks for equality are
+ // necessary to ensure that assigment into the nsCOMPtr does
+ // not temporarily reduce the refCount of the editor to zero
+ if (mHTMLEditor == aHTMLEditor) {
+ return NS_OK;
+ }
+
+ if (mHTMLEditor) {
+ RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor);
+ htmlEditor->PreDestroy();
+ MOZ_ASSERT(!mHTMLEditor,
+ "Nested call of nsDocShellEditorData::SetHTMLEditor() detected");
+ }
+
+ mHTMLEditor = aHTMLEditor; // owning addref
+ if (!mHTMLEditor) {
+ mMakeEditable = false;
+ }
+
+ return NS_OK;
+}
+
+// This creates the editing session on the content docShell that owns 'this'.
+void nsDocShellEditorData::EnsureEditingSession() {
+ NS_ASSERTION(mDocShell, "Should have docShell here");
+ NS_ASSERTION(!mIsDetached, "This will stomp editing session!");
+
+ if (!mEditingSession) {
+ mEditingSession = new nsEditingSession();
+ }
+}
+
+nsresult nsDocShellEditorData::DetachFromWindow() {
+ NS_ASSERTION(mEditingSession,
+ "Can't detach when we don't have a session to detach!");
+
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow =
+ mDocShell ? mDocShell->GetWindow() : nullptr;
+ nsresult rv = mEditingSession->DetachFromWindow(domWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mIsDetached = true;
+ mDetachedMakeEditable = mMakeEditable;
+ mMakeEditable = false;
+
+ nsCOMPtr<dom::Document> doc = domWindow->GetDoc();
+ mDetachedEditingState = doc->GetEditingState();
+
+ mDocShell = nullptr;
+
+ return NS_OK;
+}
+
+nsresult nsDocShellEditorData::ReattachToWindow(nsIDocShell* aDocShell) {
+ mDocShell = aDocShell;
+
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow =
+ mDocShell ? mDocShell->GetWindow() : nullptr;
+ nsresult rv = mEditingSession->ReattachToWindow(domWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mIsDetached = false;
+ mMakeEditable = mDetachedMakeEditable;
+
+ RefPtr<dom::Document> doc = domWindow->GetDoc();
+ doc->SetEditingState(mDetachedEditingState);
+
+ return NS_OK;
+}
diff --git a/docshell/base/nsDocShellEditorData.h b/docshell/base/nsDocShellEditorData.h
new file mode 100644
index 0000000000..27f840675b
--- /dev/null
+++ b/docshell/base/nsDocShellEditorData.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsDocShellEditorData_h__
+#define nsDocShellEditorData_h__
+
+#ifndef nsCOMPtr_h___
+# include "nsCOMPtr.h"
+#endif
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/Document.h"
+
+class nsIDocShell;
+class nsEditingSession;
+
+namespace mozilla {
+class HTMLEditor;
+}
+
+class nsDocShellEditorData {
+ public:
+ explicit nsDocShellEditorData(nsIDocShell* aOwningDocShell);
+ ~nsDocShellEditorData();
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult MakeEditable(bool aWaitForUriLoad);
+ bool GetEditable();
+ nsEditingSession* GetEditingSession();
+ mozilla::HTMLEditor* GetHTMLEditor() const { return mHTMLEditor; }
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+ SetHTMLEditor(mozilla::HTMLEditor* aHTMLEditor);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void TearDownEditor();
+ nsresult DetachFromWindow();
+ nsresult ReattachToWindow(nsIDocShell* aDocShell);
+ bool WaitingForLoad() const { return mMakeEditable; }
+
+ protected:
+ void EnsureEditingSession();
+
+ // The doc shell that owns us. Weak ref, since it always outlives us.
+ nsIDocShell* mDocShell;
+
+ // Only present for the content root docShell. Session is owned here.
+ RefPtr<nsEditingSession> mEditingSession;
+
+ // If this frame is editable, store HTML editor here. It's owned here.
+ RefPtr<mozilla::HTMLEditor> mHTMLEditor;
+
+ // Backup for the corresponding HTMLDocument's editing state while
+ // the editor is detached.
+ mozilla::dom::Document::EditingState mDetachedEditingState;
+
+ // Indicates whether to make an editor after a url load.
+ bool mMakeEditable;
+
+ // Denotes if the editor is detached from its window. The editor is detached
+ // while it's stored in the session history bfcache.
+ bool mIsDetached;
+
+ // Backup for mMakeEditable while the editor is detached.
+ bool mDetachedMakeEditable;
+};
+
+#endif // nsDocShellEditorData_h__
diff --git a/docshell/base/nsDocShellEnumerator.cpp b/docshell/base/nsDocShellEnumerator.cpp
new file mode 100644
index 0000000000..5ad0ad35e6
--- /dev/null
+++ b/docshell/base/nsDocShellEnumerator.cpp
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDocShellEnumerator.h"
+
+#include "nsDocShell.h"
+
+using namespace mozilla;
+
+nsDocShellEnumerator::nsDocShellEnumerator(
+ nsDocShellEnumerator::EnumerationDirection aDirection,
+ int32_t aDocShellType, nsDocShell& aRootItem)
+ : mRootItem(&aRootItem),
+ mDocShellType(aDocShellType),
+ mDirection(aDirection) {}
+
+nsresult nsDocShellEnumerator::BuildDocShellArray(
+ nsTArray<RefPtr<nsIDocShell>>& aItemArray) {
+ MOZ_ASSERT(mRootItem);
+
+ aItemArray.Clear();
+
+ if (mDirection == EnumerationDirection::Forwards) {
+ return BuildArrayRecursiveForwards(mRootItem, aItemArray);
+ }
+ MOZ_ASSERT(mDirection == EnumerationDirection::Backwards);
+ return BuildArrayRecursiveBackwards(mRootItem, aItemArray);
+}
+
+nsresult nsDocShellEnumerator::BuildArrayRecursiveForwards(
+ nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray) {
+ nsresult rv;
+
+ // add this item to the array
+ if (mDocShellType == nsIDocShellTreeItem::typeAll ||
+ aItem->ItemType() == mDocShellType) {
+ if (!aItemArray.AppendElement(aItem, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ int32_t numChildren = aItem->ChildCount();
+
+ for (int32_t i = 0; i < numChildren; ++i) {
+ RefPtr<nsDocShell> curChild = aItem->GetInProcessChildAt(i);
+ MOZ_ASSERT(curChild);
+
+ rv = BuildArrayRecursiveForwards(curChild, aItemArray);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShellEnumerator::BuildArrayRecursiveBackwards(
+ nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray) {
+ nsresult rv;
+
+ uint32_t numChildren = aItem->ChildCount();
+
+ for (int32_t i = numChildren - 1; i >= 0; --i) {
+ RefPtr<nsDocShell> curChild = aItem->GetInProcessChildAt(i);
+ MOZ_ASSERT(curChild);
+
+ rv = BuildArrayRecursiveBackwards(curChild, aItemArray);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // add this item to the array
+ if (mDocShellType == nsIDocShellTreeItem::typeAll ||
+ aItem->ItemType() == mDocShellType) {
+ if (!aItemArray.AppendElement(aItem, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/docshell/base/nsDocShellEnumerator.h b/docshell/base/nsDocShellEnumerator.h
new file mode 100644
index 0000000000..668ddee7e9
--- /dev/null
+++ b/docshell/base/nsDocShellEnumerator.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellEnumerator_h___
+#define nsDocShellEnumerator_h___
+
+#include "nsTArray.h"
+
+class nsDocShell;
+class nsIDocShell;
+
+class MOZ_STACK_CLASS nsDocShellEnumerator {
+ public:
+ enum class EnumerationDirection : uint8_t { Forwards, Backwards };
+
+ nsDocShellEnumerator(EnumerationDirection aDirection, int32_t aDocShellType,
+ nsDocShell& aRootItem);
+
+ public:
+ nsresult BuildDocShellArray(nsTArray<RefPtr<nsIDocShell>>& aItemArray);
+
+ private:
+ nsresult BuildArrayRecursiveForwards(
+ nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray);
+ nsresult BuildArrayRecursiveBackwards(
+ nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray);
+
+ private:
+ const RefPtr<nsDocShell> mRootItem;
+
+ const int32_t mDocShellType; // only want shells of this type
+
+ const EnumerationDirection mDirection;
+};
+
+#endif // nsDocShellEnumerator_h___
diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp
new file mode 100644
index 0000000000..587617e73d
--- /dev/null
+++ b/docshell/base/nsDocShellLoadState.cpp
@@ -0,0 +1,1325 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDocShellLoadState.h"
+#include "nsIDocShell.h"
+#include "nsDocShell.h"
+#include "nsIProtocolHandler.h"
+#include "nsISHEntry.h"
+#include "nsIURIFixup.h"
+#include "nsIWebNavigation.h"
+#include "nsIChannel.h"
+#include "nsIURLQueryStringStripper.h"
+#include "nsIXULRuntime.h"
+#include "nsNetUtil.h"
+#include "nsQueryObject.h"
+#include "ReferrerInfo.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/Telemetry.h"
+
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/StaticPtr.h"
+
+#include "mozilla/dom/PContent.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// Global reference to the URI fixup service.
+static mozilla::StaticRefPtr<nsIURIFixup> sURIFixup;
+
+nsDocShellLoadState::nsDocShellLoadState(nsIURI* aURI)
+ : nsDocShellLoadState(aURI, nsContentUtils::GenerateLoadIdentifier()) {}
+
+nsDocShellLoadState::nsDocShellLoadState(
+ const DocShellLoadStateInit& aLoadState, mozilla::ipc::IProtocol* aActor,
+ bool* aReadSuccess)
+ : mNotifiedBeforeUnloadListeners(false),
+ mLoadIdentifier(aLoadState.LoadIdentifier()) {
+ // If we return early, we failed to read in the data.
+ *aReadSuccess = false;
+ if (!aLoadState.URI()) {
+ MOZ_ASSERT_UNREACHABLE("Cannot create a LoadState with a null URI!");
+ return;
+ }
+
+ mResultPrincipalURI = aLoadState.ResultPrincipalURI();
+ mResultPrincipalURIIsSome = aLoadState.ResultPrincipalURIIsSome();
+ mKeepResultPrincipalURIIfSet = aLoadState.KeepResultPrincipalURIIfSet();
+ mLoadReplace = aLoadState.LoadReplace();
+ mInheritPrincipal = aLoadState.InheritPrincipal();
+ mPrincipalIsExplicit = aLoadState.PrincipalIsExplicit();
+ mForceAllowDataURI = aLoadState.ForceAllowDataURI();
+ mIsExemptFromHTTPSFirstMode = aLoadState.IsExemptFromHTTPSFirstMode();
+ mOriginalFrameSrc = aLoadState.OriginalFrameSrc();
+ mIsFormSubmission = aLoadState.IsFormSubmission();
+ mLoadType = aLoadState.LoadType();
+ mTarget = aLoadState.Target();
+ mTargetBrowsingContext = aLoadState.TargetBrowsingContext();
+ mLoadFlags = aLoadState.LoadFlags();
+ mInternalLoadFlags = aLoadState.InternalLoadFlags();
+ mFirstParty = aLoadState.FirstParty();
+ mHasValidUserGestureActivation = aLoadState.HasValidUserGestureActivation();
+ mAllowFocusMove = aLoadState.AllowFocusMove();
+ mTypeHint = aLoadState.TypeHint();
+ mFileName = aLoadState.FileName();
+ mIsFromProcessingFrameAttributes =
+ aLoadState.IsFromProcessingFrameAttributes();
+ mReferrerInfo = aLoadState.ReferrerInfo();
+ mURI = aLoadState.URI();
+ mOriginalURI = aLoadState.OriginalURI();
+ mSourceBrowsingContext = aLoadState.SourceBrowsingContext();
+ mBaseURI = aLoadState.BaseURI();
+ mTriggeringPrincipal = aLoadState.TriggeringPrincipal();
+ mPrincipalToInherit = aLoadState.PrincipalToInherit();
+ mPartitionedPrincipalToInherit = aLoadState.PartitionedPrincipalToInherit();
+ mTriggeringSandboxFlags = aLoadState.TriggeringSandboxFlags();
+ mTriggeringWindowId = aLoadState.TriggeringWindowId();
+ mTriggeringStorageAccess = aLoadState.TriggeringStorageAccess();
+ mTriggeringRemoteType = aLoadState.TriggeringRemoteType();
+ mWasSchemelessInput = aLoadState.WasSchemelessInput();
+ mCsp = aLoadState.Csp();
+ mOriginalURIString = aLoadState.OriginalURIString();
+ mCancelContentJSEpoch = aLoadState.CancelContentJSEpoch();
+ mPostDataStream = aLoadState.PostDataStream();
+ mHeadersStream = aLoadState.HeadersStream();
+ mSrcdocData = aLoadState.SrcdocData();
+ mChannelInitialized = aLoadState.ChannelInitialized();
+ mIsMetaRefresh = aLoadState.IsMetaRefresh();
+ if (aLoadState.loadingSessionHistoryInfo().isSome()) {
+ mLoadingSessionHistoryInfo = MakeUnique<LoadingSessionHistoryInfo>(
+ aLoadState.loadingSessionHistoryInfo().ref());
+ }
+ mUnstrippedURI = aLoadState.UnstrippedURI();
+ mRemoteTypeOverride = aLoadState.RemoteTypeOverride();
+
+ // We know this was created remotely, as we just received it over IPC.
+ mWasCreatedRemotely = true;
+
+ // If we're in the parent process, potentially validate against a LoadState
+ // which we sent to the source content process.
+ if (XRE_IsParentProcess()) {
+ mozilla::ipc::IToplevelProtocol* top = aActor->ToplevelProtocol();
+ if (!top ||
+ top->GetProtocolId() != mozilla::ipc::ProtocolId::PContentMsgStart ||
+ top->GetSide() != mozilla::ipc::ParentSide) {
+ aActor->FatalError("nsDocShellLoadState must be received over PContent");
+ return;
+ }
+ ContentParent* cp = static_cast<ContentParent*>(top);
+
+ // If this load was sent down to the content process as a navigation
+ // request, ensure it still matches the one we sent down.
+ if (RefPtr<nsDocShellLoadState> originalState =
+ cp->TakePendingLoadStateForId(mLoadIdentifier)) {
+ if (const char* mismatch = ValidateWithOriginalState(originalState)) {
+ aActor->FatalError(
+ nsPrintfCString(
+ "nsDocShellLoadState %s changed while in content process",
+ mismatch)
+ .get());
+ return;
+ }
+ } else if (mTriggeringRemoteType != cp->GetRemoteType()) {
+ // If we don't have a previous load to compare to, the content process
+ // must be the triggering process.
+ aActor->FatalError(
+ "nsDocShellLoadState with invalid triggering remote type");
+ return;
+ }
+ }
+
+ // We successfully read in the data - return a success value.
+ *aReadSuccess = true;
+}
+
+nsDocShellLoadState::nsDocShellLoadState(const nsDocShellLoadState& aOther)
+ : mReferrerInfo(aOther.mReferrerInfo),
+ mURI(aOther.mURI),
+ mOriginalURI(aOther.mOriginalURI),
+ mResultPrincipalURI(aOther.mResultPrincipalURI),
+ mResultPrincipalURIIsSome(aOther.mResultPrincipalURIIsSome),
+ mTriggeringPrincipal(aOther.mTriggeringPrincipal),
+ mTriggeringSandboxFlags(aOther.mTriggeringSandboxFlags),
+ mTriggeringWindowId(aOther.mTriggeringWindowId),
+ mTriggeringStorageAccess(aOther.mTriggeringStorageAccess),
+ mCsp(aOther.mCsp),
+ mKeepResultPrincipalURIIfSet(aOther.mKeepResultPrincipalURIIfSet),
+ mLoadReplace(aOther.mLoadReplace),
+ mInheritPrincipal(aOther.mInheritPrincipal),
+ mPrincipalIsExplicit(aOther.mPrincipalIsExplicit),
+ mNotifiedBeforeUnloadListeners(aOther.mNotifiedBeforeUnloadListeners),
+ mPrincipalToInherit(aOther.mPrincipalToInherit),
+ mPartitionedPrincipalToInherit(aOther.mPartitionedPrincipalToInherit),
+ mForceAllowDataURI(aOther.mForceAllowDataURI),
+ mIsExemptFromHTTPSFirstMode(aOther.mIsExemptFromHTTPSFirstMode),
+ mOriginalFrameSrc(aOther.mOriginalFrameSrc),
+ mIsFormSubmission(aOther.mIsFormSubmission),
+ mLoadType(aOther.mLoadType),
+ mSHEntry(aOther.mSHEntry),
+ mTarget(aOther.mTarget),
+ mTargetBrowsingContext(aOther.mTargetBrowsingContext),
+ mPostDataStream(aOther.mPostDataStream),
+ mHeadersStream(aOther.mHeadersStream),
+ mSrcdocData(aOther.mSrcdocData),
+ mSourceBrowsingContext(aOther.mSourceBrowsingContext),
+ mBaseURI(aOther.mBaseURI),
+ mLoadFlags(aOther.mLoadFlags),
+ mInternalLoadFlags(aOther.mInternalLoadFlags),
+ mFirstParty(aOther.mFirstParty),
+ mHasValidUserGestureActivation(aOther.mHasValidUserGestureActivation),
+ mAllowFocusMove(aOther.mAllowFocusMove),
+ mTypeHint(aOther.mTypeHint),
+ mFileName(aOther.mFileName),
+ mIsFromProcessingFrameAttributes(aOther.mIsFromProcessingFrameAttributes),
+ mPendingRedirectedChannel(aOther.mPendingRedirectedChannel),
+ mOriginalURIString(aOther.mOriginalURIString),
+ mCancelContentJSEpoch(aOther.mCancelContentJSEpoch),
+ mLoadIdentifier(aOther.mLoadIdentifier),
+ mChannelInitialized(aOther.mChannelInitialized),
+ mIsMetaRefresh(aOther.mIsMetaRefresh),
+ mWasCreatedRemotely(aOther.mWasCreatedRemotely),
+ mUnstrippedURI(aOther.mUnstrippedURI),
+ mRemoteTypeOverride(aOther.mRemoteTypeOverride),
+ mTriggeringRemoteType(aOther.mTriggeringRemoteType),
+ mWasSchemelessInput(aOther.mWasSchemelessInput) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ XRE_IsParentProcess(),
+ "Cloning a nsDocShellLoadState with the same load identifier is only "
+ "allowed in the parent process, as it could break triggering remote type "
+ "tracking in content.");
+ if (aOther.mLoadingSessionHistoryInfo) {
+ mLoadingSessionHistoryInfo = MakeUnique<LoadingSessionHistoryInfo>(
+ *aOther.mLoadingSessionHistoryInfo);
+ }
+}
+
+nsDocShellLoadState::nsDocShellLoadState(nsIURI* aURI, uint64_t aLoadIdentifier)
+ : mURI(aURI),
+ mResultPrincipalURIIsSome(false),
+ mTriggeringSandboxFlags(0),
+ mTriggeringWindowId(0),
+ mTriggeringStorageAccess(false),
+ mKeepResultPrincipalURIIfSet(false),
+ mLoadReplace(false),
+ mInheritPrincipal(false),
+ mPrincipalIsExplicit(false),
+ mNotifiedBeforeUnloadListeners(false),
+ mForceAllowDataURI(false),
+ mIsExemptFromHTTPSFirstMode(false),
+ mOriginalFrameSrc(false),
+ mIsFormSubmission(false),
+ mLoadType(LOAD_NORMAL),
+ mSrcdocData(VoidString()),
+ mLoadFlags(0),
+ mInternalLoadFlags(0),
+ mFirstParty(false),
+ mHasValidUserGestureActivation(false),
+ mAllowFocusMove(false),
+ mTypeHint(VoidCString()),
+ mFileName(VoidString()),
+ mIsFromProcessingFrameAttributes(false),
+ mLoadIdentifier(aLoadIdentifier),
+ mChannelInitialized(false),
+ mIsMetaRefresh(false),
+ mWasCreatedRemotely(false),
+ mTriggeringRemoteType(XRE_IsContentProcess()
+ ? ContentChild::GetSingleton()->GetRemoteType()
+ : NOT_REMOTE_TYPE),
+ mWasSchemelessInput(false) {
+ MOZ_ASSERT(aURI, "Cannot create a LoadState with a null URI!");
+}
+
+nsDocShellLoadState::~nsDocShellLoadState() {
+ if (mWasCreatedRemotely && XRE_IsContentProcess()) {
+ ContentChild::GetSingleton()->SendCleanupPendingLoadState(mLoadIdentifier);
+ }
+}
+
+nsresult nsDocShellLoadState::CreateFromPendingChannel(
+ nsIChannel* aPendingChannel, uint64_t aLoadIdentifier,
+ uint64_t aRegistrarId, nsDocShellLoadState** aResult) {
+ // Create the nsDocShellLoadState object with default state pulled from the
+ // passed-in channel.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aPendingChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsDocShellLoadState> loadState =
+ new nsDocShellLoadState(uri, aLoadIdentifier);
+ loadState->mPendingRedirectedChannel = aPendingChannel;
+ loadState->mChannelRegistrarId = aRegistrarId;
+
+ // Pull relevant state from the channel, and store it on the
+ // nsDocShellLoadState.
+ nsCOMPtr<nsIURI> originalUri;
+ rv = aPendingChannel->GetOriginalURI(getter_AddRefs(originalUri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ loadState->SetOriginalURI(originalUri);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aPendingChannel->LoadInfo();
+ loadState->SetTriggeringPrincipal(loadInfo->TriggeringPrincipal());
+
+ // Return the newly created loadState.
+ loadState.forget(aResult);
+ return NS_OK;
+}
+
+static uint32_t WebNavigationFlagsToFixupFlags(nsIURI* aURI,
+ const nsACString& aURIString,
+ uint32_t aNavigationFlags) {
+ if (aURI) {
+ aNavigationFlags &= ~nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ }
+ uint32_t fixupFlags = nsIURIFixup::FIXUP_FLAG_NONE;
+ if (aNavigationFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
+ fixupFlags |= nsIURIFixup::FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ }
+ if (aNavigationFlags & nsIWebNavigation::LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
+ fixupFlags |= nsIURIFixup::FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ }
+ return fixupFlags;
+};
+
+nsresult nsDocShellLoadState::CreateFromLoadURIOptions(
+ BrowsingContext* aBrowsingContext, const nsAString& aURI,
+ const LoadURIOptions& aLoadURIOptions, nsDocShellLoadState** aResult) {
+ uint32_t loadFlags = aLoadURIOptions.mLoadFlags;
+
+ NS_ASSERTION(
+ (loadFlags & nsDocShell::INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS) == 0,
+ "Unexpected flags");
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_OK;
+
+ NS_ConvertUTF16toUTF8 uriString(aURI);
+ // Cleanup the empty spaces that might be on each end.
+ uriString.Trim(" ");
+ // Eliminate embedded newlines, which single-line text fields now allow:
+ uriString.StripCRLF();
+ NS_ENSURE_TRUE(!uriString.IsEmpty(), NS_ERROR_FAILURE);
+
+ // Just create a URI and see what happens...
+ rv = NS_NewURI(getter_AddRefs(uri), uriString);
+ bool fixup = true;
+ if (NS_SUCCEEDED(rv) && uri &&
+ (uri->SchemeIs("about") || uri->SchemeIs("chrome"))) {
+ // Avoid third party fixup as a performance optimization.
+ loadFlags &= ~nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ fixup = false;
+ } else if (!sURIFixup && !XRE_IsContentProcess()) {
+ nsCOMPtr<nsIURIFixup> uriFixup = components::URIFixup::Service();
+ if (uriFixup) {
+ sURIFixup = uriFixup;
+ ClearOnShutdown(&sURIFixup);
+ } else {
+ fixup = false;
+ }
+ }
+
+ nsAutoString searchProvider, keyword;
+ RefPtr<nsIInputStream> fixupStream;
+ if (fixup) {
+ uint32_t fixupFlags =
+ WebNavigationFlagsToFixupFlags(uri, uriString, loadFlags);
+
+ // If we don't allow keyword lookups for this URL string, make sure to
+ // update loadFlags to indicate this as well.
+ if (!(fixupFlags & nsIURIFixup::FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP)) {
+ loadFlags &= ~nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ }
+ // Ensure URIFixup will use the right search engine in Private Browsing.
+ if (aBrowsingContext->UsePrivateBrowsing()) {
+ fixupFlags |= nsIURIFixup::FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+
+ if (!XRE_IsContentProcess()) {
+ nsCOMPtr<nsIURIFixupInfo> fixupInfo;
+ sURIFixup->GetFixupURIInfo(uriString, fixupFlags,
+ getter_AddRefs(fixupInfo));
+ if (fixupInfo) {
+ // We could fix the uri, clear NS_ERROR_MALFORMED_URI.
+ rv = NS_OK;
+ fixupInfo->GetPreferredURI(getter_AddRefs(uri));
+ fixupInfo->SetConsumer(aBrowsingContext);
+ fixupInfo->GetKeywordProviderName(searchProvider);
+ fixupInfo->GetKeywordAsSent(keyword);
+ // GetFixupURIInfo only returns a post data stream if it succeeded
+ // and changed the URI, in which case we should override the
+ // passed-in post data by passing this as an override arg to
+ // our internal method.
+ fixupInfo->GetPostData(getter_AddRefs(fixupStream));
+
+ if (fixupInfo &&
+ loadFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
+ nsCOMPtr<nsIObserverService> serv = services::GetObserverService();
+ if (serv) {
+ serv->NotifyObservers(fixupInfo, "keyword-uri-fixup",
+ PromiseFlatString(aURI).get());
+ }
+ }
+ nsDocShell::MaybeNotifyKeywordSearchLoading(searchProvider, keyword);
+ }
+ }
+ }
+
+ if (rv == NS_ERROR_MALFORMED_URI) {
+ MOZ_ASSERT(!uri);
+ return rv;
+ }
+
+ if (NS_FAILED(rv) || !uri) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsDocShellLoadState> loadState;
+ rv = CreateFromLoadURIOptions(
+ aBrowsingContext, uri, aLoadURIOptions, loadFlags,
+ fixupStream ? fixupStream : aLoadURIOptions.mPostData,
+ getter_AddRefs(loadState));
+ NS_ENSURE_SUCCESS(rv, rv);
+ loadState->SetOriginalURIString(uriString);
+ loadState.forget(aResult);
+ return NS_OK;
+}
+
+nsresult nsDocShellLoadState::CreateFromLoadURIOptions(
+ BrowsingContext* aBrowsingContext, nsIURI* aURI,
+ const LoadURIOptions& aLoadURIOptions, nsDocShellLoadState** aResult) {
+ return CreateFromLoadURIOptions(aBrowsingContext, aURI, aLoadURIOptions,
+ aLoadURIOptions.mLoadFlags,
+ aLoadURIOptions.mPostData, aResult);
+}
+
+nsresult nsDocShellLoadState::CreateFromLoadURIOptions(
+ BrowsingContext* aBrowsingContext, nsIURI* aURI,
+ const LoadURIOptions& aLoadURIOptions, uint32_t aLoadFlagsOverride,
+ nsIInputStream* aPostDataOverride, nsDocShellLoadState** aResult) {
+ nsresult rv = NS_OK;
+ uint32_t loadFlags = aLoadFlagsOverride;
+ RefPtr<nsIInputStream> postData = aPostDataOverride;
+ uint64_t available;
+ if (postData) {
+ rv = postData->Available(&available);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (available == 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ if (aLoadURIOptions.mHeaders) {
+ rv = aLoadURIOptions.mHeaders->Available(&available);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (available == 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ bool forceAllowDataURI =
+ loadFlags & nsIWebNavigation::LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
+
+ // Don't pass certain flags that aren't needed and end up confusing
+ // ConvertLoadTypeToDocShellInfoLoadType. We do need to ensure that they are
+ // passed to LoadURI though, since it uses them.
+ uint32_t extraFlags = (loadFlags & EXTRA_LOAD_FLAGS);
+ loadFlags &= ~EXTRA_LOAD_FLAGS;
+
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI);
+ loadState->SetReferrerInfo(aLoadURIOptions.mReferrerInfo);
+
+ loadState->SetLoadType(MAKE_LOAD_TYPE(LOAD_NORMAL, loadFlags));
+
+ loadState->SetLoadFlags(extraFlags);
+ loadState->SetFirstParty(true);
+ loadState->SetHasValidUserGestureActivation(
+ aLoadURIOptions.mHasValidUserGestureActivation);
+ loadState->SetTriggeringSandboxFlags(aLoadURIOptions.mTriggeringSandboxFlags);
+ loadState->SetTriggeringWindowId(aLoadURIOptions.mTriggeringWindowId);
+ loadState->SetTriggeringStorageAccess(
+ aLoadURIOptions.mTriggeringStorageAccess);
+ loadState->SetPostDataStream(postData);
+ loadState->SetHeadersStream(aLoadURIOptions.mHeaders);
+ loadState->SetBaseURI(aLoadURIOptions.mBaseURI);
+ loadState->SetTriggeringPrincipal(aLoadURIOptions.mTriggeringPrincipal);
+ loadState->SetCsp(aLoadURIOptions.mCsp);
+ loadState->SetForceAllowDataURI(forceAllowDataURI);
+ if (aLoadURIOptions.mCancelContentJSEpoch) {
+ loadState->SetCancelContentJSEpoch(aLoadURIOptions.mCancelContentJSEpoch);
+ }
+
+ if (aLoadURIOptions.mTriggeringRemoteType.WasPassed()) {
+ if (XRE_IsParentProcess()) {
+ loadState->SetTriggeringRemoteType(
+ aLoadURIOptions.mTriggeringRemoteType.Value());
+ } else if (ContentChild::GetSingleton()->GetRemoteType() !=
+ aLoadURIOptions.mTriggeringRemoteType.Value()) {
+ NS_WARNING("Invalid TriggeringRemoteType from LoadURIOptions in content");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ if (aLoadURIOptions.mRemoteTypeOverride.WasPassed()) {
+ loadState->SetRemoteTypeOverride(
+ aLoadURIOptions.mRemoteTypeOverride.Value());
+ }
+
+ loadState->SetWasSchemelessInput(aLoadURIOptions.mWasSchemelessInput);
+
+ loadState.forget(aResult);
+ return NS_OK;
+}
+
+nsIReferrerInfo* nsDocShellLoadState::GetReferrerInfo() const {
+ return mReferrerInfo;
+}
+
+void nsDocShellLoadState::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
+ mReferrerInfo = aReferrerInfo;
+}
+
+nsIURI* nsDocShellLoadState::URI() const { return mURI; }
+
+void nsDocShellLoadState::SetURI(nsIURI* aURI) { mURI = aURI; }
+
+nsIURI* nsDocShellLoadState::OriginalURI() const { return mOriginalURI; }
+
+void nsDocShellLoadState::SetOriginalURI(nsIURI* aOriginalURI) {
+ mOriginalURI = aOriginalURI;
+}
+
+nsIURI* nsDocShellLoadState::ResultPrincipalURI() const {
+ return mResultPrincipalURI;
+}
+
+void nsDocShellLoadState::SetResultPrincipalURI(nsIURI* aResultPrincipalURI) {
+ mResultPrincipalURI = aResultPrincipalURI;
+}
+
+bool nsDocShellLoadState::ResultPrincipalURIIsSome() const {
+ return mResultPrincipalURIIsSome;
+}
+
+void nsDocShellLoadState::SetResultPrincipalURIIsSome(bool aIsSome) {
+ mResultPrincipalURIIsSome = aIsSome;
+}
+
+bool nsDocShellLoadState::KeepResultPrincipalURIIfSet() const {
+ return mKeepResultPrincipalURIIfSet;
+}
+
+void nsDocShellLoadState::SetKeepResultPrincipalURIIfSet(bool aKeep) {
+ mKeepResultPrincipalURIIfSet = aKeep;
+}
+
+bool nsDocShellLoadState::LoadReplace() const { return mLoadReplace; }
+
+void nsDocShellLoadState::SetLoadReplace(bool aLoadReplace) {
+ mLoadReplace = aLoadReplace;
+}
+
+nsIPrincipal* nsDocShellLoadState::TriggeringPrincipal() const {
+ return mTriggeringPrincipal;
+}
+
+void nsDocShellLoadState::SetTriggeringPrincipal(
+ nsIPrincipal* aTriggeringPrincipal) {
+ mTriggeringPrincipal = aTriggeringPrincipal;
+}
+
+nsIPrincipal* nsDocShellLoadState::PrincipalToInherit() const {
+ return mPrincipalToInherit;
+}
+
+void nsDocShellLoadState::SetPrincipalToInherit(
+ nsIPrincipal* aPrincipalToInherit) {
+ mPrincipalToInherit = aPrincipalToInherit;
+}
+
+nsIPrincipal* nsDocShellLoadState::PartitionedPrincipalToInherit() const {
+ return mPartitionedPrincipalToInherit;
+}
+
+void nsDocShellLoadState::SetPartitionedPrincipalToInherit(
+ nsIPrincipal* aPartitionedPrincipalToInherit) {
+ mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit;
+}
+
+void nsDocShellLoadState::SetCsp(nsIContentSecurityPolicy* aCsp) {
+ mCsp = aCsp;
+}
+
+nsIContentSecurityPolicy* nsDocShellLoadState::Csp() const { return mCsp; }
+
+void nsDocShellLoadState::SetTriggeringSandboxFlags(uint32_t flags) {
+ mTriggeringSandboxFlags = flags;
+}
+
+uint32_t nsDocShellLoadState::TriggeringSandboxFlags() const {
+ return mTriggeringSandboxFlags;
+}
+
+void nsDocShellLoadState::SetTriggeringWindowId(uint64_t aTriggeringWindowId) {
+ mTriggeringWindowId = aTriggeringWindowId;
+}
+
+uint64_t nsDocShellLoadState::TriggeringWindowId() const {
+ return mTriggeringWindowId;
+}
+
+void nsDocShellLoadState::SetTriggeringStorageAccess(
+ bool aTriggeringStorageAccess) {
+ mTriggeringStorageAccess = aTriggeringStorageAccess;
+}
+
+bool nsDocShellLoadState::TriggeringStorageAccess() const {
+ return mTriggeringStorageAccess;
+}
+
+bool nsDocShellLoadState::InheritPrincipal() const { return mInheritPrincipal; }
+
+void nsDocShellLoadState::SetInheritPrincipal(bool aInheritPrincipal) {
+ mInheritPrincipal = aInheritPrincipal;
+}
+
+bool nsDocShellLoadState::PrincipalIsExplicit() const {
+ return mPrincipalIsExplicit;
+}
+
+void nsDocShellLoadState::SetPrincipalIsExplicit(bool aPrincipalIsExplicit) {
+ mPrincipalIsExplicit = aPrincipalIsExplicit;
+}
+
+bool nsDocShellLoadState::NotifiedBeforeUnloadListeners() const {
+ return mNotifiedBeforeUnloadListeners;
+}
+
+void nsDocShellLoadState::SetNotifiedBeforeUnloadListeners(
+ bool aNotifiedBeforeUnloadListeners) {
+ mNotifiedBeforeUnloadListeners = aNotifiedBeforeUnloadListeners;
+}
+
+bool nsDocShellLoadState::ForceAllowDataURI() const {
+ return mForceAllowDataURI;
+}
+
+void nsDocShellLoadState::SetForceAllowDataURI(bool aForceAllowDataURI) {
+ mForceAllowDataURI = aForceAllowDataURI;
+}
+
+bool nsDocShellLoadState::IsExemptFromHTTPSFirstMode() const {
+ return mIsExemptFromHTTPSFirstMode;
+}
+
+void nsDocShellLoadState::SetIsExemptFromHTTPSFirstMode(
+ bool aIsExemptFromHTTPSFirstMode) {
+ mIsExemptFromHTTPSFirstMode = aIsExemptFromHTTPSFirstMode;
+}
+
+bool nsDocShellLoadState::OriginalFrameSrc() const { return mOriginalFrameSrc; }
+
+void nsDocShellLoadState::SetOriginalFrameSrc(bool aOriginalFrameSrc) {
+ mOriginalFrameSrc = aOriginalFrameSrc;
+}
+
+bool nsDocShellLoadState::IsFormSubmission() const { return mIsFormSubmission; }
+
+void nsDocShellLoadState::SetIsFormSubmission(bool aIsFormSubmission) {
+ mIsFormSubmission = aIsFormSubmission;
+}
+
+uint32_t nsDocShellLoadState::LoadType() const { return mLoadType; }
+
+void nsDocShellLoadState::SetLoadType(uint32_t aLoadType) {
+ mLoadType = aLoadType;
+}
+
+nsISHEntry* nsDocShellLoadState::SHEntry() const { return mSHEntry; }
+
+void nsDocShellLoadState::SetSHEntry(nsISHEntry* aSHEntry) {
+ mSHEntry = aSHEntry;
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aSHEntry);
+ if (she) {
+ mLoadingSessionHistoryInfo = MakeUnique<LoadingSessionHistoryInfo>(she);
+ } else {
+ mLoadingSessionHistoryInfo = nullptr;
+ }
+}
+
+void nsDocShellLoadState::SetLoadingSessionHistoryInfo(
+ const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo) {
+ SetLoadingSessionHistoryInfo(
+ MakeUnique<mozilla::dom::LoadingSessionHistoryInfo>(aLoadingInfo));
+}
+
+void nsDocShellLoadState::SetLoadingSessionHistoryInfo(
+ mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> aLoadingInfo) {
+ mLoadingSessionHistoryInfo = std::move(aLoadingInfo);
+}
+
+const mozilla::dom::LoadingSessionHistoryInfo*
+nsDocShellLoadState::GetLoadingSessionHistoryInfo() const {
+ return mLoadingSessionHistoryInfo.get();
+}
+
+void nsDocShellLoadState::SetLoadIsFromSessionHistory(
+ int32_t aOffset, bool aLoadingCurrentEntry) {
+ if (mLoadingSessionHistoryInfo) {
+ mLoadingSessionHistoryInfo->mLoadIsFromSessionHistory = true;
+ mLoadingSessionHistoryInfo->mOffset = aOffset;
+ mLoadingSessionHistoryInfo->mLoadingCurrentEntry = aLoadingCurrentEntry;
+ }
+}
+
+void nsDocShellLoadState::ClearLoadIsFromSessionHistory() {
+ if (mLoadingSessionHistoryInfo) {
+ mLoadingSessionHistoryInfo->mLoadIsFromSessionHistory = false;
+ }
+ mSHEntry = nullptr;
+}
+
+bool nsDocShellLoadState::LoadIsFromSessionHistory() const {
+ return mLoadingSessionHistoryInfo
+ ? mLoadingSessionHistoryInfo->mLoadIsFromSessionHistory
+ : !!mSHEntry;
+}
+
+void nsDocShellLoadState::MaybeStripTrackerQueryStrings(
+ BrowsingContext* aContext) {
+ MOZ_ASSERT(aContext);
+
+ // Return early if the triggering principal doesn't exist. This could happen
+ // when loading a URL by using a browsing context in the Browser Toolbox.
+ if (!TriggeringPrincipal()) {
+ return;
+ }
+
+ // We don't need to strip for sub frames because the query string has been
+ // stripped in the top-level content. Also, we don't apply stripping if it
+ // is triggered by addons.
+ //
+ // Note that we don't need to do the stripping if the channel has been
+ // initialized. This means that this has been loaded speculatively in the
+ // parent process before and the stripping was happening by then.
+ if (GetChannelInitialized() || !aContext->IsTopContent() ||
+ BasePrincipal::Cast(TriggeringPrincipal())->AddonPolicy()) {
+ return;
+ }
+
+ // We don't strip the URI if it's the same-site navigation. Note that we will
+ // consider the system principal triggered load as third-party in case the
+ // user copies and pastes a URL which has tracking query parameters or an
+ // loading from external applications, such as clicking a link in an email
+ // client.
+ bool isThirdPartyURI = false;
+ if (!TriggeringPrincipal()->IsSystemPrincipal() &&
+ (NS_FAILED(
+ TriggeringPrincipal()->IsThirdPartyURI(URI(), &isThirdPartyURI)) ||
+ !isThirdPartyURI)) {
+ return;
+ }
+
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_QUERY_STRIPPING_COUNT::Navigation);
+
+ nsCOMPtr<nsIURI> strippedURI;
+
+ nsresult rv;
+ nsCOMPtr<nsIURLQueryStringStripper> queryStripper =
+ components::URLQueryStringStripper::Service(&rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ uint32_t numStripped;
+
+ queryStripper->Strip(URI(), aContext->UsePrivateBrowsing(),
+ getter_AddRefs(strippedURI), &numStripped);
+ if (numStripped) {
+ if (!mUnstrippedURI) {
+ mUnstrippedURI = URI();
+ }
+ SetURI(strippedURI);
+
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_QUERY_STRIPPING_COUNT::StripForNavigation);
+ Telemetry::Accumulate(Telemetry::QUERY_STRIPPING_PARAM_COUNT, numStripped);
+ }
+
+#ifdef DEBUG
+ // Make sure that unstripped URI is the same as URI() but only the query
+ // string could be different.
+ if (mUnstrippedURI) {
+ nsCOMPtr<nsIURI> uri;
+ Unused << queryStripper->Strip(mUnstrippedURI,
+ aContext->UsePrivateBrowsing(),
+ getter_AddRefs(uri), &numStripped);
+ bool equals = false;
+ Unused << URI()->Equals(uri, &equals);
+ MOZ_ASSERT(equals);
+ }
+#endif
+}
+
+const nsString& nsDocShellLoadState::Target() const { return mTarget; }
+
+void nsDocShellLoadState::SetTarget(const nsAString& aTarget) {
+ mTarget = aTarget;
+}
+
+nsIInputStream* nsDocShellLoadState::PostDataStream() const {
+ return mPostDataStream;
+}
+
+void nsDocShellLoadState::SetPostDataStream(nsIInputStream* aStream) {
+ mPostDataStream = aStream;
+}
+
+nsIInputStream* nsDocShellLoadState::HeadersStream() const {
+ return mHeadersStream;
+}
+
+void nsDocShellLoadState::SetHeadersStream(nsIInputStream* aHeadersStream) {
+ mHeadersStream = aHeadersStream;
+}
+
+const nsString& nsDocShellLoadState::SrcdocData() const { return mSrcdocData; }
+
+void nsDocShellLoadState::SetSrcdocData(const nsAString& aSrcdocData) {
+ mSrcdocData = aSrcdocData;
+}
+
+void nsDocShellLoadState::SetSourceBrowsingContext(
+ BrowsingContext* aSourceBrowsingContext) {
+ mSourceBrowsingContext = aSourceBrowsingContext;
+}
+
+void nsDocShellLoadState::SetTargetBrowsingContext(
+ BrowsingContext* aTargetBrowsingContext) {
+ mTargetBrowsingContext = aTargetBrowsingContext;
+}
+
+nsIURI* nsDocShellLoadState::BaseURI() const { return mBaseURI; }
+
+void nsDocShellLoadState::SetBaseURI(nsIURI* aBaseURI) { mBaseURI = aBaseURI; }
+
+void nsDocShellLoadState::GetMaybeResultPrincipalURI(
+ mozilla::Maybe<nsCOMPtr<nsIURI>>& aRPURI) const {
+ bool isSome = ResultPrincipalURIIsSome();
+ aRPURI.reset();
+
+ if (!isSome) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri = ResultPrincipalURI();
+ aRPURI.emplace(std::move(uri));
+}
+
+void nsDocShellLoadState::SetMaybeResultPrincipalURI(
+ mozilla::Maybe<nsCOMPtr<nsIURI>> const& aRPURI) {
+ SetResultPrincipalURI(aRPURI.refOr(nullptr));
+ SetResultPrincipalURIIsSome(aRPURI.isSome());
+}
+
+uint32_t nsDocShellLoadState::LoadFlags() const { return mLoadFlags; }
+
+void nsDocShellLoadState::SetLoadFlags(uint32_t aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+}
+
+void nsDocShellLoadState::SetLoadFlag(uint32_t aFlag) { mLoadFlags |= aFlag; }
+
+void nsDocShellLoadState::UnsetLoadFlag(uint32_t aFlag) {
+ mLoadFlags &= ~aFlag;
+}
+
+bool nsDocShellLoadState::HasLoadFlags(uint32_t aFlags) {
+ return (mLoadFlags & aFlags) == aFlags;
+}
+
+uint32_t nsDocShellLoadState::InternalLoadFlags() const {
+ return mInternalLoadFlags;
+}
+
+void nsDocShellLoadState::SetInternalLoadFlags(uint32_t aLoadFlags) {
+ mInternalLoadFlags = aLoadFlags;
+}
+
+void nsDocShellLoadState::SetInternalLoadFlag(uint32_t aFlag) {
+ mInternalLoadFlags |= aFlag;
+}
+
+void nsDocShellLoadState::UnsetInternalLoadFlag(uint32_t aFlag) {
+ mInternalLoadFlags &= ~aFlag;
+}
+
+bool nsDocShellLoadState::HasInternalLoadFlags(uint32_t aFlags) {
+ return (mInternalLoadFlags & aFlags) == aFlags;
+}
+
+bool nsDocShellLoadState::FirstParty() const { return mFirstParty; }
+
+void nsDocShellLoadState::SetFirstParty(bool aFirstParty) {
+ mFirstParty = aFirstParty;
+}
+
+bool nsDocShellLoadState::HasValidUserGestureActivation() const {
+ return mHasValidUserGestureActivation;
+}
+
+void nsDocShellLoadState::SetHasValidUserGestureActivation(
+ bool aHasValidUserGestureActivation) {
+ mHasValidUserGestureActivation = aHasValidUserGestureActivation;
+}
+
+const nsCString& nsDocShellLoadState::TypeHint() const { return mTypeHint; }
+
+void nsDocShellLoadState::SetTypeHint(const nsCString& aTypeHint) {
+ mTypeHint = aTypeHint;
+}
+
+const nsString& nsDocShellLoadState::FileName() const { return mFileName; }
+
+void nsDocShellLoadState::SetFileName(const nsAString& aFileName) {
+ MOZ_DIAGNOSTIC_ASSERT(aFileName.FindChar(char16_t(0)) == kNotFound,
+ "The filename should never contain null characters");
+ mFileName = aFileName;
+}
+
+const nsCString& nsDocShellLoadState::GetEffectiveTriggeringRemoteType() const {
+ // Consider non-errorpage loads from session history as being triggred by the
+ // parent process, as we'll validate them against the history entry.
+ //
+ // NOTE: Keep this check in-sync with the session-history validation check in
+ // `DocumentLoadListener::Open`!
+ if (LoadIsFromSessionHistory() && LoadType() != LOAD_ERROR_PAGE) {
+ return NOT_REMOTE_TYPE;
+ }
+ return mTriggeringRemoteType;
+}
+
+void nsDocShellLoadState::SetTriggeringRemoteType(
+ const nsACString& aTriggeringRemoteType) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), "only settable in parent");
+ mTriggeringRemoteType = aTriggeringRemoteType;
+}
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+void nsDocShellLoadState::AssertProcessCouldTriggerLoadIfSystem() {
+ // Early check to see if we're trying to start a file URI load with a system
+ // principal within a web content process.
+ // If this assertion fails, the load will fail later during
+ // nsContentSecurityManager checks, however this assertion should happen
+ // closer to whichever caller is triggering the system-principal load.
+ if (mozilla::SessionHistoryInParent() &&
+ TriggeringPrincipal()->IsSystemPrincipal() &&
+ mozilla::dom::IsWebRemoteType(GetEffectiveTriggeringRemoteType())) {
+ bool localFile = false;
+ if (NS_SUCCEEDED(NS_URIChainHasFlags(
+ URI(), nsIProtocolHandler::URI_IS_LOCAL_FILE, &localFile)) &&
+ localFile) {
+ NS_WARNING(nsPrintfCString("Unexpected system load of file URI (%s) from "
+ "web content process",
+ URI()->GetSpecOrDefault().get())
+ .get());
+ MOZ_CRASH("Unexpected system load of file URI from web content process");
+ }
+ }
+}
+#endif
+
+nsresult nsDocShellLoadState::SetupInheritingPrincipal(
+ BrowsingContext::Type aType,
+ const mozilla::OriginAttributes& aOriginAttributes) {
+ // We need a principalToInherit.
+ //
+ // If principalIsExplicit is not set there are 4 possibilities:
+ // (1) If the system principal or an expanded principal was passed
+ // in and we're a typeContent docshell, inherit the principal
+ // from the current document instead.
+ // (2) In all other cases when the principal passed in is not null,
+ // use that principal.
+ // (3) If the caller has allowed inheriting from the current document,
+ // or if we're being called from system code (eg chrome JS or pure
+ // C++) then inheritPrincipal should be true and InternalLoad will get
+ // a principal from the current document. If none of these things are
+ // true, then
+ // (4) we don't pass a principal into the channel, and a principal will be
+ // created later from the channel's internal data.
+ //
+ // If principalIsExplicit *is* set, there are 4 possibilities
+ // (1) If the system principal or an expanded principal was passed in
+ // and we're a typeContent docshell, return an error.
+ // (2) In all other cases when the principal passed in is not null,
+ // use that principal.
+ // (3) If the caller has allowed inheriting from the current document,
+ // then inheritPrincipal should be true and InternalLoad will get
+ // a principal from the current document. If none of these things are
+ // true, then
+ // (4) we dont' pass a principal into the channel, and a principal will be
+ // created later from the channel's internal data.
+ mPrincipalToInherit = mTriggeringPrincipal;
+ if (mPrincipalToInherit && aType != BrowsingContext::Type::Chrome) {
+ if (mPrincipalToInherit->IsSystemPrincipal()) {
+ if (mPrincipalIsExplicit) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ mPrincipalToInherit = nullptr;
+ mInheritPrincipal = true;
+ } else if (nsContentUtils::IsExpandedPrincipal(mPrincipalToInherit)) {
+ if (mPrincipalIsExplicit) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ // Don't inherit from the current page. Just do the safe thing
+ // and pretend that we were loaded by a nullprincipal.
+ //
+ // We didn't inherit OriginAttributes here as ExpandedPrincipal doesn't
+ // have origin attributes.
+ mPrincipalToInherit = NullPrincipal::Create(aOriginAttributes);
+ mInheritPrincipal = false;
+ }
+ }
+
+ if (!mPrincipalToInherit && !mInheritPrincipal && !mPrincipalIsExplicit) {
+ // See if there's system or chrome JS code running
+ mInheritPrincipal = nsContentUtils::LegacyIsCallerChromeOrNativeCode();
+ }
+
+ if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL) {
+ mInheritPrincipal = false;
+ // Create a new null principal URI based on our precursor principal.
+ nsCOMPtr<nsIURI> nullPrincipalURI =
+ NullPrincipal::CreateURI(mPrincipalToInherit);
+ // If mFirstParty is true and the pref 'privacy.firstparty.isolate' is
+ // enabled, we will set firstPartyDomain on the origin attributes.
+ OriginAttributes attrs(aOriginAttributes);
+ if (mFirstParty) {
+ attrs.SetFirstPartyDomain(true, nullPrincipalURI);
+ }
+ mPrincipalToInherit = NullPrincipal::Create(attrs, nullPrincipalURI);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocShellLoadState::SetupTriggeringPrincipal(
+ const mozilla::OriginAttributes& aOriginAttributes) {
+ // If the triggeringPrincipal is not set, we first try to create a principal
+ // from the referrer, since the referrer URI reflects the web origin that
+ // triggered the load. If there is no referrer URI, we fall back to using the
+ // SystemPrincipal. It's safe to assume that no provided triggeringPrincipal
+ // and no referrer simulate a load that was triggered by the system. It's
+ // important to note that this block of code needs to appear *after* the block
+ // where we munge the principalToInherit, because otherwise we would never
+ // enter code blocks checking if the principalToInherit is null and we will
+ // end up with a wrong inheritPrincipal flag.
+ if (!mTriggeringPrincipal) {
+ if (mReferrerInfo) {
+ nsCOMPtr<nsIURI> referrer = mReferrerInfo->GetOriginalReferrer();
+ mTriggeringPrincipal =
+ BasePrincipal::CreateContentPrincipal(referrer, aOriginAttributes);
+
+ if (!mTriggeringPrincipal) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ mTriggeringPrincipal = nsContentUtils::GetSystemPrincipal();
+ }
+ }
+ return NS_OK;
+}
+
+void nsDocShellLoadState::CalculateLoadURIFlags() {
+ if (mInheritPrincipal) {
+ MOZ_ASSERT(
+ !mPrincipalToInherit || !mPrincipalToInherit->IsSystemPrincipal(),
+ "Should not inherit SystemPrincipal");
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL;
+ }
+
+ if (mReferrerInfo && !mReferrerInfo->GetSendReferrer()) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER;
+ }
+ if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
+ mInternalLoadFlags |=
+ nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ }
+
+ if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FIRST_LOAD;
+ }
+
+ if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CLASSIFIER) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER;
+ }
+
+ if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_FORCE_ALLOW_COOKIES) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES;
+ }
+
+ if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE) {
+ mInternalLoadFlags |=
+ nsDocShell::INTERNAL_LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE;
+ }
+
+ if (!mSrcdocData.IsVoid()) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_IS_SRCDOC;
+ }
+
+ if (mForceAllowDataURI) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
+ }
+
+ if (mOriginalFrameSrc) {
+ mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC;
+ }
+}
+
+nsLoadFlags nsDocShellLoadState::CalculateChannelLoadFlags(
+ BrowsingContext* aBrowsingContext, Maybe<bool> aUriModified,
+ Maybe<bool> aIsEmbeddingBlockedError) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ nsLoadFlags loadFlags = aBrowsingContext->GetDefaultLoadFlags();
+
+ if (FirstParty()) {
+ // tag first party URL loads
+ loadFlags |= nsIChannel::LOAD_INITIAL_DOCUMENT_URI;
+ }
+
+ const uint32_t loadType = LoadType();
+
+ // These values aren't available for loads initiated in the Parent process.
+ MOZ_ASSERT_IF(loadType == LOAD_HISTORY, aUriModified.isSome());
+ MOZ_ASSERT_IF(loadType == LOAD_ERROR_PAGE, aIsEmbeddingBlockedError.isSome());
+
+ if (loadType == LOAD_ERROR_PAGE) {
+ // Error pages are LOAD_BACKGROUND, unless it's an
+ // XFO / frame-ancestors error for which we want an error page to load
+ // but additionally want the onload() event to fire.
+ if (!*aIsEmbeddingBlockedError) {
+ loadFlags |= nsIChannel::LOAD_BACKGROUND;
+ }
+ }
+
+ // Mark the channel as being a document URI and allow content sniffing...
+ loadFlags |=
+ nsIChannel::LOAD_DOCUMENT_URI | nsIChannel::LOAD_CALL_CONTENT_SNIFFERS;
+
+ if (nsDocShell::SandboxFlagsImplyCookies(
+ aBrowsingContext->GetSandboxFlags())) {
+ loadFlags |= nsIRequest::LOAD_DOCUMENT_NEEDS_COOKIE;
+ }
+
+ // Load attributes depend on load type...
+ switch (loadType) {
+ case LOAD_HISTORY: {
+ // Only send VALIDATE_NEVER if mLSHE's URI was never changed via
+ // push/replaceState (bug 669671).
+ if (!*aUriModified) {
+ loadFlags |= nsIRequest::VALIDATE_NEVER;
+ }
+ break;
+ }
+
+ case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE:
+ case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE:
+ loadFlags |=
+ nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::LOAD_FRESH_CONNECTION;
+ [[fallthrough]];
+
+ case LOAD_REFRESH:
+ loadFlags |= nsIRequest::VALIDATE_ALWAYS;
+ break;
+
+ case LOAD_NORMAL_BYPASS_CACHE:
+ case LOAD_NORMAL_BYPASS_PROXY:
+ case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE:
+ case LOAD_RELOAD_BYPASS_CACHE:
+ case LOAD_RELOAD_BYPASS_PROXY:
+ case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE:
+ case LOAD_REPLACE_BYPASS_CACHE:
+ loadFlags |=
+ nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::LOAD_FRESH_CONNECTION;
+ break;
+
+ case LOAD_RELOAD_NORMAL:
+ if (!StaticPrefs::
+ browser_soft_reload_only_force_validate_top_level_document()) {
+ loadFlags |= nsIRequest::VALIDATE_ALWAYS;
+ break;
+ }
+ [[fallthrough]];
+ case LOAD_NORMAL:
+ case LOAD_LINK:
+ // Set cache checking flags
+ switch (StaticPrefs::browser_cache_check_doc_frequency()) {
+ case 0:
+ loadFlags |= nsIRequest::VALIDATE_ONCE_PER_SESSION;
+ break;
+ case 1:
+ loadFlags |= nsIRequest::VALIDATE_ALWAYS;
+ break;
+ case 2:
+ loadFlags |= nsIRequest::VALIDATE_NEVER;
+ break;
+ }
+ break;
+ }
+
+ if (HasInternalLoadFlags(nsDocShell::INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER)) {
+ loadFlags |= nsIChannel::LOAD_BYPASS_URL_CLASSIFIER;
+ }
+
+ // If the user pressed shift-reload, then do not allow ServiceWorker
+ // interception to occur. See step 12.1 of the SW HandleFetch algorithm.
+ if (IsForceReloadType(loadType)) {
+ loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+ }
+
+ return loadFlags;
+}
+
+const char* nsDocShellLoadState::ValidateWithOriginalState(
+ nsDocShellLoadState* aOriginalState) {
+ MOZ_ASSERT(mLoadIdentifier == aOriginalState->mLoadIdentifier);
+
+ // Check that `aOriginalState` is sufficiently similar to this state that
+ // they're performing the same load.
+ auto uriEq = [](nsIURI* a, nsIURI* b) -> bool {
+ bool eq = false;
+ return a == b || (a && b && NS_SUCCEEDED(a->Equals(b, &eq)) && eq);
+ };
+ if (!uriEq(mURI, aOriginalState->mURI)) {
+ return "URI";
+ }
+ if (!uriEq(mUnstrippedURI, aOriginalState->mUnstrippedURI)) {
+ return "UnstrippedURI";
+ }
+ if (!uriEq(mOriginalURI, aOriginalState->mOriginalURI)) {
+ return "OriginalURI";
+ }
+ if (!uriEq(mBaseURI, aOriginalState->mBaseURI)) {
+ return "BaseURI";
+ }
+
+ if (!mTriggeringPrincipal->Equals(aOriginalState->mTriggeringPrincipal)) {
+ return "TriggeringPrincipal";
+ }
+ if (mTriggeringSandboxFlags != aOriginalState->mTriggeringSandboxFlags) {
+ return "TriggeringSandboxFlags";
+ }
+ if (mTriggeringRemoteType != aOriginalState->mTriggeringRemoteType) {
+ return "TriggeringRemoteType";
+ }
+
+ if (mOriginalURIString != aOriginalState->mOriginalURIString) {
+ return "OriginalURIString";
+ }
+
+ if (mRemoteTypeOverride != aOriginalState->mRemoteTypeOverride) {
+ return "RemoteTypeOverride";
+ }
+
+ if (mSourceBrowsingContext.ContextId() !=
+ aOriginalState->mSourceBrowsingContext.ContextId()) {
+ return "SourceBrowsingContext";
+ }
+
+ // FIXME: Consider calculating less information in the target process so that
+ // we can validate more properties more easily.
+ // FIXME: Identify what other flags will not change when sent through a
+ // content process.
+
+ return nullptr;
+}
+
+DocShellLoadStateInit nsDocShellLoadState::Serialize(
+ mozilla::ipc::IProtocol* aActor) {
+ MOZ_ASSERT(aActor);
+ DocShellLoadStateInit loadState;
+ loadState.ResultPrincipalURI() = mResultPrincipalURI;
+ loadState.ResultPrincipalURIIsSome() = mResultPrincipalURIIsSome;
+ loadState.KeepResultPrincipalURIIfSet() = mKeepResultPrincipalURIIfSet;
+ loadState.LoadReplace() = mLoadReplace;
+ loadState.InheritPrincipal() = mInheritPrincipal;
+ loadState.PrincipalIsExplicit() = mPrincipalIsExplicit;
+ loadState.ForceAllowDataURI() = mForceAllowDataURI;
+ loadState.IsExemptFromHTTPSFirstMode() = mIsExemptFromHTTPSFirstMode;
+ loadState.OriginalFrameSrc() = mOriginalFrameSrc;
+ loadState.IsFormSubmission() = mIsFormSubmission;
+ loadState.LoadType() = mLoadType;
+ loadState.Target() = mTarget;
+ loadState.TargetBrowsingContext() = mTargetBrowsingContext;
+ loadState.LoadFlags() = mLoadFlags;
+ loadState.InternalLoadFlags() = mInternalLoadFlags;
+ loadState.FirstParty() = mFirstParty;
+ loadState.HasValidUserGestureActivation() = mHasValidUserGestureActivation;
+ loadState.AllowFocusMove() = mAllowFocusMove;
+ loadState.TypeHint() = mTypeHint;
+ loadState.FileName() = mFileName;
+ loadState.IsFromProcessingFrameAttributes() =
+ mIsFromProcessingFrameAttributes;
+ loadState.URI() = mURI;
+ loadState.OriginalURI() = mOriginalURI;
+ loadState.SourceBrowsingContext() = mSourceBrowsingContext;
+ loadState.BaseURI() = mBaseURI;
+ loadState.TriggeringPrincipal() = mTriggeringPrincipal;
+ loadState.PrincipalToInherit() = mPrincipalToInherit;
+ loadState.PartitionedPrincipalToInherit() = mPartitionedPrincipalToInherit;
+ loadState.TriggeringSandboxFlags() = mTriggeringSandboxFlags;
+ loadState.TriggeringWindowId() = mTriggeringWindowId;
+ loadState.TriggeringStorageAccess() = mTriggeringStorageAccess;
+ loadState.TriggeringRemoteType() = mTriggeringRemoteType;
+ loadState.WasSchemelessInput() = mWasSchemelessInput;
+ loadState.Csp() = mCsp;
+ loadState.OriginalURIString() = mOriginalURIString;
+ loadState.CancelContentJSEpoch() = mCancelContentJSEpoch;
+ loadState.ReferrerInfo() = mReferrerInfo;
+ loadState.PostDataStream() = mPostDataStream;
+ loadState.HeadersStream() = mHeadersStream;
+ loadState.SrcdocData() = mSrcdocData;
+ loadState.ResultPrincipalURI() = mResultPrincipalURI;
+ loadState.LoadIdentifier() = mLoadIdentifier;
+ loadState.ChannelInitialized() = mChannelInitialized;
+ loadState.IsMetaRefresh() = mIsMetaRefresh;
+ if (mLoadingSessionHistoryInfo) {
+ loadState.loadingSessionHistoryInfo().emplace(*mLoadingSessionHistoryInfo);
+ }
+ loadState.UnstrippedURI() = mUnstrippedURI;
+ loadState.RemoteTypeOverride() = mRemoteTypeOverride;
+
+ if (XRE_IsParentProcess()) {
+ mozilla::ipc::IToplevelProtocol* top = aActor->ToplevelProtocol();
+ MOZ_RELEASE_ASSERT(top &&
+ top->GetProtocolId() ==
+ mozilla::ipc::ProtocolId::PContentMsgStart &&
+ top->GetSide() == mozilla::ipc::ParentSide,
+ "nsDocShellLoadState must be sent over PContent");
+ ContentParent* cp = static_cast<ContentParent*>(top);
+ cp->StorePendingLoadState(this);
+ }
+
+ return loadState;
+}
+
+nsIURI* nsDocShellLoadState::GetUnstrippedURI() const { return mUnstrippedURI; }
+
+void nsDocShellLoadState::SetUnstrippedURI(nsIURI* aUnstrippedURI) {
+ mUnstrippedURI = aUnstrippedURI;
+}
diff --git a/docshell/base/nsDocShellLoadState.h b/docshell/base/nsDocShellLoadState.h
new file mode 100644
index 0000000000..a34ca1b54b
--- /dev/null
+++ b/docshell/base/nsDocShellLoadState.h
@@ -0,0 +1,609 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellLoadState_h__
+#define nsDocShellLoadState_h__
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/SessionHistoryEntry.h"
+
+// Helper Classes
+#include "mozilla/Maybe.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsDocShellLoadTypes.h"
+#include "nsTArrayForwardDeclare.h"
+
+class nsIContentSecurityPolicy;
+class nsIInputStream;
+class nsISHEntry;
+class nsIURI;
+class nsIDocShell;
+class nsIChannel;
+class nsIReferrerInfo;
+namespace mozilla {
+class OriginAttributes;
+template <typename, class>
+class UniquePtr;
+namespace dom {
+class DocShellLoadStateInit;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * nsDocShellLoadState contains setup information used in a nsIDocShell::loadURI
+ * call.
+ */
+class nsDocShellLoadState final {
+ using BrowsingContext = mozilla::dom::BrowsingContext;
+ template <typename T>
+ using MaybeDiscarded = mozilla::dom::MaybeDiscarded<T>;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(nsDocShellLoadState);
+
+ explicit nsDocShellLoadState(nsIURI* aURI);
+ explicit nsDocShellLoadState(
+ const mozilla::dom::DocShellLoadStateInit& aLoadState,
+ mozilla::ipc::IProtocol* aActor, bool* aReadSuccess);
+ explicit nsDocShellLoadState(const nsDocShellLoadState& aOther);
+ nsDocShellLoadState(nsIURI* aURI, uint64_t aLoadIdentifier);
+
+ static nsresult CreateFromPendingChannel(nsIChannel* aPendingChannel,
+ uint64_t aLoadIdentifier,
+ uint64_t aRegistarId,
+ nsDocShellLoadState** aResult);
+
+ static nsresult CreateFromLoadURIOptions(
+ BrowsingContext* aBrowsingContext, const nsAString& aURI,
+ const mozilla::dom::LoadURIOptions& aLoadURIOptions,
+ nsDocShellLoadState** aResult);
+ static nsresult CreateFromLoadURIOptions(
+ BrowsingContext* aBrowsingContext, nsIURI* aURI,
+ const mozilla::dom::LoadURIOptions& aLoadURIOptions,
+ nsDocShellLoadState** aResult);
+
+ // Getters and Setters
+
+ nsIReferrerInfo* GetReferrerInfo() const;
+
+ void SetReferrerInfo(nsIReferrerInfo* aReferrerInfo);
+
+ nsIURI* URI() const;
+
+ void SetURI(nsIURI* aURI);
+
+ nsIURI* OriginalURI() const;
+
+ void SetOriginalURI(nsIURI* aOriginalURI);
+
+ nsIURI* ResultPrincipalURI() const;
+
+ void SetResultPrincipalURI(nsIURI* aResultPrincipalURI);
+
+ bool ResultPrincipalURIIsSome() const;
+
+ void SetResultPrincipalURIIsSome(bool aIsSome);
+
+ bool KeepResultPrincipalURIIfSet() const;
+
+ void SetKeepResultPrincipalURIIfSet(bool aKeep);
+
+ nsIPrincipal* PrincipalToInherit() const;
+
+ void SetPrincipalToInherit(nsIPrincipal* aPrincipalToInherit);
+
+ nsIPrincipal* PartitionedPrincipalToInherit() const;
+
+ void SetPartitionedPrincipalToInherit(
+ nsIPrincipal* aPartitionedPrincipalToInherit);
+
+ bool LoadReplace() const;
+
+ void SetLoadReplace(bool aLoadReplace);
+
+ nsIPrincipal* TriggeringPrincipal() const;
+
+ void SetTriggeringPrincipal(nsIPrincipal* aTriggeringPrincipal);
+
+ uint32_t TriggeringSandboxFlags() const;
+
+ void SetTriggeringSandboxFlags(uint32_t aTriggeringSandboxFlags);
+
+ uint64_t TriggeringWindowId() const;
+
+ void SetTriggeringWindowId(uint64_t aTriggeringWindowId);
+
+ bool TriggeringStorageAccess() const;
+
+ void SetTriggeringStorageAccess(bool aTriggeringStorageAccess);
+
+ nsIContentSecurityPolicy* Csp() const;
+
+ void SetCsp(nsIContentSecurityPolicy* aCsp);
+
+ bool InheritPrincipal() const;
+
+ void SetInheritPrincipal(bool aInheritPrincipal);
+
+ bool PrincipalIsExplicit() const;
+
+ void SetPrincipalIsExplicit(bool aPrincipalIsExplicit);
+
+ // If true, "beforeunload" event listeners were notified by the creater of the
+ // LoadState and given the chance to abort the navigation, and should not be
+ // notified again.
+ bool NotifiedBeforeUnloadListeners() const;
+
+ void SetNotifiedBeforeUnloadListeners(bool aNotifiedBeforeUnloadListeners);
+
+ bool ForceAllowDataURI() const;
+
+ void SetForceAllowDataURI(bool aForceAllowDataURI);
+
+ bool IsExemptFromHTTPSFirstMode() const;
+
+ void SetIsExemptFromHTTPSFirstMode(bool aIsExemptFromHTTPSFirstMode);
+
+ bool OriginalFrameSrc() const;
+
+ void SetOriginalFrameSrc(bool aOriginalFrameSrc);
+
+ bool IsFormSubmission() const;
+
+ void SetIsFormSubmission(bool aIsFormSubmission);
+
+ uint32_t LoadType() const;
+
+ void SetLoadType(uint32_t aLoadType);
+
+ nsISHEntry* SHEntry() const;
+
+ void SetSHEntry(nsISHEntry* aSHEntry);
+
+ const mozilla::dom::LoadingSessionHistoryInfo* GetLoadingSessionHistoryInfo()
+ const;
+
+ // Copies aLoadingInfo and stores the copy in this nsDocShellLoadState.
+ void SetLoadingSessionHistoryInfo(
+ const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo);
+
+ // Stores aLoadingInfo in this nsDocShellLoadState.
+ void SetLoadingSessionHistoryInfo(
+ mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> aLoadingInfo);
+
+ bool LoadIsFromSessionHistory() const;
+
+ const nsString& Target() const;
+
+ void SetTarget(const nsAString& aTarget);
+
+ nsIInputStream* PostDataStream() const;
+
+ void SetPostDataStream(nsIInputStream* aStream);
+
+ nsIInputStream* HeadersStream() const;
+
+ void SetHeadersStream(nsIInputStream* aHeadersStream);
+
+ bool IsSrcdocLoad() const;
+
+ const nsString& SrcdocData() const;
+
+ void SetSrcdocData(const nsAString& aSrcdocData);
+
+ const MaybeDiscarded<BrowsingContext>& SourceBrowsingContext() const {
+ return mSourceBrowsingContext;
+ }
+
+ void SetSourceBrowsingContext(BrowsingContext*);
+
+ void SetAllowFocusMove(bool aAllow) { mAllowFocusMove = aAllow; }
+
+ bool AllowFocusMove() const { return mAllowFocusMove; }
+
+ const MaybeDiscarded<BrowsingContext>& TargetBrowsingContext() const {
+ return mTargetBrowsingContext;
+ }
+
+ void SetTargetBrowsingContext(BrowsingContext* aTargetBrowsingContext);
+
+ nsIURI* BaseURI() const;
+
+ void SetBaseURI(nsIURI* aBaseURI);
+
+ // Helper function allowing convenient work with mozilla::Maybe in C++, hiding
+ // resultPrincipalURI and resultPrincipalURIIsSome attributes from the
+ // consumer.
+ void GetMaybeResultPrincipalURI(
+ mozilla::Maybe<nsCOMPtr<nsIURI>>& aRPURI) const;
+
+ void SetMaybeResultPrincipalURI(
+ mozilla::Maybe<nsCOMPtr<nsIURI>> const& aRPURI);
+
+ uint32_t LoadFlags() const;
+
+ void SetLoadFlags(uint32_t aFlags);
+
+ void SetLoadFlag(uint32_t aFlag);
+
+ void UnsetLoadFlag(uint32_t aFlag);
+
+ bool HasLoadFlags(uint32_t aFlag);
+
+ uint32_t InternalLoadFlags() const;
+
+ void SetInternalLoadFlags(uint32_t aFlags);
+
+ void SetInternalLoadFlag(uint32_t aFlag);
+
+ void UnsetInternalLoadFlag(uint32_t aFlag);
+
+ bool HasInternalLoadFlags(uint32_t aFlag);
+
+ bool FirstParty() const;
+
+ void SetFirstParty(bool aFirstParty);
+
+ bool HasValidUserGestureActivation() const;
+
+ void SetHasValidUserGestureActivation(bool HasValidUserGestureActivation);
+
+ const nsCString& TypeHint() const;
+
+ void SetTypeHint(const nsCString& aTypeHint);
+
+ const nsString& FileName() const;
+
+ void SetFileName(const nsAString& aFileName);
+
+ nsIURI* GetUnstrippedURI() const;
+
+ void SetUnstrippedURI(nsIURI* aUnstrippedURI);
+
+ // Give the type of DocShell we're loading into (chrome/content/etc) and
+ // origin attributes for the URI we're loading, figure out if we should
+ // inherit our principal from the document the load was requested from, or
+ // else if the principal should be set up later in the process (after loads).
+ // See comments in function for more info on principal selection algorithm
+ nsresult SetupInheritingPrincipal(
+ mozilla::dom::BrowsingContext::Type aType,
+ const mozilla::OriginAttributes& aOriginAttributes);
+
+ // If no triggering principal exists at the moment, create one using referrer
+ // information and origin attributes.
+ nsresult SetupTriggeringPrincipal(
+ const mozilla::OriginAttributes& aOriginAttributes);
+
+ void SetIsFromProcessingFrameAttributes() {
+ mIsFromProcessingFrameAttributes = true;
+ }
+ bool GetIsFromProcessingFrameAttributes() const {
+ return mIsFromProcessingFrameAttributes;
+ }
+
+ nsIChannel* GetPendingRedirectedChannel() {
+ return mPendingRedirectedChannel;
+ }
+
+ uint64_t GetPendingRedirectChannelRegistrarId() const {
+ return mChannelRegistrarId;
+ }
+
+ void SetOriginalURIString(const nsCString& aOriginalURI) {
+ mOriginalURIString.emplace(aOriginalURI);
+ }
+ const mozilla::Maybe<nsCString>& GetOriginalURIString() const {
+ return mOriginalURIString;
+ }
+
+ void SetCancelContentJSEpoch(int32_t aCancelEpoch) {
+ mCancelContentJSEpoch.emplace(aCancelEpoch);
+ }
+ const mozilla::Maybe<int32_t>& GetCancelContentJSEpoch() const {
+ return mCancelContentJSEpoch;
+ }
+
+ uint64_t GetLoadIdentifier() const { return mLoadIdentifier; }
+
+ void SetChannelInitialized(bool aInitilized) {
+ mChannelInitialized = aInitilized;
+ }
+
+ bool GetChannelInitialized() const { return mChannelInitialized; }
+
+ void SetIsMetaRefresh(bool aMetaRefresh) { mIsMetaRefresh = aMetaRefresh; }
+
+ bool IsMetaRefresh() const { return mIsMetaRefresh; }
+
+ const mozilla::Maybe<nsCString>& GetRemoteTypeOverride() const {
+ return mRemoteTypeOverride;
+ }
+
+ void SetRemoteTypeOverride(const nsCString& aRemoteTypeOverride) {
+ mRemoteTypeOverride = mozilla::Some(aRemoteTypeOverride);
+ }
+
+ void SetWasSchemelessInput(bool aWasSchemelessInput) {
+ mWasSchemelessInput = aWasSchemelessInput;
+ }
+
+ bool GetWasSchemelessInput() { return mWasSchemelessInput; }
+
+ // Determine the remote type of the process which should be considered
+ // responsible for this load for the purposes of security checks.
+ //
+ // This will generally be the process which created the nsDocShellLoadState
+ // originally, however non-errorpage history loads are always considered to be
+ // triggered by the parent process, as we can validate them against the
+ // history entry.
+ const nsCString& GetEffectiveTriggeringRemoteType() const;
+
+ void SetTriggeringRemoteType(const nsACString& aTriggeringRemoteType);
+
+ // Diagnostic assert if this is a system-principal triggered load, and it is
+ // trivial to determine that the effective triggering remote type would not be
+ // allowed to perform this load.
+ //
+ // This is called early during the load to crash as close to the cause as
+ // possible. See bug 1838686 for details.
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ void AssertProcessCouldTriggerLoadIfSystem();
+#else
+ void AssertProcessCouldTriggerLoadIfSystem() {}
+#endif
+
+ // When loading a document through nsDocShell::LoadURI(), a special set of
+ // flags needs to be set based on other values in nsDocShellLoadState. This
+ // function calculates those flags, before the LoadState is passed to
+ // nsDocShell::InternalLoad.
+ void CalculateLoadURIFlags();
+
+ // Compute the load flags to be used by creating channel. aUriModified and
+ // aIsEmbeddingBlockedError are expected to be Nothing when called from parent
+ // process.
+ nsLoadFlags CalculateChannelLoadFlags(
+ mozilla::dom::BrowsingContext* aBrowsingContext,
+ mozilla::Maybe<bool> aUriModified,
+ mozilla::Maybe<bool> aIsEmbeddingBlockedError);
+
+ mozilla::dom::DocShellLoadStateInit Serialize(
+ mozilla::ipc::IProtocol* aActor);
+
+ void SetLoadIsFromSessionHistory(int32_t aOffset, bool aLoadingCurrentEntry);
+ void ClearLoadIsFromSessionHistory();
+
+ void MaybeStripTrackerQueryStrings(mozilla::dom::BrowsingContext* aContext);
+
+ protected:
+ // Destructor can't be defaulted or inlined, as header doesn't have all type
+ // includes it needs to do so.
+ ~nsDocShellLoadState();
+
+ // Given the original `nsDocShellLoadState` which was sent to a content
+ // process, validate that they corespond to the same load.
+ // Returns a static (telemetry-safe) string naming what did not match, or
+ // nullptr if it succeeds.
+ const char* ValidateWithOriginalState(nsDocShellLoadState* aOriginalState);
+
+ static nsresult CreateFromLoadURIOptions(
+ BrowsingContext* aBrowsingContext, nsIURI* aURI,
+ const mozilla::dom::LoadURIOptions& aLoadURIOptions,
+ uint32_t aLoadFlagsOverride, nsIInputStream* aPostDataOverride,
+ nsDocShellLoadState** aResult);
+
+ // This is the referrer for the load.
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+
+ // The URI we are navigating to. Will not be null once set.
+ nsCOMPtr<nsIURI> mURI;
+
+ // The URI to set as the originalURI on the channel that does the load. If
+ // null, aURI will be set as the originalURI.
+ nsCOMPtr<nsIURI> mOriginalURI;
+
+ // The URI to be set to loadInfo.resultPrincipalURI
+ // - When Nothing, there will be no change
+ // - When Some, the principal URI will overwrite even
+ // with a null value.
+ //
+ // Valid only if mResultPrincipalURIIsSome is true (has the same meaning as
+ // isSome() on mozilla::Maybe.)
+ nsCOMPtr<nsIURI> mResultPrincipalURI;
+ bool mResultPrincipalURIIsSome;
+
+ // The principal of the load, that is, the entity responsible for causing the
+ // load to occur. In most cases the referrer and the triggeringPrincipal's URI
+ // will be identical.
+ //
+ // Please note that this is the principal that is used for security checks. If
+ // the argument aURI is provided by the web, then please do not pass a
+ // SystemPrincipal as the triggeringPrincipal.
+ nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
+
+ // The SandboxFlags of the load, that are, the SandboxFlags of the entity
+ // responsible for causing the load to occur. Most likely this are the
+ // SandboxFlags of the document that started the load.
+ uint32_t mTriggeringSandboxFlags;
+
+ // The window ID and current "has storage access" value of the entity
+ // triggering the load. This allows the identification of self-initiated
+ // same-origin navigations that should propogate unpartitioned storage access.
+ uint64_t mTriggeringWindowId;
+ bool mTriggeringStorageAccess;
+
+ // The CSP of the load, that is, the CSP of the entity responsible for causing
+ // the load to occur. Most likely this is the CSP of the document that started
+ // the load. In case the entity starting the load did not use a CSP, then mCsp
+ // can be null. Please note that this is also the CSP that will be applied to
+ // the load in case the load encounters a server side redirect.
+ nsCOMPtr<nsIContentSecurityPolicy> mCsp;
+
+ // If a refresh is caused by http-equiv="refresh" we want to set
+ // aResultPrincipalURI, but we do not want to overwrite the channel's
+ // ResultPrincipalURI, if it has already been set on the channel by a protocol
+ // handler.
+ bool mKeepResultPrincipalURIIfSet;
+
+ // If set LOAD_REPLACE flag will be set on the channel. If aOriginalURI is
+ // null, this argument is ignored.
+ bool mLoadReplace;
+
+ // If this attribute is true and no triggeringPrincipal is specified,
+ // copy the principal from the referring document.
+ bool mInheritPrincipal;
+
+ // If this attribute is true only ever use the principal specified
+ // by the triggeringPrincipal and inheritPrincipal attributes.
+ // If there are security reasons for why this is unsafe, such
+ // as trying to use a systemprincipal as the triggeringPrincipal
+ // for a content docshell the load fails.
+ bool mPrincipalIsExplicit;
+
+ bool mNotifiedBeforeUnloadListeners;
+
+ // Principal we're inheriting. If null, this means the principal should be
+ // inherited from the current document. If set to NullPrincipal, the channel
+ // will fill in principal information later in the load. See internal comments
+ // of SetupInheritingPrincipal for more info.
+ //
+ // When passed to InternalLoad, If this argument is null then
+ // principalToInherit is computed differently. See nsDocShell::InternalLoad
+ // for more comments.
+
+ nsCOMPtr<nsIPrincipal> mPrincipalToInherit;
+
+ nsCOMPtr<nsIPrincipal> mPartitionedPrincipalToInherit;
+
+ // If this attribute is true, then a top-level navigation
+ // to a data URI will be allowed.
+ bool mForceAllowDataURI;
+
+ // If this attribute is true, then the top-level navigaion
+ // will be exempt from HTTPS-Only-Mode upgrades.
+ bool mIsExemptFromHTTPSFirstMode;
+
+ // If this attribute is true, this load corresponds to a frame
+ // element loading its original src (or srcdoc) attribute.
+ bool mOriginalFrameSrc;
+
+ // If this attribute is true, then the load was initiated by a
+ // form submission.
+ bool mIsFormSubmission;
+
+ // Contains a load type as specified by the nsDocShellLoadTypes::load*
+ // constants
+ uint32_t mLoadType;
+
+ // Active Session History entry (if loading from SH)
+ nsCOMPtr<nsISHEntry> mSHEntry;
+
+ // Loading session history info for the load
+ mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo>
+ mLoadingSessionHistoryInfo;
+
+ // Target for load, like _content, _blank etc.
+ nsString mTarget;
+
+ // When set, this is the Target Browsing Context for the navigation
+ // after retargeting.
+ MaybeDiscarded<BrowsingContext> mTargetBrowsingContext;
+
+ // Post data stream (if POSTing)
+ nsCOMPtr<nsIInputStream> mPostDataStream;
+
+ // Additional Headers
+ nsCOMPtr<nsIInputStream> mHeadersStream;
+
+ // When set, the load will be interpreted as a srcdoc load, where contents of
+ // this string will be loaded instead of the URI. Setting srcdocData sets
+ // isSrcdocLoad to true
+ nsString mSrcdocData;
+
+ // When set, this is the Source Browsing Context for the navigation.
+ MaybeDiscarded<BrowsingContext> mSourceBrowsingContext;
+
+ // Used for srcdoc loads to give view-source knowledge of the load's base URI
+ // as this information isn't embedded in the load's URI.
+ nsCOMPtr<nsIURI> mBaseURI;
+
+ // Set of Load Flags, taken from nsDocShellLoadTypes.h and nsIWebNavigation
+ uint32_t mLoadFlags;
+
+ // Set of internal load flags
+ uint32_t mInternalLoadFlags;
+
+ // Is this a First Party Load?
+ bool mFirstParty;
+
+ // Is this load triggered by a user gesture?
+ bool mHasValidUserGestureActivation;
+
+ // Whether this load can steal the focus from the source browsing context.
+ bool mAllowFocusMove;
+
+ // A hint as to the content-type of the resulting data. If no hint, IsVoid()
+ // should return true.
+ nsCString mTypeHint;
+
+ // Non-void when the link should be downloaded as the given filename.
+ // mFileName being non-void but empty means that no filename hint was
+ // specified, but link should still trigger a download. If not a download,
+ // mFileName.IsVoid() should return true.
+ nsString mFileName;
+
+ // This will be true if this load is triggered by attribute changes.
+ // See nsILoadInfo.isFromProcessingFrameAttributes
+ bool mIsFromProcessingFrameAttributes;
+
+ // If set, a pending cross-process redirected channel should be used to
+ // perform the load. The channel will be stored in this value.
+ nsCOMPtr<nsIChannel> mPendingRedirectedChannel;
+
+ // An optional string representation of mURI, before any
+ // fixups were applied, so that we can send it to a search
+ // engine service if needed.
+ mozilla::Maybe<nsCString> mOriginalURIString;
+
+ // An optional value to pass to nsIDocShell::setCancelJSEpoch
+ // when initiating the load.
+ mozilla::Maybe<int32_t> mCancelContentJSEpoch;
+
+ // If mPendingRedirectChannel is set, then this is the identifier
+ // that the parent-process equivalent channel has been registered
+ // with using RedirectChannelRegistrar.
+ uint64_t mChannelRegistrarId;
+
+ // An identifier to make it possible to examine if two loads are
+ // equal, and which browsing context they belong to (see
+ // BrowsingContext::{Get, Set}CurrentLoadIdentifier)
+ const uint64_t mLoadIdentifier;
+
+ // Optional value to indicate that a channel has been
+ // pre-initialized in the parent process.
+ bool mChannelInitialized;
+
+ // True if the load was triggered by a meta refresh.
+ bool mIsMetaRefresh;
+
+ // True if the nsDocShellLoadState was received over IPC.
+ bool mWasCreatedRemotely = false;
+
+ // The original URI before query stripping happened. If it's present, it shows
+ // the query stripping happened. Otherwise, it will be a nullptr.
+ nsCOMPtr<nsIURI> mUnstrippedURI;
+
+ // If set, the remote type which the load should be completed within.
+ mozilla::Maybe<nsCString> mRemoteTypeOverride;
+
+ // Remote type of the process which originally requested the load.
+ nsCString mTriggeringRemoteType;
+
+ // if the to-be-loaded address had it protocol added through a fixup
+ bool mWasSchemelessInput = false;
+};
+
+#endif /* nsDocShellLoadState_h__ */
diff --git a/docshell/base/nsDocShellLoadTypes.h b/docshell/base/nsDocShellLoadTypes.h
new file mode 100644
index 0000000000..1de19e81eb
--- /dev/null
+++ b/docshell/base/nsDocShellLoadTypes.h
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellLoadTypes_h_
+#define nsDocShellLoadTypes_h_
+
+#ifdef MOZILLA_INTERNAL_API
+
+# include "nsDOMNavigationTiming.h"
+# include "nsIDocShell.h"
+# include "nsIWebNavigation.h"
+
+/**
+ * Load flag for error pages. This uses one of the reserved flag
+ * values from nsIWebNavigation.
+ */
+# define LOAD_FLAGS_ERROR_PAGE 0x0001U
+
+# define MAKE_LOAD_TYPE(type, flags) ((type) | ((flags) << 16))
+# define LOAD_TYPE_HAS_FLAGS(type, flags) ((type) & ((flags) << 16))
+
+/**
+ * These are flags that confuse ConvertLoadTypeToDocShellLoadInfo and should
+ * not be passed to MAKE_LOAD_TYPE. In particular this includes all flags
+ * above 0xffff (e.g. LOAD_FLAGS_BYPASS_CLASSIFIER), since MAKE_LOAD_TYPE would
+ * just shift them out anyway.
+ */
+# define EXTRA_LOAD_FLAGS \
+ (nsIWebNavigation::LOAD_FLAGS_FROM_EXTERNAL | \
+ nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD | \
+ nsIWebNavigation::LOAD_FLAGS_ALLOW_POPUPS | 0xffff0000)
+
+/* load types are legal combinations of load commands and flags
+ *
+ * NOTE:
+ * Remember to update the IsValidLoadType function below if you change this
+ * enum to ensure bad flag combinations will be rejected.
+ */
+enum LoadType : uint32_t {
+ LOAD_NORMAL = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_NONE),
+ LOAD_NORMAL_REPLACE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY),
+ LOAD_HISTORY = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_HISTORY,
+ nsIWebNavigation::LOAD_FLAGS_NONE),
+ LOAD_NORMAL_BYPASS_CACHE = MAKE_LOAD_TYPE(
+ nsIDocShell::LOAD_CMD_NORMAL, nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE),
+ LOAD_NORMAL_BYPASS_PROXY = MAKE_LOAD_TYPE(
+ nsIDocShell::LOAD_CMD_NORMAL, nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY),
+ LOAD_NORMAL_BYPASS_PROXY_AND_CACHE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE |
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY),
+ LOAD_RELOAD_NORMAL = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD,
+ nsIWebNavigation::LOAD_FLAGS_NONE),
+ LOAD_RELOAD_BYPASS_CACHE = MAKE_LOAD_TYPE(
+ nsIDocShell::LOAD_CMD_RELOAD, nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE),
+ LOAD_RELOAD_BYPASS_PROXY = MAKE_LOAD_TYPE(
+ nsIDocShell::LOAD_CMD_RELOAD, nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY),
+ LOAD_RELOAD_BYPASS_PROXY_AND_CACHE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD,
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE |
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY),
+ LOAD_LINK = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_IS_LINK),
+ LOAD_REFRESH = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_IS_REFRESH),
+ LOAD_REFRESH_REPLACE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_IS_REFRESH |
+ nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY),
+ LOAD_RELOAD_CHARSET_CHANGE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD,
+ nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE),
+ LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD,
+ nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE |
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE |
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY),
+ LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD,
+ nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE |
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE),
+ LOAD_BYPASS_HISTORY =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_HISTORY),
+ LOAD_STOP_CONTENT = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_STOP_CONTENT),
+ LOAD_STOP_CONTENT_AND_REPLACE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_STOP_CONTENT |
+ nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY),
+ LOAD_PUSHSTATE = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_PUSHSTATE,
+ nsIWebNavigation::LOAD_FLAGS_NONE),
+ LOAD_REPLACE_BYPASS_CACHE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL,
+ nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY |
+ nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE),
+ /**
+ * Load type for an error page. These loads are never triggered by users of
+ * Docshell. Instead, Docshell triggers the load itself when a
+ * consumer-triggered load failed.
+ */
+ LOAD_ERROR_PAGE =
+ MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, LOAD_FLAGS_ERROR_PAGE)
+
+ // NOTE: Adding a new value? Remember to update IsValidLoadType!
+};
+
+static inline bool IsForceReloadType(uint32_t aLoadType) {
+ switch (aLoadType) {
+ case LOAD_RELOAD_BYPASS_CACHE:
+ case LOAD_RELOAD_BYPASS_PROXY:
+ case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE:
+ return true;
+ }
+ return false;
+}
+
+static inline bool IsValidLoadType(uint32_t aLoadType) {
+ switch (aLoadType) {
+ case LOAD_NORMAL:
+ case LOAD_NORMAL_REPLACE:
+ case LOAD_NORMAL_BYPASS_CACHE:
+ case LOAD_NORMAL_BYPASS_PROXY:
+ case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE:
+ case LOAD_HISTORY:
+ case LOAD_RELOAD_NORMAL:
+ case LOAD_RELOAD_BYPASS_CACHE:
+ case LOAD_RELOAD_BYPASS_PROXY:
+ case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE:
+ case LOAD_LINK:
+ case LOAD_REFRESH:
+ case LOAD_REFRESH_REPLACE:
+ case LOAD_RELOAD_CHARSET_CHANGE:
+ case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE:
+ case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE:
+ case LOAD_BYPASS_HISTORY:
+ case LOAD_STOP_CONTENT:
+ case LOAD_STOP_CONTENT_AND_REPLACE:
+ case LOAD_PUSHSTATE:
+ case LOAD_REPLACE_BYPASS_CACHE:
+ case LOAD_ERROR_PAGE:
+ return true;
+ }
+ return false;
+}
+
+inline nsDOMNavigationTiming::Type ConvertLoadTypeToNavigationType(
+ uint32_t aLoadType) {
+ // Not initialized, assume it's normal load.
+ if (aLoadType == 0) {
+ aLoadType = LOAD_NORMAL;
+ }
+
+ auto result = nsDOMNavigationTiming::TYPE_RESERVED;
+ switch (aLoadType) {
+ case LOAD_NORMAL:
+ case LOAD_NORMAL_BYPASS_CACHE:
+ case LOAD_NORMAL_BYPASS_PROXY:
+ case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE:
+ case LOAD_NORMAL_REPLACE:
+ case LOAD_LINK:
+ case LOAD_STOP_CONTENT:
+ // FIXME: It isn't clear that LOAD_REFRESH_REPLACE should have a different
+ // navigation type than LOAD_REFRESH. Those loads historically used the
+ // LOAD_NORMAL_REPLACE type, and therefore wound up with TYPE_NAVIGATE by
+ // default.
+ case LOAD_REFRESH_REPLACE:
+ case LOAD_REPLACE_BYPASS_CACHE:
+ result = nsDOMNavigationTiming::TYPE_NAVIGATE;
+ break;
+ case LOAD_HISTORY:
+ result = nsDOMNavigationTiming::TYPE_BACK_FORWARD;
+ break;
+ case LOAD_RELOAD_NORMAL:
+ case LOAD_RELOAD_CHARSET_CHANGE:
+ case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE:
+ case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE:
+ case LOAD_RELOAD_BYPASS_CACHE:
+ case LOAD_RELOAD_BYPASS_PROXY:
+ case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE:
+ result = nsDOMNavigationTiming::TYPE_RELOAD;
+ break;
+ case LOAD_STOP_CONTENT_AND_REPLACE:
+ case LOAD_REFRESH:
+ case LOAD_BYPASS_HISTORY:
+ case LOAD_ERROR_PAGE:
+ case LOAD_PUSHSTATE:
+ result = nsDOMNavigationTiming::TYPE_RESERVED;
+ break;
+ default:
+ result = nsDOMNavigationTiming::TYPE_RESERVED;
+ break;
+ }
+
+ return result;
+}
+
+#endif // MOZILLA_INTERNAL_API
+#endif
diff --git a/docshell/base/nsDocShellTelemetryUtils.cpp b/docshell/base/nsDocShellTelemetryUtils.cpp
new file mode 100644
index 0000000000..bd4ed865bd
--- /dev/null
+++ b/docshell/base/nsDocShellTelemetryUtils.cpp
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDocShellTelemetryUtils.h"
+
+namespace {
+
+using ErrorLabel = mozilla::Telemetry::LABELS_PAGE_LOAD_ERROR;
+
+struct LoadErrorTelemetryResult {
+ nsresult mValue;
+ ErrorLabel mLabel;
+};
+
+static const LoadErrorTelemetryResult sResult[] = {
+ {
+ NS_ERROR_UNKNOWN_PROTOCOL,
+ ErrorLabel::UNKNOWN_PROTOCOL,
+ },
+ {
+ NS_ERROR_FILE_NOT_FOUND,
+ ErrorLabel::FILE_NOT_FOUND,
+ },
+ {
+ NS_ERROR_FILE_ACCESS_DENIED,
+ ErrorLabel::FILE_ACCESS_DENIED,
+ },
+ {
+ NS_ERROR_UNKNOWN_HOST,
+ ErrorLabel::UNKNOWN_HOST,
+ },
+ {
+ NS_ERROR_CONNECTION_REFUSED,
+ ErrorLabel::CONNECTION_REFUSED,
+ },
+ {
+ NS_ERROR_PROXY_BAD_GATEWAY,
+ ErrorLabel::PROXY_BAD_GATEWAY,
+ },
+ {
+ NS_ERROR_NET_INTERRUPT,
+ ErrorLabel::NET_INTERRUPT,
+ },
+ {
+ NS_ERROR_NET_TIMEOUT,
+ ErrorLabel::NET_TIMEOUT,
+ },
+ {
+ NS_ERROR_PROXY_GATEWAY_TIMEOUT,
+ ErrorLabel::P_GATEWAY_TIMEOUT,
+ },
+ {
+ NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION,
+ ErrorLabel::CSP_FRAME_ANCEST,
+ },
+ {
+ NS_ERROR_CSP_FORM_ACTION_VIOLATION,
+ ErrorLabel::CSP_FORM_ACTION,
+ },
+ {
+ NS_ERROR_XFO_VIOLATION,
+ ErrorLabel::XFO_VIOLATION,
+ },
+ {
+ NS_ERROR_PHISHING_URI,
+ ErrorLabel::PHISHING_URI,
+ },
+ {
+ NS_ERROR_MALWARE_URI,
+ ErrorLabel::MALWARE_URI,
+ },
+ {
+ NS_ERROR_UNWANTED_URI,
+ ErrorLabel::UNWANTED_URI,
+ },
+ {
+ NS_ERROR_HARMFUL_URI,
+ ErrorLabel::HARMFUL_URI,
+ },
+ {
+ NS_ERROR_CONTENT_CRASHED,
+ ErrorLabel::CONTENT_CRASHED,
+ },
+ {
+ NS_ERROR_FRAME_CRASHED,
+ ErrorLabel::FRAME_CRASHED,
+ },
+ {
+ NS_ERROR_BUILDID_MISMATCH,
+ ErrorLabel::BUILDID_MISMATCH,
+ },
+ {
+ NS_ERROR_NET_RESET,
+ ErrorLabel::NET_RESET,
+ },
+ {
+ NS_ERROR_MALFORMED_URI,
+ ErrorLabel::MALFORMED_URI,
+ },
+ {
+ NS_ERROR_REDIRECT_LOOP,
+ ErrorLabel::REDIRECT_LOOP,
+ },
+ {
+ NS_ERROR_UNKNOWN_SOCKET_TYPE,
+ ErrorLabel::UNKNOWN_SOCKET,
+ },
+ {
+ NS_ERROR_DOCUMENT_NOT_CACHED,
+ ErrorLabel::DOCUMENT_N_CACHED,
+ },
+ {
+ NS_ERROR_OFFLINE,
+ ErrorLabel::OFFLINE,
+ },
+ {
+ NS_ERROR_DOCUMENT_IS_PRINTMODE,
+ ErrorLabel::DOC_PRINTMODE,
+ },
+ {
+ NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
+ ErrorLabel::PORT_ACCESS,
+ },
+ {
+ NS_ERROR_UNKNOWN_PROXY_HOST,
+ ErrorLabel::UNKNOWN_PROXY_HOST,
+ },
+ {
+ NS_ERROR_PROXY_CONNECTION_REFUSED,
+ ErrorLabel::PROXY_CONNECTION,
+ },
+ {
+ NS_ERROR_PROXY_FORBIDDEN,
+ ErrorLabel::PROXY_FORBIDDEN,
+ },
+ {
+ NS_ERROR_PROXY_NOT_IMPLEMENTED,
+ ErrorLabel::P_NOT_IMPLEMENTED,
+ },
+ {
+ NS_ERROR_PROXY_AUTHENTICATION_FAILED,
+ ErrorLabel::PROXY_AUTH,
+ },
+ {
+ NS_ERROR_PROXY_TOO_MANY_REQUESTS,
+ ErrorLabel::PROXY_TOO_MANY,
+ },
+ {
+ NS_ERROR_INVALID_CONTENT_ENCODING,
+ ErrorLabel::CONTENT_ENCODING,
+ },
+ {
+ NS_ERROR_UNSAFE_CONTENT_TYPE,
+ ErrorLabel::UNSAFE_CONTENT,
+ },
+ {
+ NS_ERROR_CORRUPTED_CONTENT,
+ ErrorLabel::CORRUPTED_CONTENT,
+ },
+ {
+ NS_ERROR_INTERCEPTION_FAILED,
+ ErrorLabel::INTERCEPTION_FAIL,
+ },
+ {
+ NS_ERROR_NET_INADEQUATE_SECURITY,
+ ErrorLabel::INADEQUATE_SEC,
+ },
+ {
+ NS_ERROR_BLOCKED_BY_POLICY,
+ ErrorLabel::BLOCKED_BY_POLICY,
+ },
+ {
+ NS_ERROR_NET_HTTP2_SENT_GOAWAY,
+ ErrorLabel::HTTP2_SENT_GOAWAY,
+ },
+ {
+ NS_ERROR_NET_HTTP3_PROTOCOL_ERROR,
+ ErrorLabel::HTTP3_PROTOCOL,
+ },
+ {
+ NS_BINDING_FAILED,
+ ErrorLabel::BINDING_FAILED,
+ },
+};
+} // anonymous namespace
+
+namespace mozilla {
+namespace dom {
+mozilla::Telemetry::LABELS_PAGE_LOAD_ERROR LoadErrorToTelemetryLabel(
+ nsresult aRv) {
+ MOZ_ASSERT(aRv != NS_OK);
+
+ for (const auto& p : sResult) {
+ if (p.mValue == aRv) {
+ return p.mLabel;
+ }
+ }
+ return ErrorLabel::otherError;
+}
+} // namespace dom
+} // namespace mozilla
diff --git a/docshell/base/nsDocShellTelemetryUtils.h b/docshell/base/nsDocShellTelemetryUtils.h
new file mode 100644
index 0000000000..4e0097caec
--- /dev/null
+++ b/docshell/base/nsDocShellTelemetryUtils.h
@@ -0,0 +1,22 @@
+//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellTelemetryUtils_h__
+#define nsDocShellTelemetryUtils_h__
+
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+namespace dom {
+/**
+ * Convert page load errors to telemetry labels
+ * Only select nsresults are converted, otherwise this function
+ * will return "errorOther", view the list of errors at
+ * docshell/base/nsDocShellTelemetryUtils.cpp.
+ */
+Telemetry::LABELS_PAGE_LOAD_ERROR LoadErrorToTelemetryLabel(nsresult aRv);
+} // namespace dom
+} // namespace mozilla
+#endif // nsDocShellTelemetryUtils_h__
diff --git a/docshell/base/nsDocShellTreeOwner.cpp b/docshell/base/nsDocShellTreeOwner.cpp
new file mode 100644
index 0000000000..9f1ab23a6c
--- /dev/null
+++ b/docshell/base/nsDocShellTreeOwner.cpp
@@ -0,0 +1,1337 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Local Includes
+#include "nsDocShellTreeOwner.h"
+#include "nsWebBrowser.h"
+
+// Helper Classes
+#include "nsContentUtils.h"
+#include "nsSize.h"
+#include "mozilla/ReflowInput.h"
+#include "mozilla/ScopeExit.h"
+#include "nsComponentManagerUtils.h"
+#include "nsString.h"
+#include "nsAtom.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "mozilla/LookAndFeel.h"
+
+// Interfaces needed to be included
+#include "nsPresContext.h"
+#include "nsITooltipListener.h"
+#include "nsINode.h"
+#include "Link.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/SVGTitleElement.h"
+#include "nsIFormControl.h"
+#include "nsIWebNavigation.h"
+#include "nsPIDOMWindow.h"
+#include "nsPIWindowRoot.h"
+#include "nsIWindowWatcher.h"
+#include "nsPIWindowWatcher.h"
+#include "nsIPrompt.h"
+#include "nsIRemoteTab.h"
+#include "nsIBrowserChild.h"
+#include "nsRect.h"
+#include "nsIWebBrowserChromeFocus.h"
+#include "nsIContent.h"
+#include "nsServiceManagerUtils.h"
+#include "nsViewManager.h"
+#include "nsView.h"
+#include "nsXULTooltipListener.h"
+#include "nsIConstraintValidation.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/Try.h"
+#include "mozilla/dom/DragEvent.h"
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/File.h" // for input type=file
+#include "mozilla/dom/FileList.h" // for input type=file
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TextEvents.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// A helper routine that navigates the tricky path from a |nsWebBrowser| to
+// a |EventTarget| via the window root and chrome event handler.
+static nsresult GetDOMEventTarget(nsWebBrowser* aInBrowser,
+ EventTarget** aTarget) {
+ if (!aInBrowser) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ aInBrowser->GetContentDOMWindow(getter_AddRefs(domWindow));
+ if (!domWindow) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto* outerWindow = nsPIDOMWindowOuter::From(domWindow);
+ nsPIDOMWindowOuter* rootWindow = outerWindow->GetPrivateRoot();
+ NS_ENSURE_TRUE(rootWindow, NS_ERROR_FAILURE);
+ nsCOMPtr<EventTarget> target = rootWindow->GetChromeEventHandler();
+ NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);
+ target.forget(aTarget);
+
+ return NS_OK;
+}
+
+nsDocShellTreeOwner::nsDocShellTreeOwner()
+ : mWebBrowser(nullptr),
+ mTreeOwner(nullptr),
+ mPrimaryContentShell(nullptr),
+ mWebBrowserChrome(nullptr),
+ mOwnerWin(nullptr),
+ mOwnerRequestor(nullptr) {}
+
+nsDocShellTreeOwner::~nsDocShellTreeOwner() { RemoveChromeListeners(); }
+
+NS_IMPL_ADDREF(nsDocShellTreeOwner)
+NS_IMPL_RELEASE(nsDocShellTreeOwner)
+
+NS_INTERFACE_MAP_BEGIN(nsDocShellTreeOwner)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDocShellTreeOwner)
+ NS_INTERFACE_MAP_ENTRY(nsIDocShellTreeOwner)
+ NS_INTERFACE_MAP_ENTRY(nsIBaseWindow)
+ NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor)
+ NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+// The class that listens to the chrome events and tells the embedding chrome to
+// show tooltips, as appropriate. Handles registering itself with the DOM with
+// AddChromeListeners() and removing itself with RemoveChromeListeners().
+class ChromeTooltipListener final : public nsIDOMEventListener {
+ protected:
+ virtual ~ChromeTooltipListener();
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ ChromeTooltipListener(nsWebBrowser* aInBrowser,
+ nsIWebBrowserChrome* aInChrome);
+
+ NS_DECL_NSIDOMEVENTLISTENER
+ NS_IMETHOD MouseMove(mozilla::dom::Event* aMouseEvent);
+
+ // Add/remove the relevant listeners, based on what interfaces the embedding
+ // chrome implements.
+ NS_IMETHOD AddChromeListeners();
+ NS_IMETHOD RemoveChromeListeners();
+
+ NS_IMETHOD HideTooltip();
+
+ bool WebProgressShowedTooltip(nsIWebProgress* aWebProgress);
+
+ private:
+ // pixel tolerance for mousemove event
+ static constexpr CSSIntCoord kTooltipMouseMoveTolerance = 7;
+
+ NS_IMETHOD AddTooltipListener();
+ NS_IMETHOD RemoveTooltipListener();
+
+ NS_IMETHOD ShowTooltip(int32_t aInXCoords, int32_t aInYCoords,
+ const nsAString& aInTipText,
+ const nsAString& aDirText);
+ nsITooltipTextProvider* GetTooltipTextProvider();
+
+ nsWebBrowser* mWebBrowser;
+ nsCOMPtr<mozilla::dom::EventTarget> mEventTarget;
+ nsCOMPtr<nsITooltipTextProvider> mTooltipTextProvider;
+
+ // This must be a strong ref in order to make sure we can hide the tooltip if
+ // the window goes away while we're displaying one. If we don't hold a strong
+ // ref, the chrome might have been disposed of before we get a chance to tell
+ // it, and no one would ever tell us of that fact.
+ nsCOMPtr<nsIWebBrowserChrome> mWebBrowserChrome;
+
+ bool mTooltipListenerInstalled;
+
+ nsCOMPtr<nsITimer> mTooltipTimer;
+ static void sTooltipCallback(nsITimer* aTimer, void* aListener);
+
+ // Mouse coordinates for last mousemove event we saw
+ CSSIntPoint mMouseClientPoint;
+
+ // Mouse coordinates for tooltip event
+ LayoutDeviceIntPoint mMouseScreenPoint;
+
+ bool mShowingTooltip;
+
+ bool mTooltipShownOnce;
+
+ // The string of text that we last displayed.
+ nsString mLastShownTooltipText;
+
+ nsWeakPtr mLastDocshell;
+
+ // The node hovered over that fired the timer. This may turn into the node
+ // that triggered the tooltip, but only if the timer ever gets around to
+ // firing. This is a strong reference, because the tooltip content can be
+ // destroyed while we're waiting for the tooltip to pop up, and we need to
+ // detect that. It's set only when the tooltip timer is created and launched.
+ // The timer must either fire or be cancelled (or possibly released?), and we
+ // release this reference in each of those cases. So we don't leak.
+ nsCOMPtr<nsINode> mPossibleTooltipNode;
+};
+
+//*****************************************************************************
+// nsDocShellTreeOwner::nsIInterfaceRequestor
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetInterface(const nsIID& aIID, void** aSink) {
+ NS_ENSURE_ARG_POINTER(aSink);
+
+ if (NS_SUCCEEDED(QueryInterface(aIID, aSink))) {
+ return NS_OK;
+ }
+
+ if (aIID.Equals(NS_GET_IID(nsIWebBrowserChromeFocus))) {
+ if (mWebBrowserChromeWeak != nullptr) {
+ return mWebBrowserChromeWeak->QueryReferent(aIID, aSink);
+ }
+ return mOwnerWin->QueryInterface(aIID, aSink);
+ }
+
+ if (aIID.Equals(NS_GET_IID(nsIPrompt))) {
+ nsCOMPtr<nsIPrompt> prompt;
+ EnsurePrompter();
+ prompt = mPrompter;
+ if (prompt) {
+ prompt.forget(aSink);
+ return NS_OK;
+ }
+ return NS_NOINTERFACE;
+ }
+
+ if (aIID.Equals(NS_GET_IID(nsIAuthPrompt))) {
+ nsCOMPtr<nsIAuthPrompt> prompt;
+ EnsureAuthPrompter();
+ prompt = mAuthPrompter;
+ if (prompt) {
+ prompt.forget(aSink);
+ return NS_OK;
+ }
+ return NS_NOINTERFACE;
+ }
+
+ nsCOMPtr<nsIInterfaceRequestor> req = GetOwnerRequestor();
+ if (req) {
+ return req->GetInterface(aIID, aSink);
+ }
+
+ return NS_NOINTERFACE;
+}
+
+//*****************************************************************************
+// nsDocShellTreeOwner::nsIDocShellTreeOwner
+//*****************************************************************************
+
+void nsDocShellTreeOwner::EnsurePrompter() {
+ if (mPrompter) {
+ return;
+ }
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch && mWebBrowser) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow));
+ if (domWindow) {
+ wwatch->GetNewPrompter(domWindow, getter_AddRefs(mPrompter));
+ }
+ }
+}
+
+void nsDocShellTreeOwner::EnsureAuthPrompter() {
+ if (mAuthPrompter) {
+ return;
+ }
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch && mWebBrowser) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow));
+ if (domWindow) {
+ wwatch->GetNewAuthPrompter(domWindow, getter_AddRefs(mAuthPrompter));
+ }
+ }
+}
+
+void nsDocShellTreeOwner::AddToWatcher() {
+ if (mWebBrowser) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow));
+ if (domWindow) {
+ nsCOMPtr<nsPIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch) {
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+ if (webBrowserChrome) {
+ wwatch->AddWindow(domWindow, webBrowserChrome);
+ }
+ }
+ }
+ }
+}
+
+void nsDocShellTreeOwner::RemoveFromWatcher() {
+ if (mWebBrowser) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow));
+ if (domWindow) {
+ nsCOMPtr<nsPIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch) {
+ wwatch->RemoveWindow(domWindow);
+ }
+ }
+ }
+}
+
+void nsDocShellTreeOwner::EnsureContentTreeOwner() {
+ if (mContentTreeOwner) {
+ return;
+ }
+
+ mContentTreeOwner = new nsDocShellTreeOwner();
+ nsCOMPtr<nsIWebBrowserChrome> browserChrome = GetWebBrowserChrome();
+ if (browserChrome) {
+ mContentTreeOwner->SetWebBrowserChrome(browserChrome);
+ }
+
+ if (mWebBrowser) {
+ mContentTreeOwner->WebBrowser(mWebBrowser);
+ }
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::ContentShellAdded(nsIDocShellTreeItem* aContentShell,
+ bool aPrimary) {
+ if (mTreeOwner) return mTreeOwner->ContentShellAdded(aContentShell, aPrimary);
+
+ EnsureContentTreeOwner();
+ aContentShell->SetTreeOwner(mContentTreeOwner);
+
+ if (aPrimary) {
+ mPrimaryContentShell = aContentShell;
+ mPrimaryRemoteTab = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::ContentShellRemoved(nsIDocShellTreeItem* aContentShell) {
+ if (mTreeOwner) {
+ return mTreeOwner->ContentShellRemoved(aContentShell);
+ }
+
+ if (mPrimaryContentShell == aContentShell) {
+ mPrimaryContentShell = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPrimaryContentShell(nsIDocShellTreeItem** aShell) {
+ NS_ENSURE_ARG_POINTER(aShell);
+
+ if (mTreeOwner) {
+ return mTreeOwner->GetPrimaryContentShell(aShell);
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> shell;
+ if (!mPrimaryRemoteTab) {
+ shell =
+ mPrimaryContentShell ? mPrimaryContentShell : mWebBrowser->mDocShell;
+ }
+ shell.forget(aShell);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::RemoteTabAdded(nsIRemoteTab* aTab, bool aPrimary) {
+ if (mTreeOwner) {
+ return mTreeOwner->RemoteTabAdded(aTab, aPrimary);
+ }
+
+ if (aPrimary) {
+ mPrimaryRemoteTab = aTab;
+ mPrimaryContentShell = nullptr;
+ } else if (mPrimaryRemoteTab == aTab) {
+ mPrimaryRemoteTab = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::RemoteTabRemoved(nsIRemoteTab* aTab) {
+ if (mTreeOwner) {
+ return mTreeOwner->RemoteTabRemoved(aTab);
+ }
+
+ if (aTab == mPrimaryRemoteTab) {
+ mPrimaryRemoteTab = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPrimaryRemoteTab(nsIRemoteTab** aTab) {
+ if (mTreeOwner) {
+ return mTreeOwner->GetPrimaryRemoteTab(aTab);
+ }
+
+ nsCOMPtr<nsIRemoteTab> tab = mPrimaryRemoteTab;
+ tab.forget(aTab);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPrimaryContentBrowsingContext(
+ mozilla::dom::BrowsingContext** aBc) {
+ if (mTreeOwner) {
+ return mTreeOwner->GetPrimaryContentBrowsingContext(aBc);
+ }
+ if (mPrimaryRemoteTab) {
+ return mPrimaryRemoteTab->GetBrowsingContext(aBc);
+ }
+ if (mPrimaryContentShell) {
+ return mPrimaryContentShell->GetBrowsingContextXPCOM(aBc);
+ }
+ if (mWebBrowser->mDocShell) {
+ return mWebBrowser->mDocShell->GetBrowsingContextXPCOM(aBc);
+ }
+ *aBc = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPrimaryContentSize(int32_t* aWidth, int32_t* aHeight) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetPrimaryContentSize(int32_t aWidth, int32_t aHeight) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetRootShellSize(int32_t* aWidth, int32_t* aHeight) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetRootShellSize(int32_t aWidth, int32_t aHeight) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SizeShellTo(nsIDocShellTreeItem* aShellItem, int32_t aCX,
+ int32_t aCY) {
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+
+ NS_ENSURE_STATE(mTreeOwner || webBrowserChrome);
+
+ if (nsCOMPtr<nsIDocShellTreeOwner> treeOwner = mTreeOwner) {
+ return treeOwner->SizeShellTo(aShellItem, aCX, aCY);
+ }
+
+ if (aShellItem == mWebBrowser->mDocShell) {
+ nsCOMPtr<nsIBrowserChild> browserChild =
+ do_QueryInterface(webBrowserChrome);
+ if (browserChild) {
+ // The XUL window to resize is in the parent process, but there we
+ // won't be able to get the size of aShellItem. We can ask the parent
+ // process to change our size instead.
+ nsCOMPtr<nsIBaseWindow> shellAsWin(do_QueryInterface(aShellItem));
+ NS_ENSURE_TRUE(shellAsWin, NS_ERROR_FAILURE);
+
+ LayoutDeviceIntSize shellSize;
+ shellAsWin->GetSize(&shellSize.width, &shellSize.height);
+ LayoutDeviceIntSize deltaSize = LayoutDeviceIntSize(aCX, aCY) - shellSize;
+
+ LayoutDeviceIntSize currentSize;
+ GetSize(&currentSize.width, &currentSize.height);
+
+ LayoutDeviceIntSize newSize = currentSize + deltaSize;
+ return SetSize(newSize.width, newSize.height, true);
+ }
+ // XXX: this is weird, but we used to call a method here
+ // (webBrowserChrome->SizeBrowserTo()) whose implementations all failed
+ // like this, so...
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("This is unimplemented, API should be cleaned up");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetPersistence(bool aPersistPosition, bool aPersistSize,
+ bool aPersistSizeMode) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPersistence(bool* aPersistPosition, bool* aPersistSize,
+ bool* aPersistSizeMode) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetTabCount(uint32_t* aResult) {
+ if (mTreeOwner) {
+ return mTreeOwner->GetTabCount(aResult);
+ }
+
+ *aResult = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetHasPrimaryContent(bool* aResult) {
+ *aResult = mPrimaryRemoteTab || mPrimaryContentShell;
+ return NS_OK;
+}
+
+//*****************************************************************************
+// nsDocShellTreeOwner::nsIBaseWindow
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::InitWindow(nativeWindow aParentNativeWindow,
+ nsIWidget* aParentWidget, int32_t aX,
+ int32_t aY, int32_t aCX, int32_t aCY) {
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::Destroy() {
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+ if (webBrowserChrome) {
+ // XXX: this is weird, but we used to call a method here
+ // (webBrowserChrome->DestroyBrowserWindow()) whose implementations all
+ // failed like this, so...
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return NS_ERROR_NULL_POINTER;
+}
+
+double nsDocShellTreeOwner::GetWidgetCSSToDeviceScale() {
+ return mWebBrowser ? mWebBrowser->GetWidgetCSSToDeviceScale() : 1.0;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetDevicePixelsPerDesktopPixel(double* aScale) {
+ if (mWebBrowser) {
+ return mWebBrowser->GetDevicePixelsPerDesktopPixel(aScale);
+ }
+
+ *aScale = 1.0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetPositionDesktopPix(int32_t aX, int32_t aY) {
+ if (mWebBrowser) {
+ nsresult rv = mWebBrowser->SetPositionDesktopPix(aX, aY);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ double scale = 1.0;
+ GetDevicePixelsPerDesktopPixel(&scale);
+ return SetPosition(NSToIntRound(aX * scale), NSToIntRound(aY * scale));
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetPosition(int32_t aX, int32_t aY) {
+ return SetDimensions(
+ {DimensionKind::Outer, Some(aX), Some(aY), Nothing(), Nothing()});
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPosition(int32_t* aX, int32_t* aY) {
+ return GetDimensions(DimensionKind::Outer, aX, aY, nullptr, nullptr);
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetSize(int32_t aCX, int32_t aCY, bool aRepaint) {
+ return SetDimensions(
+ {DimensionKind::Outer, Nothing(), Nothing(), Some(aCX), Some(aCY)});
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetSize(int32_t* aCX, int32_t* aCY) {
+ return GetDimensions(DimensionKind::Outer, nullptr, nullptr, aCX, aCY);
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetPositionAndSize(int32_t aX, int32_t aY, int32_t aCX,
+ int32_t aCY, uint32_t aFlags) {
+ return SetDimensions(
+ {DimensionKind::Outer, Some(aX), Some(aY), Some(aCX), Some(aCY)});
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aCX,
+ int32_t* aCY) {
+ return GetDimensions(DimensionKind::Outer, aX, aY, aCX, aCY);
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetDimensions(DimensionRequest&& aRequest) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->SetDimensions(std::move(aRequest));
+ }
+
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+ NS_ENSURE_STATE(webBrowserChrome);
+ return webBrowserChrome->SetDimensions(std::move(aRequest));
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetDimensions(DimensionKind aDimensionKind, int32_t* aX,
+ int32_t* aY, int32_t* aCX, int32_t* aCY) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->GetDimensions(aDimensionKind, aX, aY, aCX, aCY);
+ }
+
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+ NS_ENSURE_STATE(webBrowserChrome);
+ return webBrowserChrome->GetDimensions(aDimensionKind, aX, aY, aCX, aCY);
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::Repaint(bool aForce) { return NS_ERROR_NULL_POINTER; }
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetParentWidget(nsIWidget** aParentWidget) {
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetParentWidget(nsIWidget* aParentWidget) {
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetParentNativeWindow(nativeWindow* aParentNativeWindow) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->GetParentNativeWindow(aParentNativeWindow);
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetParentNativeWindow(nativeWindow aParentNativeWindow) {
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetNativeHandle(nsAString& aNativeHandle) {
+ // the nativeHandle should be accessed from nsIAppWindow
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetVisibility(bool* aVisibility) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->GetVisibility(aVisibility);
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetVisibility(bool aVisibility) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->SetVisibility(aVisibility);
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetEnabled(bool* aEnabled) {
+ NS_ENSURE_ARG_POINTER(aEnabled);
+ *aEnabled = true;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetEnabled(bool aEnabled) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetMainWidget(nsIWidget** aMainWidget) {
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::GetTitle(nsAString& aTitle) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->GetTitle(aTitle);
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetTitle(const nsAString& aTitle) {
+ nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin();
+ if (ownerWin) {
+ return ownerWin->SetTitle(aTitle);
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+//*****************************************************************************
+// nsDocShellTreeOwner::nsIWebProgressListener
+//*****************************************************************************
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::OnProgressChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ // In the absence of DOM document creation event, this method is the
+ // most convenient place to install the mouse listener on the
+ // DOM document.
+ return AddChromeListeners();
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::OnStateChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest,
+ uint32_t aProgressStateFlags,
+ nsresult aStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* aURI,
+ uint32_t aFlags) {
+ if (mChromeTooltipListener && aWebProgress &&
+ !(aFlags & nsIWebProgressListener::LOCATION_CHANGE_SAME_DOCUMENT) &&
+ mChromeTooltipListener->WebProgressShowedTooltip(aWebProgress)) {
+ mChromeTooltipListener->HideTooltip();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ return NS_OK;
+}
+
+//*****************************************************************************
+// nsDocShellTreeOwner: Accessors
+//*****************************************************************************
+
+void nsDocShellTreeOwner::WebBrowser(nsWebBrowser* aWebBrowser) {
+ if (!aWebBrowser) {
+ RemoveChromeListeners();
+ }
+ if (aWebBrowser != mWebBrowser) {
+ mPrompter = nullptr;
+ mAuthPrompter = nullptr;
+ }
+
+ mWebBrowser = aWebBrowser;
+
+ if (mContentTreeOwner) {
+ mContentTreeOwner->WebBrowser(aWebBrowser);
+ if (!aWebBrowser) {
+ mContentTreeOwner = nullptr;
+ }
+ }
+}
+
+nsWebBrowser* nsDocShellTreeOwner::WebBrowser() { return mWebBrowser; }
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner) {
+ if (aTreeOwner) {
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome(do_GetInterface(aTreeOwner));
+ NS_ENSURE_TRUE(webBrowserChrome, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_SUCCESS(SetWebBrowserChrome(webBrowserChrome),
+ NS_ERROR_INVALID_ARG);
+ mTreeOwner = aTreeOwner;
+ } else {
+ mTreeOwner = nullptr;
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+ if (!webBrowserChrome) {
+ NS_ENSURE_SUCCESS(SetWebBrowserChrome(nullptr), NS_ERROR_FAILURE);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::SetWebBrowserChrome(
+ nsIWebBrowserChrome* aWebBrowserChrome) {
+ if (!aWebBrowserChrome) {
+ mWebBrowserChrome = nullptr;
+ mOwnerWin = nullptr;
+ mOwnerRequestor = nullptr;
+ mWebBrowserChromeWeak = nullptr;
+ } else {
+ nsCOMPtr<nsISupportsWeakReference> supportsweak =
+ do_QueryInterface(aWebBrowserChrome);
+ if (supportsweak) {
+ supportsweak->GetWeakReference(getter_AddRefs(mWebBrowserChromeWeak));
+ } else {
+ nsCOMPtr<nsIBaseWindow> ownerWin(do_QueryInterface(aWebBrowserChrome));
+ nsCOMPtr<nsIInterfaceRequestor> requestor(
+ do_QueryInterface(aWebBrowserChrome));
+
+ // it's ok for ownerWin or requestor to be null.
+ mWebBrowserChrome = aWebBrowserChrome;
+ mOwnerWin = ownerWin;
+ mOwnerRequestor = requestor;
+ }
+ }
+
+ if (mContentTreeOwner) {
+ mContentTreeOwner->SetWebBrowserChrome(aWebBrowserChrome);
+ }
+
+ return NS_OK;
+}
+
+// Hook up things to the chrome like context menus and tooltips, if the chrome
+// has implemented the right interfaces.
+NS_IMETHODIMP
+nsDocShellTreeOwner::AddChromeListeners() {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+ if (!webBrowserChrome) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // install tooltips
+ if (!mChromeTooltipListener) {
+ nsCOMPtr<nsITooltipListener> tooltipListener(
+ do_QueryInterface(webBrowserChrome));
+ if (tooltipListener) {
+ mChromeTooltipListener =
+ new ChromeTooltipListener(mWebBrowser, webBrowserChrome);
+ rv = mChromeTooltipListener->AddChromeListeners();
+ }
+ }
+
+ nsCOMPtr<EventTarget> target;
+ GetDOMEventTarget(mWebBrowser, getter_AddRefs(target));
+
+ // register dragover and drop event listeners with the listener manager
+ MOZ_ASSERT(target, "how does this happen? (see bug 1659758)");
+ if (target) {
+ if (EventListenerManager* elmP = target->GetOrCreateListenerManager()) {
+ elmP->AddEventListenerByType(this, u"dragover"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this, u"drop"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::RemoveChromeListeners() {
+ if (mChromeTooltipListener) {
+ mChromeTooltipListener->RemoveChromeListeners();
+ mChromeTooltipListener = nullptr;
+ }
+
+ nsCOMPtr<EventTarget> piTarget;
+ GetDOMEventTarget(mWebBrowser, getter_AddRefs(piTarget));
+ if (!piTarget) {
+ return NS_OK;
+ }
+
+ EventListenerManager* elmP = piTarget->GetOrCreateListenerManager();
+ if (elmP) {
+ elmP->RemoveEventListenerByType(this, u"dragover"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this, u"drop"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShellTreeOwner::HandleEvent(Event* aEvent) {
+ DragEvent* dragEvent = aEvent ? aEvent->AsDragEvent() : nullptr;
+ if (NS_WARN_IF(!dragEvent)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (dragEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDroppedLinkHandler> handler =
+ do_GetService("@mozilla.org/content/dropped-link-handler;1");
+ if (!handler) {
+ return NS_OK;
+ }
+
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("dragover")) {
+ bool canDropLink = false;
+ handler->CanDropLink(dragEvent, false, &canDropLink);
+ if (canDropLink) {
+ aEvent->PreventDefault();
+ }
+ } else if (eventType.EqualsLiteral("drop")) {
+ nsCOMPtr<nsIWebNavigation> webnav =
+ static_cast<nsIWebNavigation*>(mWebBrowser);
+
+ // The page might have cancelled the dragover event itself, so check to
+ // make sure that the link can be dropped first.
+ bool canDropLink = false;
+ handler->CanDropLink(dragEvent, false, &canDropLink);
+ if (!canDropLink) {
+ return NS_OK;
+ }
+
+ nsTArray<RefPtr<nsIDroppedLinkItem>> links;
+ if (webnav && NS_SUCCEEDED(handler->DropLinks(dragEvent, true, links))) {
+ if (links.Length() >= 1) {
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ handler->GetTriggeringPrincipal(dragEvent,
+ getter_AddRefs(triggeringPrincipal));
+ if (triggeringPrincipal) {
+ nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome =
+ GetWebBrowserChrome();
+ if (webBrowserChrome) {
+ nsCOMPtr<nsIBrowserChild> browserChild =
+ do_QueryInterface(webBrowserChrome);
+ if (browserChild) {
+ nsresult rv = browserChild->RemoteDropLinks(links);
+ return rv;
+ }
+ }
+ nsAutoString url;
+ if (NS_SUCCEEDED(links[0]->GetUrl(url))) {
+ if (!url.IsEmpty()) {
+#ifndef ANDROID
+ MOZ_ASSERT(triggeringPrincipal,
+ "nsDocShellTreeOwner::HandleEvent: Need a valid "
+ "triggeringPrincipal");
+#endif
+ LoadURIOptions loadURIOptions;
+ loadURIOptions.mTriggeringPrincipal = triggeringPrincipal;
+ nsCOMPtr<nsIContentSecurityPolicy> csp;
+ handler->GetCsp(dragEvent, getter_AddRefs(csp));
+ loadURIOptions.mCsp = csp;
+ webnav->FixupAndLoadURIString(url, loadURIOptions);
+ }
+ }
+ }
+ }
+ } else {
+ aEvent->StopPropagation();
+ aEvent->PreventDefault();
+ }
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<nsIWebBrowserChrome>
+nsDocShellTreeOwner::GetWebBrowserChrome() {
+ nsCOMPtr<nsIWebBrowserChrome> chrome;
+ if (mWebBrowserChromeWeak) {
+ chrome = do_QueryReferent(mWebBrowserChromeWeak);
+ } else if (mWebBrowserChrome) {
+ chrome = mWebBrowserChrome;
+ }
+ return chrome.forget();
+}
+
+already_AddRefed<nsIBaseWindow> nsDocShellTreeOwner::GetOwnerWin() {
+ nsCOMPtr<nsIBaseWindow> win;
+ if (mWebBrowserChromeWeak) {
+ win = do_QueryReferent(mWebBrowserChromeWeak);
+ } else if (mOwnerWin) {
+ win = mOwnerWin;
+ }
+ return win.forget();
+}
+
+already_AddRefed<nsIInterfaceRequestor>
+nsDocShellTreeOwner::GetOwnerRequestor() {
+ nsCOMPtr<nsIInterfaceRequestor> req;
+ if (mWebBrowserChromeWeak) {
+ req = do_QueryReferent(mWebBrowserChromeWeak);
+ } else if (mOwnerRequestor) {
+ req = mOwnerRequestor;
+ }
+ return req.forget();
+}
+
+NS_IMPL_ISUPPORTS(ChromeTooltipListener, nsIDOMEventListener)
+
+ChromeTooltipListener::ChromeTooltipListener(nsWebBrowser* aInBrowser,
+ nsIWebBrowserChrome* aInChrome)
+ : mWebBrowser(aInBrowser),
+ mWebBrowserChrome(aInChrome),
+ mTooltipListenerInstalled(false),
+ mShowingTooltip(false),
+ mTooltipShownOnce(false) {}
+
+ChromeTooltipListener::~ChromeTooltipListener() {}
+
+nsITooltipTextProvider* ChromeTooltipListener::GetTooltipTextProvider() {
+ if (!mTooltipTextProvider) {
+ mTooltipTextProvider = do_GetService(NS_TOOLTIPTEXTPROVIDER_CONTRACTID);
+ }
+
+ if (!mTooltipTextProvider) {
+ mTooltipTextProvider =
+ do_GetService(NS_DEFAULTTOOLTIPTEXTPROVIDER_CONTRACTID);
+ }
+
+ return mTooltipTextProvider;
+}
+
+// Hook up things to the chrome like context menus and tooltips, if the chrome
+// has implemented the right interfaces.
+NS_IMETHODIMP
+ChromeTooltipListener::AddChromeListeners() {
+ if (!mEventTarget) {
+ GetDOMEventTarget(mWebBrowser, getter_AddRefs(mEventTarget));
+ }
+
+ // Register the appropriate events for tooltips, but only if
+ // the embedding chrome cares.
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsITooltipListener> tooltipListener(
+ do_QueryInterface(mWebBrowserChrome));
+ if (tooltipListener && !mTooltipListenerInstalled) {
+ rv = AddTooltipListener();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return rv;
+}
+
+// Subscribe to the events that will allow us to track tooltips. We need "mouse"
+// for mouseExit, "mouse motion" for mouseMove, and "key" for keyDown. As we
+// add the listeners, keep track of how many succeed so we can clean up
+// correctly in Release().
+NS_IMETHODIMP
+ChromeTooltipListener::AddTooltipListener() {
+ if (mEventTarget) {
+ MOZ_TRY(mEventTarget->AddSystemEventListener(u"keydown"_ns, this, false,
+ false));
+ MOZ_TRY(mEventTarget->AddSystemEventListener(u"mousedown"_ns, this, false,
+ false));
+ MOZ_TRY(mEventTarget->AddSystemEventListener(u"mouseout"_ns, this, false,
+ false));
+ MOZ_TRY(mEventTarget->AddSystemEventListener(u"mousemove"_ns, this, false,
+ false));
+
+ mTooltipListenerInstalled = true;
+ }
+
+ return NS_OK;
+}
+
+// Unsubscribe from the various things we've hooked up to the window root.
+NS_IMETHODIMP
+ChromeTooltipListener::RemoveChromeListeners() {
+ HideTooltip();
+
+ if (mTooltipListenerInstalled) {
+ RemoveTooltipListener();
+ }
+
+ mEventTarget = nullptr;
+
+ // it really doesn't matter if these fail...
+ return NS_OK;
+}
+
+// Unsubscribe from all the various tooltip events that we were listening to.
+NS_IMETHODIMP
+ChromeTooltipListener::RemoveTooltipListener() {
+ if (mEventTarget) {
+ mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false);
+ mEventTarget->RemoveSystemEventListener(u"mousedown"_ns, this, false);
+ mEventTarget->RemoveSystemEventListener(u"mouseout"_ns, this, false);
+ mEventTarget->RemoveSystemEventListener(u"mousemove"_ns, this, false);
+ mTooltipListenerInstalled = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ChromeTooltipListener::HandleEvent(Event* aEvent) {
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+
+ if (eventType.EqualsLiteral("mousedown")) {
+ return HideTooltip();
+ } else if (eventType.EqualsLiteral("keydown")) {
+ WidgetKeyboardEvent* keyEvent = aEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (nsXULTooltipListener::KeyEventHidesTooltip(*keyEvent)) {
+ return HideTooltip();
+ }
+ return NS_OK;
+ } else if (eventType.EqualsLiteral("mouseout")) {
+ // Reset flag so that tooltip will display on the next MouseMove
+ mTooltipShownOnce = false;
+ return HideTooltip();
+ } else if (eventType.EqualsLiteral("mousemove")) {
+ return MouseMove(aEvent);
+ }
+
+ NS_ERROR("Unexpected event type");
+ return NS_OK;
+}
+
+// If we're a tooltip, fire off a timer to see if a tooltip should be shown. If
+// the timer fires, we cache the node in |mPossibleTooltipNode|.
+nsresult ChromeTooltipListener::MouseMove(Event* aMouseEvent) {
+ if (!nsXULTooltipListener::ShowTooltips()) {
+ return NS_OK;
+ }
+
+ MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ if (!mouseEvent) {
+ return NS_OK;
+ }
+
+ // stash the coordinates of the event so that we can still get back to it from
+ // within the timer callback. On win32, we'll get a MouseMove event even when
+ // a popup goes away -- even when the mouse doesn't change position! To get
+ // around this, we make sure the mouse has really moved before proceeding.
+ CSSIntPoint newMouseClientPoint = mouseEvent->ClientPoint();
+ if (mMouseClientPoint == newMouseClientPoint) {
+ return NS_OK;
+ }
+
+ // Filter out minor mouse movements.
+ if (mShowingTooltip &&
+ (abs(mMouseClientPoint.x - newMouseClientPoint.x) <=
+ kTooltipMouseMoveTolerance) &&
+ (abs(mMouseClientPoint.y - newMouseClientPoint.y) <=
+ kTooltipMouseMoveTolerance)) {
+ return NS_OK;
+ }
+
+ mMouseClientPoint = newMouseClientPoint;
+ mMouseScreenPoint = mouseEvent->ScreenPointLayoutDevicePix();
+
+ if (mTooltipTimer) {
+ mTooltipTimer->Cancel();
+ mTooltipTimer = nullptr;
+ }
+
+ if (!mShowingTooltip) {
+ if (nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetComposedTarget()) {
+ mPossibleTooltipNode = nsINode::FromEventTarget(eventTarget);
+ }
+
+ if (mPossibleTooltipNode) {
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTooltipTimer), sTooltipCallback, this,
+ LookAndFeel::GetInt(LookAndFeel::IntID::TooltipDelay, 500),
+ nsITimer::TYPE_ONE_SHOT, "ChromeTooltipListener::MouseMove",
+ GetMainThreadSerialEventTarget());
+ if (NS_FAILED(rv)) {
+ mPossibleTooltipNode = nullptr;
+ NS_WARNING("Could not create a timer for tooltip tracking");
+ }
+ }
+ } else {
+ mTooltipShownOnce = true;
+ return HideTooltip();
+ }
+
+ return NS_OK;
+}
+
+// Tell the registered chrome that they should show the tooltip.
+NS_IMETHODIMP
+ChromeTooltipListener::ShowTooltip(int32_t aInXCoords, int32_t aInYCoords,
+ const nsAString& aInTipText,
+ const nsAString& aTipDir) {
+ nsresult rv = NS_OK;
+
+ // do the work to call the client
+ nsCOMPtr<nsITooltipListener> tooltipListener(
+ do_QueryInterface(mWebBrowserChrome));
+ if (tooltipListener) {
+ rv = tooltipListener->OnShowTooltip(aInXCoords, aInYCoords, aInTipText,
+ aTipDir);
+ if (NS_SUCCEEDED(rv)) {
+ mShowingTooltip = true;
+ }
+ }
+
+ return rv;
+}
+
+// Tell the registered chrome that they should rollup the tooltip
+// NOTE: This routine is safe to call even if the popup is already closed.
+NS_IMETHODIMP
+ChromeTooltipListener::HideTooltip() {
+ nsresult rv = NS_OK;
+
+ // shut down the relevant timers
+ if (mTooltipTimer) {
+ mTooltipTimer->Cancel();
+ mTooltipTimer = nullptr;
+ // release tooltip target
+ mPossibleTooltipNode = nullptr;
+ mLastDocshell = nullptr;
+ }
+
+ // if we're showing the tip, tell the chrome to hide it
+ if (mShowingTooltip) {
+ nsCOMPtr<nsITooltipListener> tooltipListener(
+ do_QueryInterface(mWebBrowserChrome));
+ if (tooltipListener) {
+ rv = tooltipListener->OnHideTooltip();
+ if (NS_SUCCEEDED(rv)) {
+ mShowingTooltip = false;
+ }
+ }
+ }
+
+ return rv;
+}
+
+bool ChromeTooltipListener::WebProgressShowedTooltip(
+ nsIWebProgress* aWebProgress) {
+ nsCOMPtr<nsIDocShell> docshell = do_QueryInterface(aWebProgress);
+ nsCOMPtr<nsIDocShell> lastUsed = do_QueryReferent(mLastDocshell);
+ while (lastUsed) {
+ if (lastUsed == docshell) {
+ return true;
+ }
+ // We can't use the docshell hierarchy here, because when the parent
+ // docshell is navigated, the child docshell is disconnected (ie its
+ // references to the parent are nulled out) despite it still being
+ // alive here. So we use the document hierarchy instead:
+ Document* document = lastUsed->GetDocument();
+ if (document) {
+ document = document->GetInProcessParentDocument();
+ }
+ if (!document) {
+ break;
+ }
+ lastUsed = document->GetDocShell();
+ }
+ return false;
+}
+
+// A timer callback, fired when the mouse has hovered inside of a frame for the
+// appropriate amount of time. Getting to this point means that we should show
+// the tooltip, but only after we determine there is an appropriate TITLE
+// element.
+//
+// This relies on certain things being cached into the |aChromeTooltipListener|
+// object passed to us by the timer:
+// -- the x/y coordinates of the mouse (mMouseClientY, mMouseClientX)
+// -- the dom node the user hovered over (mPossibleTooltipNode)
+void ChromeTooltipListener::sTooltipCallback(nsITimer* aTimer,
+ void* aChromeTooltipListener) {
+ auto* self = static_cast<ChromeTooltipListener*>(aChromeTooltipListener);
+ if (!self || !self->mPossibleTooltipNode) {
+ return;
+ }
+ // release tooltip target once done, no matter what we do here.
+ auto cleanup = MakeScopeExit([&] { self->mPossibleTooltipNode = nullptr; });
+ if (!self->mPossibleTooltipNode->IsInComposedDoc()) {
+ return;
+ }
+ // Check that the document or its ancestors haven't been replaced.
+ {
+ Document* doc = self->mPossibleTooltipNode->OwnerDoc();
+ while (doc) {
+ if (!doc->IsCurrentActiveDocument()) {
+ return;
+ }
+ doc = doc->GetInProcessParentDocument();
+ }
+ }
+
+ nsCOMPtr<nsIDocShell> docShell =
+ do_GetInterface(static_cast<nsIWebBrowser*>(self->mWebBrowser));
+ if (!docShell || !docShell->GetBrowsingContext()->IsActive()) {
+ return;
+ }
+
+ // if there is text associated with the node, show the tip and fire
+ // off a timer to auto-hide it.
+ nsITooltipTextProvider* tooltipProvider = self->GetTooltipTextProvider();
+ if (!tooltipProvider) {
+ return;
+ }
+ nsString tooltipText;
+ nsString directionText;
+ bool textFound = false;
+ tooltipProvider->GetNodeText(self->mPossibleTooltipNode,
+ getter_Copies(tooltipText),
+ getter_Copies(directionText), &textFound);
+
+ if (textFound && (!self->mTooltipShownOnce ||
+ tooltipText != self->mLastShownTooltipText)) {
+ // ShowTooltip expects screen-relative position.
+ self->ShowTooltip(self->mMouseScreenPoint.x, self->mMouseScreenPoint.y,
+ tooltipText, directionText);
+ self->mLastShownTooltipText = std::move(tooltipText);
+ self->mLastDocshell = do_GetWeakReference(
+ self->mPossibleTooltipNode->OwnerDoc()->GetDocShell());
+ }
+}
diff --git a/docshell/base/nsDocShellTreeOwner.h b/docshell/base/nsDocShellTreeOwner.h
new file mode 100644
index 0000000000..0627357606
--- /dev/null
+++ b/docshell/base/nsDocShellTreeOwner.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellTreeOwner_h__
+#define nsDocShellTreeOwner_h__
+
+// Helper Classes
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+// Interfaces Needed
+#include "nsIBaseWindow.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsIDOMEventListener.h"
+#include "nsIWebProgressListener.h"
+#include "nsWeakReference.h"
+#include "nsITimer.h"
+#include "nsIPrompt.h"
+#include "nsIAuthPrompt.h"
+#include "nsITooltipTextProvider.h"
+#include "nsCTooltipTextProvider.h"
+
+namespace mozilla {
+namespace dom {
+class Event;
+class EventTarget;
+} // namespace dom
+} // namespace mozilla
+
+class nsIDocShellTreeItem;
+class nsWebBrowser;
+class ChromeTooltipListener;
+
+class nsDocShellTreeOwner final : public nsIDocShellTreeOwner,
+ public nsIBaseWindow,
+ public nsIInterfaceRequestor,
+ public nsIWebProgressListener,
+ public nsIDOMEventListener,
+ public nsSupportsWeakReference {
+ friend class nsWebBrowser;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIBASEWINDOW
+ NS_DECL_NSIDOCSHELLTREEOWNER
+ NS_DECL_NSIDOMEVENTLISTENER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ protected:
+ nsDocShellTreeOwner();
+ virtual ~nsDocShellTreeOwner();
+
+ void WebBrowser(nsWebBrowser* aWebBrowser);
+
+ nsWebBrowser* WebBrowser();
+ NS_IMETHOD SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner);
+ NS_IMETHOD SetWebBrowserChrome(nsIWebBrowserChrome* aWebBrowserChrome);
+
+ NS_IMETHOD AddChromeListeners();
+ NS_IMETHOD RemoveChromeListeners();
+
+ void EnsurePrompter();
+ void EnsureAuthPrompter();
+
+ void AddToWatcher();
+ void RemoveFromWatcher();
+
+ void EnsureContentTreeOwner();
+
+ // These helper functions return the correct instances of the requested
+ // interfaces. If the object passed to SetWebBrowserChrome() implements
+ // nsISupportsWeakReference, then these functions call QueryReferent on
+ // that object. Otherwise, they return an addrefed pointer. If the
+ // WebBrowserChrome object doesn't exist, they return nullptr.
+ already_AddRefed<nsIWebBrowserChrome> GetWebBrowserChrome();
+ already_AddRefed<nsIBaseWindow> GetOwnerWin();
+ already_AddRefed<nsIInterfaceRequestor> GetOwnerRequestor();
+
+ protected:
+ // Weak References
+ nsWebBrowser* mWebBrowser;
+ nsIDocShellTreeOwner* mTreeOwner;
+ nsIDocShellTreeItem* mPrimaryContentShell;
+
+ nsIWebBrowserChrome* mWebBrowserChrome;
+ nsIBaseWindow* mOwnerWin;
+ nsIInterfaceRequestor* mOwnerRequestor;
+
+ nsWeakPtr mWebBrowserChromeWeak; // nsIWebBrowserChrome
+
+ // the objects that listen for chrome events like context menus and tooltips.
+ // They are separate objects to avoid circular references between |this|
+ // and the DOM.
+ RefPtr<ChromeTooltipListener> mChromeTooltipListener;
+
+ RefPtr<nsDocShellTreeOwner> mContentTreeOwner;
+
+ nsCOMPtr<nsIPrompt> mPrompter;
+ nsCOMPtr<nsIAuthPrompt> mAuthPrompter;
+ nsCOMPtr<nsIRemoteTab> mPrimaryRemoteTab;
+};
+
+#endif /* nsDocShellTreeOwner_h__ */
diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl
new file mode 100644
index 0000000000..21f09a517e
--- /dev/null
+++ b/docshell/base/nsIDocShell.idl
@@ -0,0 +1,766 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "domstubs.idl"
+#include "nsIDocShellTreeItem.idl"
+#include "nsIRequest.idl"
+
+%{ C++
+#include "js/TypeDecls.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+class nsCommandManager;
+class nsPresContext;
+class nsDocShellLoadState;
+namespace mozilla {
+class Encoding;
+class HTMLEditor;
+class PresShell;
+namespace dom {
+class BrowsingContext;
+class ClientSource;
+} // namespace dom
+}
+%}
+
+/**
+ * The nsIDocShell interface.
+ */
+
+[ptr] native nsPresContext(nsPresContext);
+[ptr] native nsCommandManager(nsCommandManager);
+[ptr] native PresShell(mozilla::PresShell);
+[ref] native MaybeURI(mozilla::Maybe<nsCOMPtr<nsIURI>>);
+[ref] native Encoding(const mozilla::Encoding*);
+ native UniqueClientSource(mozilla::UniquePtr<mozilla::dom::ClientSource>);
+
+interface nsIURI;
+interface nsIChannel;
+interface nsIContentSecurityPolicy;
+interface nsIDocumentViewer;
+interface nsIEditor;
+interface nsIEditingSession;
+interface nsIInputStream;
+interface nsIRequest;
+interface nsISHEntry;
+interface nsILayoutHistoryState;
+interface nsISecureBrowserUI;
+interface nsIScriptGlobalObject;
+interface nsIStructuredCloneContainer;
+interface nsIDOMStorage;
+interface nsIPrincipal;
+interface nsIPrivacyTransitionObserver;
+interface nsIReflowObserver;
+interface nsIScrollObserver;
+interface nsIRemoteTab;
+interface nsIBrowserChild;
+interface nsICommandParams;
+interface nsILoadURIDelegate;
+native BrowserChildRef(already_AddRefed<nsIBrowserChild>);
+native nsDocShellLoadStatePtr(nsDocShellLoadState*);
+
+webidl BrowsingContext;
+webidl ContentFrameMessageManager;
+webidl EventTarget;
+webidl Document;
+
+/**
+ * nsIDocShell is an interface corresponding to the native nsDocShell object,
+ * which is a legacy in-process object roughly corresponding to a 'browsing
+ * context', as created for a browser tab or an iframe, for example.
+ *
+ * nsIDocShell has a 1:1 relationship with its paired dom::BrowsingContext and
+ * nsGlobalWindowOuter. It may be replaced during navigation.
+ *
+ * See also the comment documenting dom::BrowsingContext and the documentation
+ * at:
+ *
+ * https://html.spec.whatwg.org/multipage/document-sequences.html#browsing-context
+ * https://firefox-source-docs.mozilla.org/dom/navigation/embedding.html
+ * https://firefox-source-docs.mozilla.org/dom/navigation/nav_replace.html
+ */
+[scriptable, builtinclass, uuid(049234fe-da10-478b-bc5d-bc6f9a1ba63d)]
+interface nsIDocShell : nsIDocShellTreeItem
+{
+ void setCancelContentJSEpoch(in long aEpoch);
+
+ /**
+ * Loads a given URI. This will give priority to loading the requested URI
+ * in the object implementing this interface. If it can't be loaded here
+ * however, the URL dispatcher will go through its normal process of content
+ * loading.
+ *
+ * @param aLoadState This is the extended load info for this load.
+ * @param aSetNavigating If we should set isNavigating to true while initiating
+ * the load.
+ */
+ [noscript]void loadURI(in nsDocShellLoadStatePtr aLoadState, in boolean aSetNavigating);
+
+ /**
+ * Do either a history.pushState() or history.replaceState() operation,
+ * depending on the value of aReplace.
+ */
+ [implicit_jscontext]
+ void addState(in jsval aData, in AString aTitle,
+ in AString aURL, in boolean aReplace);
+
+ /**
+ * Reset state to a new content model within the current document and the document
+ * viewer. Called by the document before initiating an out of band document.write().
+ */
+ void prepareForNewContentModel();
+
+ /**
+ * Helper for the session store to change the URI associated with the
+ * document.
+ */
+ void setCurrentURIForSessionStore(in nsIURI aURI);
+
+ /**
+ * Notify the associated content viewer and all child docshells that they are
+ * about to be hidden. If |isUnload| is true, then the document is being
+ * unloaded and all dynamic subframe history entries are removed as well.
+ *
+ * @param isUnload
+ * True to fire the unload event in addition to the pagehide event,
+ * and remove all dynamic subframe history entries.
+ */
+ [noscript] void firePageHideNotification(in boolean isUnload);
+
+ /**
+ * Presentation context for the currently loaded document. This may be null.
+ */
+ [notxpcom,nostdcall] readonly attribute nsPresContext presContext;
+
+ /**
+ * Presentation shell for the currently loaded document. This may be null.
+ */
+ [notxpcom,nostdcall] readonly attribute PresShell presShell;
+
+ /**
+ * Presentation shell for the oldest document, if this docshell is
+ * currently transitioning between documents.
+ */
+ [notxpcom,nostdcall] readonly attribute PresShell eldestPresShell;
+
+ /**
+ * Document Viewer that is currently loaded for this DocShell. This may
+ * change as the underlying content changes.
+ */
+ [infallible] readonly attribute nsIDocumentViewer docViewer;
+
+ /**
+ * Get the id of the outer window that is or will be in this docshell.
+ */
+ [infallible] readonly attribute unsigned long long outerWindowID;
+
+ /**
+ * This attribute allows chrome to tie in to handle DOM events that may
+ * be of interest to chrome.
+ */
+ attribute EventTarget chromeEventHandler;
+
+ /**
+ * This allows chrome to set a custom User agent on a specific docshell
+ */
+ attribute AString customUserAgent;
+
+ /**
+ * Whether CSS error reporting is enabled.
+ */
+ attribute boolean cssErrorReportingEnabled;
+
+ /**
+ * Attribute stating if refresh based redirects can be allowed
+ */
+ attribute boolean allowMetaRedirects;
+
+ /**
+ * Attribute stating if it should allow subframes (framesets/iframes) or not
+ */
+ attribute boolean allowSubframes;
+
+ /**
+ * Attribute stating whether or not images should be loaded.
+ */
+ attribute boolean allowImages;
+
+ /**
+ * Attribute stating whether or not media (audio/video) should be loaded.
+ */
+ [infallible] attribute boolean allowMedia;
+
+ /**
+ * Attribute that determines whether DNS prefetch is allowed for this subtree
+ * of the docshell tree. Defaults to true. Setting this will make it take
+ * effect starting with the next document loaded in the docshell.
+ */
+ attribute boolean allowDNSPrefetch;
+
+ /**
+ * Attribute that determines whether window control (move/resize) is allowed.
+ */
+ attribute boolean allowWindowControl;
+
+ /**
+ * True if the docshell allows its content to be handled by a content listener
+ * other than the docshell itself, including the external helper app service,
+ * and false otherwise. Defaults to true.
+ */
+ [infallible] attribute boolean allowContentRetargeting;
+
+ /**
+ * True if new child docshells should allow content retargeting.
+ * Setting allowContentRetargeting also overwrites this value.
+ */
+ [infallible] attribute boolean allowContentRetargetingOnChildren;
+
+ /**
+ * Get an array of this docShell and its children.
+ *
+ * @param aItemType - Only include docShells of this type, or if typeAll,
+ * include all child shells.
+ * Uses types from nsIDocShellTreeItem.
+ * @param aDirection - Whether to enumerate forwards or backwards.
+ */
+
+ cenum DocShellEnumeratorDirection : 8 {
+ ENUMERATE_FORWARDS = 0,
+ ENUMERATE_BACKWARDS = 1
+ };
+
+ Array<nsIDocShell> getAllDocShellsInSubtree(in long aItemType,
+ in nsIDocShell_DocShellEnumeratorDirection aDirection);
+
+ /**
+ * The type of application that created this window.
+ *
+ * DO NOT DELETE, see bug 176166. For firefox, this value will always be
+ * UNKNOWN. However, it is used heavily in Thunderbird/comm-central and we
+ * don't really have a great replacement at the moment, so we'll just leave it
+ * here.
+ */
+ cenum AppType : 8 {
+ APP_TYPE_UNKNOWN = 0,
+ APP_TYPE_MAIL = 1,
+ APP_TYPE_EDITOR = 2
+ };
+
+ [infallible] attribute nsIDocShell_AppType appType;
+
+ /**
+ * certain docshells (like the message pane)
+ * should not throw up auth dialogs
+ * because it can act as a password trojan
+ */
+ attribute boolean allowAuth;
+
+ /**
+ * Set/Get the document scale factor. When setting this attribute, a
+ * NS_ERROR_NOT_IMPLEMENTED error may be returned by implementations
+ * not supporting zoom. Implementations not supporting zoom should return
+ * 1.0 all the time for the Get operation. 1.0 by the way is the default
+ * of zoom. This means 100% of normal scaling or in other words normal size
+ * no zoom.
+ */
+ attribute float zoom;
+
+ /*
+ * Tells the docshell to offer focus to its tree owner.
+ * This is currently only necessary for embedding chrome.
+ * If forDocumentNavigation is true, then document navigation should be
+ * performed, where only the root of documents are selected. Otherwise, the
+ * next element in the parent should be returned. Returns true if focus was
+ * successfully taken by the tree owner.
+ */
+ bool tabToTreeOwner(in boolean forward, in boolean forDocumentNavigation);
+
+ /**
+ * Current busy state for DocShell
+ */
+ cenum BusyFlags : 8 {
+ BUSY_FLAGS_NONE = 0,
+ BUSY_FLAGS_BUSY = 1,
+ BUSY_FLAGS_BEFORE_PAGE_LOAD = 2,
+ BUSY_FLAGS_PAGE_LOADING = 4,
+ };
+
+ [infallible] readonly attribute nsIDocShell_BusyFlags busyFlags;
+
+ /**
+ * Load commands for the document
+ */
+ cenum LoadCommand : 8 {
+ LOAD_CMD_NORMAL = 0x1, // Normal load
+ LOAD_CMD_RELOAD = 0x2, // Reload
+ LOAD_CMD_HISTORY = 0x4, // Load from history
+ LOAD_CMD_PUSHSTATE = 0x8, // History.pushState()
+ };
+
+ /*
+ * Attribute to access the loadtype for the document. LoadType Enum is
+ * defined in nsDocShellLoadTypes.h
+ */
+ [infallible] attribute unsigned long loadType;
+
+ /*
+ * Default load flags (as defined in nsIRequest) that will be set on all
+ * requests made by this docShell and propagated to all child docShells and
+ * to nsILoadGroup::defaultLoadFlags for the docShell's loadGroup.
+ * Default is no flags. Once set, only future requests initiated by the
+ * docShell are affected, so in general, these flags should be set before
+ * the docShell loads any content.
+ */
+ attribute nsLoadFlags defaultLoadFlags;
+
+ /*
+ * returns true if the docshell is being destroyed, false otherwise
+ */
+ boolean isBeingDestroyed();
+
+ /*
+ * Returns true if the docshell is currently executing the onLoad Handler
+ */
+ readonly attribute boolean isExecutingOnLoadHandler;
+
+ attribute nsILayoutHistoryState layoutHistoryState;
+
+ /**
+ * Object used to delegate URI loading to an upper context.
+ * Currently only set for GeckoView to allow handling of load requests
+ * at the application level.
+ */
+ readonly attribute nsILoadURIDelegate loadURIDelegate;
+
+ /**
+ * Cancel the XPCOM timers for each meta-refresh URI in this docshell,
+ * and this docshell's children, recursively. The meta-refresh timers can be
+ * restarted using resumeRefreshURIs(). If the timers are already suspended,
+ * this has no effect.
+ */
+ void suspendRefreshURIs();
+
+ /**
+ * Restart the XPCOM timers for each meta-refresh URI in this docshell,
+ * and this docshell's children, recursively. If the timers are already
+ * running, this has no effect.
+ */
+ void resumeRefreshURIs();
+
+ /**
+ * Begin firing WebProgressListener notifications for restoring a page
+ * presentation. |viewer| is the content viewer whose document we are
+ * starting to load. If null, it defaults to the docshell's current content
+ * viewer, creating one if necessary. |top| should be true for the toplevel
+ * docshell that is being restored; it will be set to false when this method
+ * is called for child docshells. This method will post an event to
+ * complete the simulated load after returning to the event loop.
+ */
+ void beginRestore(in nsIDocumentViewer viewer, in boolean top);
+
+ /**
+ * Finish firing WebProgressListener notifications and DOM events for
+ * restoring a page presentation. This should only be called via
+ * beginRestore().
+ */
+ void finishRestore();
+
+ void clearCachedUserAgent();
+
+ void clearCachedPlatform();
+
+ /* Track whether we're currently restoring a document presentation. */
+ readonly attribute boolean restoringDocument;
+
+ /* attribute to access whether error pages are enabled */
+ attribute boolean useErrorPages;
+
+ /**
+ * Display a load error in a frame while keeping that frame's currentURI
+ * pointing correctly to the page where the error ocurred, rather than to
+ * the error document page. You must provide either the aURI or aURL parameter.
+ *
+ * @param aError The error code to be displayed
+ * @param aURI nsIURI of the page where the error happened
+ * @param aURL wstring of the page where the error happened
+ * @param aFailedChannel The channel related to this error
+ *
+ * Returns whether or not we displayed an error page (note: will always
+ * return false if in-content error pages are disabled!)
+ */
+ boolean displayLoadError(in nsresult aError,
+ in nsIURI aURI,
+ in wstring aURL,
+ [optional] in nsIChannel aFailedChannel);
+
+ /**
+ * The channel that failed to load and resulted in an error page.
+ * May be null. Relevant only to error pages.
+ */
+ readonly attribute nsIChannel failedChannel;
+
+ /**
+ * Keeps track of the previous nsISHEntry index and the current
+ * nsISHEntry index at the time that the doc shell begins to load.
+ * Used for ContentViewer eviction.
+ */
+ readonly attribute long previousEntryIndex;
+ readonly attribute long loadedEntryIndex;
+
+ /**
+ * Notification that entries have been removed from the beginning of a
+ * nsSHistory which has this as its rootDocShell.
+ *
+ * @param numEntries - The number of entries removed
+ */
+ void historyPurged(in long numEntries);
+
+ /**
+ * Gets the channel for the currently loaded document, if any.
+ * For a new document load, this will be the channel of the previous document
+ * until after OnLocationChange fires.
+ */
+ readonly attribute nsIChannel currentDocumentChannel;
+
+ /**
+ * Find out whether the docshell is currently in the middle of a page
+ * transition. This is set just before the pagehide/unload events fire.
+ */
+ [infallible] readonly attribute boolean isInUnload;
+
+ /**
+ * Disconnects this docshell's editor from its window, and stores the
+ * editor data in the open document's session history entry. This
+ * should be called only during page transitions.
+ */
+ [noscript, notxpcom] void DetachEditorFromWindow();
+
+ /**
+ * Propagated to the print preview document viewer. Must only be called on
+ * a document viewer that has been initialized for print preview.
+ */
+ void exitPrintPreview();
+
+ /**
+ * The ID of the docshell in the session history.
+ */
+ readonly attribute nsIDRef historyID;
+
+ /**
+ * Helper method for accessing this value from C++
+ */
+ [noscript, notxpcom] nsIDRef HistoryID();
+
+ /**
+ * Create a new about:blank document and content viewer.
+ * @param aPrincipal the principal to use for the new document.
+ * @param aPartitionedPrincipal the partitioned principal to use for the new
+ * document.
+ * @param aCsp the CSP to use for the new document.
+ */
+ void createAboutBlankDocumentViewer(in nsIPrincipal aPrincipal,
+ in nsIPrincipal aPartitionedPrincipal,
+ [optional] in nsIContentSecurityPolicy aCSP);
+
+ /**
+ * Upon getting, returns the canonical encoding label of the document
+ * currently loaded into this docshell.
+ */
+ readonly attribute ACString charset;
+
+ void forceEncodingDetection();
+
+ /**
+ * In a child docshell, this is the charset of the parent docshell
+ */
+ [noscript, notxpcom, nostdcall] void setParentCharset(
+ in Encoding parentCharset,
+ in int32_t parentCharsetSource,
+ in nsIPrincipal parentCharsetPrincipal);
+ [noscript, notxpcom, nostdcall] void getParentCharset(
+ out Encoding parentCharset,
+ out int32_t parentCharsetSource,
+ out nsIPrincipal parentCharsetPrincipal);
+
+ /**
+ * Return a DOMHighResTimeStamp representing the number of
+ * milliseconds from an arbitrary point in time. The reference
+ * point is shared by all DocShells and is also used by timestamps
+ * on markers.
+ */
+ DOMHighResTimeStamp now();
+
+ /**
+ * Add an observer to the list of parties to be notified when this docshell's
+ * private browsing status is changed. |obs| must support weak references.
+ */
+ void addWeakPrivacyTransitionObserver(in nsIPrivacyTransitionObserver obs);
+
+ /**
+ * Add an observer to the list of parties to be notified when reflows are
+ * occurring. |obs| must support weak references.
+ */
+ void addWeakReflowObserver(in nsIReflowObserver obs);
+
+ /**
+ * Remove an observer from the list of parties to be notified about reflows.
+ */
+ void removeWeakReflowObserver(in nsIReflowObserver obs);
+
+ /**
+ * Notify all attached observers that a reflow has just occurred.
+ *
+ * @param interruptible if true, the reflow was interruptible.
+ * @param start timestamp when reflow started, in milliseconds since
+ * navigationStart (accurate to 1/1000 of a ms)
+ * @param end timestamp when reflow ended, in milliseconds since
+ * navigationStart (accurate to 1/1000 of a ms)
+ */
+ [noscript] void notifyReflowObservers(in bool interruptible,
+ in DOMHighResTimeStamp start,
+ in DOMHighResTimeStamp end);
+
+ /**
+ * Add an observer to the list of parties to be notified when scroll position
+ * of some elements is changed.
+ */
+ [noscript] void addWeakScrollObserver(in nsIScrollObserver obs);
+
+ /**
+ * Add an observer to the list of parties to be notified when scroll position
+ * of some elements is changed.
+ */
+ [noscript] void removeWeakScrollObserver(in nsIScrollObserver obs);
+
+ /**
+ * Notify all attached observers that the scroll position of some element
+ * has changed.
+ */
+ [noscript] void notifyScrollObservers();
+
+ /**
+ * Returns true if this docshell is the top level content docshell.
+ */
+ [infallible] readonly attribute boolean isTopLevelContentDocShell;
+
+ /**
+ * True iff asynchronous panning and zooming is enabled for this
+ * docshell.
+ */
+ readonly attribute bool asyncPanZoomEnabled;
+
+ /**
+ * Indicates whether the UI may enable the character encoding menu. The UI
+ * must disable the menu when this property is false.
+ */
+ [infallible] readonly attribute boolean mayEnableCharacterEncodingMenu;
+
+ attribute nsIEditor editor;
+ readonly attribute boolean editable; /* this docShell is editable */
+ readonly attribute boolean hasEditingSession; /* this docShell has an editing session */
+
+ /**
+ * Make this docShell editable, setting a flag that causes
+ * an editor to get created, either immediately, or after
+ * a url has been loaded.
+ * @param inWaitForUriLoad true to wait for a URI before
+ * creating the editor.
+ */
+ void makeEditable(in boolean inWaitForUriLoad);
+
+ /**
+ * Returns false for mLSHE, true for mOSHE
+ */
+ boolean getCurrentSHEntry(out nsISHEntry aEntry);
+
+ /**
+ * Cherry picked parts of nsIController.
+ * They are here, because we want to call these functions
+ * from JS.
+ */
+ boolean isCommandEnabled(in string command);
+ [can_run_script]
+ void doCommand(in string command);
+ [can_run_script]
+ void doCommandWithParams(in string command, in nsICommandParams aParams);
+
+ /**
+ * Invisible DocShell are dummy construct to simulate DOM windows
+ * without any actual visual representation. They have to be marked
+ * at construction time, to avoid any painting activity.
+ */
+ [noscript, notxpcom] bool IsInvisible();
+ [noscript, notxpcom] void SetInvisible(in bool aIsInvisibleDocshell);
+
+/**
+ * Get the script global for the document in this docshell.
+*/
+ [noscript,notxpcom,nostdcall] nsIScriptGlobalObject GetScriptGlobalObject();
+
+ [noscript,notxpcom,nostdcall] Document getExtantDocument();
+
+ /**
+ * This attribute determines whether a document which is not about:blank has
+ * already be loaded by this docShell.
+ */
+ [infallible] readonly attribute boolean hasLoadedNonBlankURI;
+
+ /**
+ * Allow usage of -moz-window-dragging:drag for content docshells.
+ * True for top level chrome docshells. Throws if set to false with
+ * top level chrome docshell.
+ */
+ attribute boolean windowDraggingAllowed;
+
+ /**
+ * Sets/gets the current scroll restoration mode.
+ * @see https://html.spec.whatwg.org/#dom-history-scroll-restoration
+ */
+ attribute boolean currentScrollRestorationIsManual;
+
+ /**
+ * Setter and getter for the origin attributes living on this docshell.
+ */
+ [implicit_jscontext]
+ jsval getOriginAttributes();
+
+ [implicit_jscontext]
+ void setOriginAttributes(in jsval aAttrs);
+
+ /**
+ * The editing session for this docshell.
+ */
+ readonly attribute nsIEditingSession editingSession;
+
+ /**
+ * The browser child for this docshell.
+ */
+ [binaryname(ScriptableBrowserChild)] readonly attribute nsIBrowserChild browserChild;
+ [noscript,notxpcom,nostdcall] BrowserChildRef GetBrowserChild();
+
+ [noscript,nostdcall,notxpcom] nsCommandManager GetCommandManager();
+
+ cenum MetaViewportOverride: 8 {
+ /**
+ * Override platform/pref default behaviour and force-disable support for
+ * <meta name="viewport">.
+ */
+ META_VIEWPORT_OVERRIDE_DISABLED = 0,
+ /**
+ * Override platform/pref default behaviour and force-enable support for
+ * <meta name="viewport">.
+ */
+ META_VIEWPORT_OVERRIDE_ENABLED = 1,
+ /**
+ * Don't override the platform/pref default behaviour for support for
+ * <meta name="viewport">.
+ */
+ META_VIEWPORT_OVERRIDE_NONE = 2,
+ };
+
+ /**
+ * This allows chrome to override the default choice of whether the
+ * <meta name="viewport"> tag is respected in a specific docshell.
+ * Possible values are listed above.
+ */
+ [infallible, setter_can_run_script] attribute nsIDocShell_MetaViewportOverride metaViewportOverride;
+
+ /**
+ * Attribute that determines whether tracking protection is enabled.
+ */
+ attribute boolean useTrackingProtection;
+
+ /**
+ * Fire a dummy location change event asynchronously.
+ */
+ [noscript] void dispatchLocationChangeEvent();
+
+
+ /**
+ * Start delayed autoplay media which are in the current document.
+ */
+ [noscript] void startDelayedAutoplayMediaComponents();
+
+ /**
+ * Take ownership of the ClientSource representing an initial about:blank
+ * document that was never needed. As an optimization we avoid creating
+ * this document if no code calls GetDocument(), but we still need a
+ * ClientSource object to represent the about:blank window. This may return
+ * nullptr; for example if the docshell has created a real window and document
+ * already.
+ */
+ [noscript, nostdcall, notxpcom]
+ UniqueClientSource TakeInitialClientSource();
+
+ void setColorMatrix(in Array<float> aMatrix);
+
+ /**
+ * Returns true if the current load is a forced reload,
+ * e.g. started by holding shift whilst triggering reload.
+ */
+ readonly attribute bool isForceReloading;
+
+ Array<float> getColorMatrix();
+
+%{C++
+ /**
+ * These methods call nsDocShell::GetHTMLEditorInternal() and
+ * nsDocShell::SetHTMLEditorInternal() with static_cast.
+ */
+ mozilla::HTMLEditor* GetHTMLEditor();
+ nsresult SetHTMLEditor(mozilla::HTMLEditor* aHTMLEditor);
+%}
+
+ /**
+ * The message manager for this docshell. This does not throw, but
+ * can return null if the docshell has no message manager.
+ */
+ [infallible] readonly attribute ContentFrameMessageManager messageManager;
+
+ /**
+ * This returns a Promise which resolves to a boolean. True when the
+ * document has Tracking Content that has been blocked from loading, false
+ * otherwise.
+ */
+ Promise getHasTrackingContentBlocked();
+
+ /**
+ * Return whether this docshell is "attempting to navigate" in the
+ * sense that's relevant to document.open.
+ */
+ [notxpcom, nostdcall] readonly attribute boolean isAttemptingToNavigate;
+
+ /*
+ * Whether or not this docshell is executing a nsIWebNavigation navigation
+ * method.
+ *
+ * This will be true when the following methods are executing:
+ * nsIWebNavigation.binaryLoadURI
+ * nsIWebNavigation.goBack
+ * nsIWebNavigation.goForward
+ * nsIWebNavigation.gotoIndex
+ * nsIWebNavigation.loadURI
+ */
+ [infallible] readonly attribute boolean isNavigating;
+
+ /**
+ * @see nsISHEntry synchronizeLayoutHistoryState().
+ */
+ void synchronizeLayoutHistoryState();
+
+ /**
+ * This attempts to save any applicable layout history state (like
+ * scroll position) in the nsISHEntry. This is normally done
+ * automatically when transitioning from page to page in the
+ * same process. We expose this function to support transitioning
+ * from page to page across processes as a workaround for bug 1630234
+ * until session history state is moved into the parent process.
+ */
+ void persistLayoutHistoryState();
+};
diff --git a/docshell/base/nsIDocShellTreeItem.idl b/docshell/base/nsIDocShellTreeItem.idl
new file mode 100644
index 0000000000..a80373832f
--- /dev/null
+++ b/docshell/base/nsIDocShellTreeItem.idl
@@ -0,0 +1,171 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIDocShellTreeOwner;
+interface nsPIDOMWindowOuter;
+
+webidl Document;
+webidl BrowsingContext;
+
+/**
+ * The nsIDocShellTreeItem supplies the methods that are required of any item
+ * that wishes to be able to live within the docshell tree either as a middle
+ * node or a leaf.
+ */
+
+[scriptable, builtinclass, uuid(9b7c586f-9214-480c-a2c4-49b526fff1a6)]
+interface nsIDocShellTreeItem : nsISupports
+{
+ /*
+ name of the DocShellTreeItem
+ */
+ attribute AString name;
+
+ /**
+ * Compares the provided name against the item's name and
+ * returns the appropriate result.
+ *
+ * @return <CODE>PR_TRUE</CODE> if names match;
+ * <CODE>PR_FALSE</CODE> otherwise.
+ */
+ boolean nameEquals(in AString name);
+
+ /*
+ Definitions for the item types.
+ */
+ const long typeChrome=0; // typeChrome must equal 0
+ const long typeContent=1; // typeContent must equal 1
+ const long typeContentWrapper=2; // typeContentWrapper must equal 2
+ const long typeChromeWrapper=3; // typeChromeWrapper must equal 3
+
+ const long typeAll=0x7FFFFFFF;
+
+ /*
+ The type this item is.
+ */
+ readonly attribute long itemType;
+ [noscript,notxpcom,nostdcall] long ItemType();
+
+ /*
+ Parent DocShell.
+
+ @deprecated: Use `BrowsingContext::GetParent()` instead.
+ (NOTE: `BrowsingContext::GetParent()` will not cross isolation boundaries)
+ */
+ [binaryname(InProcessParent)]
+ readonly attribute nsIDocShellTreeItem parent;
+
+ /*
+ This getter returns the same thing parent does however if the parent
+ is of a different itemType, or if the parent is an <iframe mozbrowser>.
+ It will instead return nullptr. This call is a convience function for
+ Ithose wishing to not cross the boundaries at which item types change.
+
+ @deprecated: Use `BrowsingContext::GetParent()` instead.
+ */
+ [binaryname(InProcessSameTypeParent)]
+ readonly attribute nsIDocShellTreeItem sameTypeParent;
+
+ /*
+ Returns the root DocShellTreeItem. This is a convience equivalent to
+ getting the parent and its parent until there isn't a parent.
+
+ @deprecated: Use `BrowsingContext::Top()` instead.
+ (NOTE: `BrowsingContext::Top()` will not cross isolation boundaries)
+ */
+ [binaryname(InProcessRootTreeItem)]
+ readonly attribute nsIDocShellTreeItem rootTreeItem;
+
+ /*
+ Returns the root DocShellTreeItem of the same type. This is a convience
+ equivalent to getting the parent of the same type and its parent until
+ there isn't a parent.
+
+ @deprecated: Use `BrowsingContext::Top()` instead.
+ */
+ [binaryname(InProcessSameTypeRootTreeItem)]
+ readonly attribute nsIDocShellTreeItem sameTypeRootTreeItem;
+
+ /*
+ The owner of the DocShell Tree. This interface will be called upon when
+ the docshell has things it needs to tell to the owner of the docshell.
+ Note that docShell tree ownership does not cross tree types. Meaning
+ setting ownership on a chrome tree does not set ownership on the content
+ sub-trees. A given tree's boundaries are identified by the type changes.
+ Trees of different types may be connected, but should not be traversed
+ for things such as ownership.
+
+ Note implementers of this interface should NOT effect the lifetime of the
+ parent DocShell by holding this reference as it creates a cycle. Owners
+ when releasing this interface should set the treeOwner to nullptr.
+ Implementers of this interface are guaranteed that when treeOwner is
+ set that the poitner is valid without having to addref.
+
+ Further note however when others try to get the interface it should be
+ addref'd before handing it to them.
+ */
+ readonly attribute nsIDocShellTreeOwner treeOwner;
+ [noscript] void setTreeOwner(in nsIDocShellTreeOwner treeOwner);
+
+ /*
+ The current number of DocShells which are immediate children of the
+ this object.
+
+
+ @deprecated: Prefer using `BrowsingContext::Children()`, as this count will
+ not include out-of-process iframes.
+ */
+ [binaryname(InProcessChildCount), infallible]
+ readonly attribute long childCount;
+
+ /*
+ Add a new child DocShellTreeItem. Adds to the end of the list.
+ Note that this does NOT take a reference to the child. The child stays
+ alive only as long as it's referenced from outside the docshell tree.
+
+ @throws NS_ERROR_ILLEGAL_VALUE if child corresponds to the same
+ object as this treenode or an ancestor of this treenode
+ @throws NS_ERROR_UNEXPECTED if this node is a leaf in the tree.
+ */
+ [noscript] void addChild(in nsIDocShellTreeItem child);
+
+ /*
+ Removes a child DocShellTreeItem.
+
+ @throws NS_ERROR_UNEXPECTED if this node is a leaf in the tree.
+ */
+ [noscript] void removeChild(in nsIDocShellTreeItem child);
+
+ /**
+ * Return the child at the index requested. This is 0-based.
+ *
+ * @deprecated: Prefer using `BrowsingContext::Children()`, as this will not
+ * include out-of-process iframes.
+ *
+ * @throws NS_ERROR_UNEXPECTED if the index is out of range
+ */
+ [binaryname(GetInProcessChildAt)]
+ nsIDocShellTreeItem getChildAt(in long index);
+
+ /**
+ * BrowsingContext associated with the DocShell.
+ */
+ [binaryname(BrowsingContextXPCOM)]
+ readonly attribute BrowsingContext browsingContext;
+
+ [noscript,notxpcom,nostdcall] BrowsingContext getBrowsingContext();
+
+ /**
+ * Returns the DOM outer window for the content viewer.
+ */
+ readonly attribute mozIDOMWindowProxy domWindow;
+
+ [noscript,nostdcall,notxpcom] Document getDocument();
+ [noscript,nostdcall,notxpcom] nsPIDOMWindowOuter getWindow();
+};
diff --git a/docshell/base/nsIDocShellTreeOwner.idl b/docshell/base/nsIDocShellTreeOwner.idl
new file mode 100644
index 0000000000..db6faa59c9
--- /dev/null
+++ b/docshell/base/nsIDocShellTreeOwner.idl
@@ -0,0 +1,113 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * The nsIDocShellTreeOwner
+ */
+
+interface nsIDocShellTreeItem;
+interface nsIRemoteTab;
+webidl BrowsingContext;
+
+[scriptable, uuid(0e3dc4b1-4cea-4a37-af71-79f0afd07574)]
+interface nsIDocShellTreeOwner : nsISupports
+{
+ /**
+ * Called when a content shell is added to the docshell tree. This is
+ * _only_ called for "root" content shells (that is, ones whose parent is a
+ * chrome shell).
+ *
+ * @param aContentShell the shell being added.
+ * @param aPrimary whether the shell is primary.
+ */
+ void contentShellAdded(in nsIDocShellTreeItem aContentShell,
+ in boolean aPrimary);
+
+ /**
+ * Called when a content shell is removed from the docshell tree. This is
+ * _only_ called for "root" content shells (that is, ones whose parent is a
+ * chrome shell). Note that if aContentShell was never added,
+ * contentShellRemoved should just do nothing.
+ *
+ * @param aContentShell the shell being removed.
+ */
+ void contentShellRemoved(in nsIDocShellTreeItem aContentShell);
+
+ /*
+ Returns the Primary Content Shell
+ */
+ readonly attribute nsIDocShellTreeItem primaryContentShell;
+
+ void remoteTabAdded(in nsIRemoteTab aTab, in boolean aPrimary);
+ void remoteTabRemoved(in nsIRemoteTab aTab);
+
+ /*
+ In multiprocess case we may not have primaryContentShell but
+ primaryRemoteTab.
+ */
+ readonly attribute nsIRemoteTab primaryRemoteTab;
+
+ /*
+ Get the BrowsingContext associated with either the primary content shell or
+ primary remote tab, depending on which is available.
+ */
+ readonly attribute BrowsingContext primaryContentBrowsingContext;
+
+ /*
+ Tells the tree owner to size its window or parent window in such a way
+ that the shell passed along will be the size specified.
+ */
+ [can_run_script]
+ void sizeShellTo(in nsIDocShellTreeItem shell, in long cx, in long cy);
+
+ /*
+ Gets the size of the primary content area in device pixels. This should work
+ for both in-process and out-of-process content areas.
+ */
+ void getPrimaryContentSize(out long width, out long height);
+ /*
+ Sets the size of the primary content area in device pixels. This should work
+ for both in-process and out-of-process content areas.
+ */
+ void setPrimaryContentSize(in long width, in long height);
+
+ /*
+ Gets the size of the root docshell in device pixels.
+ */
+ void getRootShellSize(out long width, out long height);
+ /*
+ Sets the size of the root docshell in device pixels.
+ */
+ void setRootShellSize(in long width, in long height);
+
+ /*
+ Sets the persistence of different attributes of the window.
+ */
+ void setPersistence(in boolean aPersistPosition,
+ in boolean aPersistSize,
+ in boolean aPersistSizeMode);
+
+ /*
+ Gets the current persistence states of the window.
+ */
+ void getPersistence(out boolean aPersistPosition,
+ out boolean aPersistSize,
+ out boolean aPersistSizeMode);
+
+ /*
+ Gets the number of tabs currently open in our window, assuming
+ this tree owner has such a concept.
+ */
+ readonly attribute unsigned long tabCount;
+
+ /*
+ Returns true if there is a primary content shell or a primary
+ remote tab.
+ */
+ readonly attribute bool hasPrimaryContent;
+};
diff --git a/docshell/base/nsIDocumentLoaderFactory.idl b/docshell/base/nsIDocumentLoaderFactory.idl
new file mode 100644
index 0000000000..e3df2b2241
--- /dev/null
+++ b/docshell/base/nsIDocumentLoaderFactory.idl
@@ -0,0 +1,39 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIChannel;
+interface nsIDocumentViewer;
+interface nsIStreamListener;
+interface nsIDocShell;
+interface nsILoadGroup;
+interface nsIPrincipal;
+
+webidl Document;
+
+/**
+ * To get a component that implements nsIDocumentLoaderFactory
+ * for a given mimetype, use nsICategoryManager to find an entry
+ * with the mimetype as its name in the category "Gecko-Content-Viewers".
+ * The value of the entry is the contractid of the component.
+ * The component is a service, so use GetService, not CreateInstance to get it.
+ */
+
+[scriptable, uuid(e795239e-9d3c-47c4-b063-9e600fb3b287)]
+interface nsIDocumentLoaderFactory : nsISupports {
+ nsIDocumentViewer createInstance(in string aCommand,
+ in nsIChannel aChannel,
+ in nsILoadGroup aLoadGroup,
+ in ACString aContentType,
+ in nsIDocShell aContainer,
+ in nsISupports aExtraInfo,
+ out nsIStreamListener aDocListenerResult);
+
+ nsIDocumentViewer createInstanceForDocument(in nsISupports aContainer,
+ in Document aDocument,
+ in string aCommand);
+};
diff --git a/docshell/base/nsIDocumentViewer.idl b/docshell/base/nsIDocumentViewer.idl
new file mode 100644
index 0000000000..afbbdfc464
--- /dev/null
+++ b/docshell/base/nsIDocumentViewer.idl
@@ -0,0 +1,318 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDocShell;
+interface nsISHEntry;
+interface nsIPrintSettings;
+webidl Document;
+webidl Node;
+
+%{ C++
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+#include "nsRect.h"
+#include "Units.h"
+
+class nsIWidget;
+class nsPresContext;
+class nsView;
+class nsDOMNavigationTiming;
+namespace mozilla {
+class Encoding;
+class PresShell;
+namespace dom {
+class WindowGlobalChild;
+} // namespace dom
+namespace layout {
+class RemotePrintJobChild;
+} // namespace layout
+} // namespace mozilla
+%}
+
+[ptr] native nsIWidgetPtr(nsIWidget);
+[ref] native nsIntRectRef(nsIntRect);
+[ptr] native nsPresContextPtr(nsPresContext);
+[ptr] native nsViewPtr(nsView);
+[ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming);
+[ptr] native Encoding(const mozilla::Encoding);
+[ptr] native PresShellPtr(mozilla::PresShell);
+[ptr] native RemotePrintJobChildPtr(mozilla::layout::RemotePrintJobChild);
+[ptr] native WindowGlobalChildPtr(mozilla::dom::WindowGlobalChild);
+
+[scriptable, builtinclass, uuid(48118355-e9a5-4452-ab18-59cc426fb817)]
+interface nsIDocumentViewer : nsISupports
+{
+ [noscript] void init(in nsIWidgetPtr aParentWidget,
+ [const] in nsIntRectRef aBounds,
+ in WindowGlobalChildPtr aWindowActor);
+
+ attribute nsIDocShell container;
+
+ [noscript,notxpcom,nostdcall] void loadStart(in Document aDoc);
+ [can_run_script] void loadComplete(in nsresult aStatus);
+ [notxpcom,nostdcall] readonly attribute boolean loadCompleted;
+
+ [notxpcom,nostdcall] readonly attribute boolean isStopped;
+
+ /**
+ * aAction is passed to PermitUnload to indicate what action to take
+ * if a beforeunload handler wants to prompt the user.
+ *
+ * ePrompt: Prompt and return the user's choice (default).
+ * eDontPromptAndDontUnload: Don't prompt and return false (unload not permitted)
+ * if the document (or its children) asks us to prompt.
+ * eDontPromptAndUnload: Don't prompt and return true (unload permitted) no matter what.
+ *
+ * NOTE: Keep this in sync with PermitUnloadAction in WindowGlobalActors.webidl.
+ */
+ cenum PermitUnloadAction : 8 {
+ ePrompt = 0,
+ eDontPromptAndDontUnload = 1,
+ eDontPromptAndUnload = 2
+ };
+
+ /**
+ * The result of dispatching a "beforeunload" event. If `eAllowNavigation`,
+ * no "beforeunload" listener requested to prevent the navigation, or its
+ * request was ignored. If `eRequestBlockNavigation`, a listener did request
+ * to block the navigation, and the user should be prompted.
+ */
+ cenum PermitUnloadResult : 8 {
+ eAllowNavigation = 0,
+ eRequestBlockNavigation = 1,
+ };
+
+ /**
+ * Overload PermitUnload method for C++ consumers with no aPermitUnloadFlags
+ * argument.
+ */
+ %{C++
+ nsresult PermitUnload(bool* canUnload) {
+ return PermitUnload(ePrompt, canUnload);
+ }
+ %}
+
+ /**
+ * Checks if the document wants to prevent unloading by firing beforeunload on
+ * the document.
+ * The result is returned.
+ */
+ boolean permitUnload([optional] in nsIDocumentViewer_PermitUnloadAction aAction);
+
+ /**
+ * Exposes whether we're blocked in a call to permitUnload.
+ */
+ readonly attribute boolean inPermitUnload;
+
+ /**
+ * Dispatches the "beforeunload" event and returns the result, as documented
+ * in the `PermitUnloadResult` enum.
+ */
+ [noscript,nostdcall,notxpcom] nsIDocumentViewer_PermitUnloadResult dispatchBeforeUnload();
+
+ /**
+ * Exposes whether we're in the process of firing the beforeunload event.
+ * In this case, the corresponding docshell will not allow navigation.
+ */
+ readonly attribute boolean beforeUnloadFiring;
+
+ [can_run_script] void pageHide(in boolean isUnload);
+
+ /**
+ * All users of a content viewer are responsible for calling both
+ * close() and destroy(), in that order.
+ *
+ * close() should be called when the load of a new page for the next
+ * content viewer begins, and destroy() should be called when the next
+ * content viewer replaces this one.
+ *
+ * |historyEntry| sets the session history entry for the content viewer. If
+ * this is null, then Destroy() will be called on the document by close().
+ * If it is non-null, the document will not be destroyed, and the following
+ * actions will happen when destroy() is called (*):
+ * - Sanitize() will be called on the viewer's document
+ * - The content viewer will set the contentViewer property on the
+ * history entry, and release its reference (ownership reversal).
+ * - hide() will be called, and no further destruction will happen.
+ *
+ * (*) unless the document is currently being printed, in which case
+ * it will never be saved in session history.
+ *
+ */
+ void close(in nsISHEntry historyEntry);
+ void destroy();
+
+ void stop();
+
+ /**
+ * Returns the same thing as getDocument(), but for use from script
+ * only. C++ consumers should use getDocument().
+ */
+ readonly attribute Document DOMDocument;
+
+ /**
+ * Returns DOMDocument without addrefing.
+ */
+ [noscript,notxpcom,nostdcall] Document getDocument();
+
+ /**
+ * Allows setting the document.
+ */
+ [noscript,nostdcall] void setDocument(in Document aDocument);
+
+ [noscript] void getBounds(in nsIntRectRef aBounds);
+ [noscript] void setBounds([const] in nsIntRectRef aBounds);
+ /**
+ * The 'aFlags' argument to setBoundsWithFlags is a set of these bits.
+ */
+ const unsigned long eDelayResize = 1;
+ [noscript] void setBoundsWithFlags([const] in nsIntRectRef aBounds,
+ in unsigned long aFlags);
+
+ /**
+ * The previous content viewer, which has been |close|d but not
+ * |destroy|ed.
+ */
+ [notxpcom,nostdcall] attribute nsIDocumentViewer previousViewer;
+
+ void move(in long aX, in long aY);
+
+ void show();
+ void hide();
+
+ attribute boolean sticky;
+
+ /**
+ * Attach the content viewer to its DOM window and docshell.
+ * @param aState A state object that might be useful in attaching the DOM
+ * window.
+ * @param aSHEntry The history entry that the content viewer was stored in.
+ * The entry must have the docshells for all of the child
+ * documents stored in its child shell list.
+ */
+ void open(in nsISupports aState, in nsISHEntry aSHEntry);
+
+ /**
+ * Clears the current history entry. This is used if we need to clear out
+ * the saved presentation state.
+ */
+ void clearHistoryEntry();
+
+ /**
+ * Change the layout to view the document with page layout (like print preview), but
+ * dynamic and editable (like Galley layout).
+ */
+ void setPageModeForTesting(in boolean aPageMode,
+ in nsIPrintSettings aPrintSettings);
+
+ /**
+ * Sets the print settings for print / print-previewing a subdocument.
+ */
+ [can_run_script] void setPrintSettingsForSubdocument(in nsIPrintSettings aPrintSettings,
+ in RemotePrintJobChildPtr aRemotePrintJob);
+
+ /**
+ * Get the history entry that this viewer will save itself into when
+ * destroyed. Can return null
+ */
+ readonly attribute nsISHEntry historyEntry;
+
+ /**
+ * Indicates when we're in a state where content shouldn't be allowed to
+ * trigger a tab-modal prompt (as opposed to a window-modal prompt) because
+ * we're part way through some operation (eg beforeunload) that shouldn't be
+ * rentrant if the user closes the tab while the prompt is showing.
+ * See bug 613800.
+ */
+ readonly attribute boolean isTabModalPromptAllowed;
+
+ /**
+ * Returns whether this content viewer is in a hidden state.
+ *
+ * @note Only Gecko internal code should set the attribute!
+ */
+ attribute boolean isHidden;
+
+ // presShell can be null.
+ [notxpcom,nostdcall] readonly attribute PresShellPtr presShell;
+ // presContext can be null.
+ [notxpcom,nostdcall] readonly attribute nsPresContextPtr presContext;
+ // aDocument must not be null.
+ [noscript] void setDocumentInternal(in Document aDocument,
+ in boolean aForceReuseInnerWindow);
+ /**
+ * Find the view to use as the container view for MakeWindow. Returns
+ * null if this will be the root of a view manager hierarchy. In that
+ * case, if mParentWidget is null then this document should not even
+ * be displayed.
+ */
+ [noscript,notxpcom,nostdcall] nsViewPtr findContainerView();
+ /**
+ * Set collector for navigation timing data (load, unload events).
+ */
+ [noscript,notxpcom,nostdcall] void setNavigationTiming(in nsDOMNavigationTimingPtr aTiming);
+
+ /**
+ * The actual full zoom in effect, as modified by the device context.
+ * For a requested full zoom, the device context may choose a slightly
+ * different effectiveFullZoom to accomodate integer rounding of app units
+ * per dev pixel. This property returns the actual zoom amount in use,
+ * though it may not be good user experience to report that a requested zoom
+ * of 90% is actually 89.1%, for example. This value is provided primarily to
+ * support media queries of dppx values, because those queries are matched
+ * against the actual native device pixel ratio and the actual full zoom.
+ *
+ * You should only need this for testing.
+ */
+ readonly attribute float deviceFullZoomForTest;
+
+ /**
+ * Disable entire author style level (including HTML presentation hints),
+ * for this viewer but not any child viewers.
+ */
+ attribute boolean authorStyleDisabled;
+
+ /**
+ * Returns the preferred width and height of the content, constrained to the
+ * given maximum values. If either maxWidth or maxHeight is less than or
+ * equal to zero, that dimension is not constrained.
+ *
+ * If a pref width is provided, it is max'd with the min-content size.
+ *
+ * All input and output values are in CSS pixels.
+ */
+ void getContentSize(in long maxWidth,
+ in long maxHeight,
+ in long prefWidth,
+ out long width,
+ out long height);
+
+%{C++
+ mozilla::Maybe<mozilla::CSSIntSize> GetContentSize(int32_t aMaxWidth = 0, int32_t aMaxHeight = 0, int32_t aPrefWidth = 0) {
+ int32_t w = 0;
+ int32_t h = 0;
+ if (NS_SUCCEEDED(GetContentSize(aMaxWidth, aMaxHeight, aPrefWidth, &w, &h))) {
+ return mozilla::Some(mozilla::CSSIntSize(w, h));
+ }
+ return mozilla::Nothing();
+ }
+%}
+
+ [noscript, notxpcom] Encoding getReloadEncodingAndSource(out int32_t aSource);
+ [noscript, notxpcom] void setReloadEncodingAndSource(in Encoding aEncoding, in int32_t aSource);
+ [noscript, notxpcom] void forgetReloadEncoding();
+};
+
+%{C++
+namespace mozilla {
+namespace dom {
+
+using XPCOMPermitUnloadAction = nsIDocumentViewer::PermitUnloadAction;
+using PermitUnloadResult = nsIDocumentViewer::PermitUnloadResult;
+
+} // namespace dom
+} // namespace mozilla
+%}
diff --git a/docshell/base/nsIDocumentViewerEdit.idl b/docshell/base/nsIDocumentViewerEdit.idl
new file mode 100644
index 0000000000..10ec203df7
--- /dev/null
+++ b/docshell/base/nsIDocumentViewerEdit.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl Node;
+
+[scriptable, uuid(e39a0c2a-5b31-4d57-a971-66ba07fab614)]
+interface nsIDocumentViewerEdit : nsISupports
+{
+ void clearSelection();
+ void selectAll();
+
+ void copySelection();
+ readonly attribute boolean copyable;
+
+ void copyLinkLocation();
+ readonly attribute boolean inLink;
+
+ const long COPY_IMAGE_TEXT = 0x0001;
+ const long COPY_IMAGE_HTML = 0x0002;
+ const long COPY_IMAGE_DATA = 0x0004;
+ const long COPY_IMAGE_ALL = -1;
+ void copyImage(in long aCopyFlags);
+ readonly attribute boolean inImage;
+
+ AString getContents(in string aMimeType, in boolean aSelectionOnly);
+ readonly attribute boolean canGetContents;
+
+ // Set the node that will be the subject of the editing commands above.
+ // Usually this will be the node that was context-clicked.
+ void setCommandNode(in Node aNode);
+};
diff --git a/docshell/base/nsILoadContext.idl b/docshell/base/nsILoadContext.idl
new file mode 100644
index 0000000000..af71b96b34
--- /dev/null
+++ b/docshell/base/nsILoadContext.idl
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: ft=cpp tw=78 sw=2 et ts=2 sts=2 cin
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+webidl Element;
+
+[ref] native OriginAttributes(mozilla::OriginAttributes);
+
+%{C++
+#ifdef MOZILLA_INTERNAL_API
+namespace mozilla {
+class OriginAttributes;
+}
+#endif
+%}
+
+/**
+ * An nsILoadContext represents the context of a load. This interface
+ * can be queried for various information about where the load is
+ * happening.
+ */
+[builtinclass, scriptable, uuid(2813a7a3-d084-4d00-acd0-f76620315c02)]
+interface nsILoadContext : nsISupports
+{
+ /**
+ * associatedWindow is the window with which the load is associated, if any.
+ * Note that the load may be triggered by a document which is different from
+ * the document in associatedWindow, and in fact the source of the load need
+ * not be same-origin with the document in associatedWindow. This attribute
+ * may be null if there is no associated window.
+ */
+ readonly attribute mozIDOMWindowProxy associatedWindow;
+
+ /**
+ * topWindow is the top window which is of same type as associatedWindow.
+ * This is equivalent to associatedWindow.top, but is provided here as a
+ * convenience. All the same caveats as associatedWindow of apply, of
+ * course. This attribute may be null if there is no associated window.
+ */
+ readonly attribute mozIDOMWindowProxy topWindow;
+
+ /**
+ * topFrameElement is the <iframe>, <frame>, or <browser> element which
+ * contains the topWindow with which the load is associated.
+ *
+ * Note that we may have a topFrameElement even when we don't have an
+ * associatedWindow, if the topFrameElement's content lives out of process.
+ * topFrameElement is available in single-process and multiprocess contexts.
+ * Note that topFrameElement may be in chrome even when the nsILoadContext is
+ * associated with content.
+ */
+ readonly attribute Element topFrameElement;
+
+ /**
+ * True if the load context is content (as opposed to chrome). This is
+ * determined based on the type of window the load is performed in, NOT based
+ * on any URIs that might be around.
+ */
+ readonly attribute boolean isContent;
+
+ /*
+ * Attribute that determines if private browsing should be used. May not be
+ * changed after a document has been loaded in this context.
+ */
+ attribute boolean usePrivateBrowsing;
+
+ /**
+ * Attribute that determines if remote (out-of-process) tabs should be used.
+ */
+ readonly attribute boolean useRemoteTabs;
+
+ /**
+ * Determines if out-of-process iframes should be used.
+ */
+ readonly attribute boolean useRemoteSubframes;
+
+ /*
+ * Attribute that determines if tracking protection should be used. May not be
+ * changed after a document has been loaded in this context.
+ */
+ attribute boolean useTrackingProtection;
+
+%{C++
+ /**
+ * De-XPCOMed getter to make call-sites cleaner.
+ */
+ bool UsePrivateBrowsing()
+ {
+ bool usingPB = false;
+ GetUsePrivateBrowsing(&usingPB);
+ return usingPB;
+ }
+
+ bool UseRemoteTabs()
+ {
+ bool usingRT = false;
+ GetUseRemoteTabs(&usingRT);
+ return usingRT;
+ }
+
+ bool UseRemoteSubframes()
+ {
+ bool usingRSF = false;
+ GetUseRemoteSubframes(&usingRSF);
+ return usingRSF;
+ }
+
+ bool UseTrackingProtection()
+ {
+ bool usingTP = false;
+ GetUseTrackingProtection(&usingTP);
+ return usingTP;
+ }
+%}
+
+ /**
+ * Set the private browsing state of the load context, meant to be used internally.
+ */
+ [noscript] void SetPrivateBrowsing(in boolean aInPrivateBrowsing);
+
+ /**
+ * Set the remote tabs state of the load context, meant to be used internally.
+ */
+ [noscript] void SetRemoteTabs(in boolean aUseRemoteTabs);
+
+ /**
+ * Set the remote subframes bit of this load context. Exclusively meant to be used internally.
+ */
+ [noscript] void SetRemoteSubframes(in boolean aUseRemoteSubframes);
+
+ /**
+ * A dictionary of the non-default origin attributes associated with this
+ * nsILoadContext.
+ */
+ [binaryname(ScriptableOriginAttributes), implicit_jscontext]
+ readonly attribute jsval originAttributes;
+
+ /**
+ * The C++ getter for origin attributes.
+ */
+ [noscript, notxpcom] void GetOriginAttributes(out OriginAttributes aAttrs);
+};
diff --git a/docshell/base/nsILoadURIDelegate.idl b/docshell/base/nsILoadURIDelegate.idl
new file mode 100644
index 0000000000..eb5d4cbaf5
--- /dev/null
+++ b/docshell/base/nsILoadURIDelegate.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIPrincipal;
+
+/**
+ * The nsILoadURIDelegate interface.
+ * Used for delegating URI loads to GeckoView's application, e.g., Custom Tabs
+ * or Progressive Web Apps.
+ */
+[scriptable, uuid(78e42d37-a34c-4d96-b901-25385669aba4)]
+interface nsILoadURIDelegate : nsISupports
+{
+ /**
+ * Delegates page load error handling. This may be called for either top-level
+ * loads or subframes.
+ *
+ * @param aURI The URI that failed to load.
+ * @param aError The error code.
+ * @param aErrorModule The error module code.
+
+ * Returns an error page URL to load, or null to show the default error page.
+ * No error page is shown at all if an error is thrown.
+ */
+ nsIURI
+ handleLoadError(in nsIURI aURI, in nsresult aError, in short aErrorModule);
+};
diff --git a/docshell/base/nsIPrivacyTransitionObserver.idl b/docshell/base/nsIPrivacyTransitionObserver.idl
new file mode 100644
index 0000000000..c85d468d33
--- /dev/null
+++ b/docshell/base/nsIPrivacyTransitionObserver.idl
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, function, uuid(b4b1449d-0ef0-47f5-b62e-adc57fd49702)]
+interface nsIPrivacyTransitionObserver : nsISupports
+{
+ void privateModeChanged(in bool enabled);
+};
diff --git a/docshell/base/nsIReflowObserver.idl b/docshell/base/nsIReflowObserver.idl
new file mode 100644
index 0000000000..fb602e2603
--- /dev/null
+++ b/docshell/base/nsIReflowObserver.idl
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "domstubs.idl"
+
+[scriptable, uuid(832e692c-c4a6-11e2-8fd1-dce678957a39)]
+interface nsIReflowObserver : nsISupports
+{
+ /**
+ * Called when an uninterruptible reflow has occurred.
+ *
+ * @param start timestamp when reflow ended, in milliseconds since
+ * navigationStart (accurate to 1/1000 of a ms)
+ * @param end timestamp when reflow ended, in milliseconds since
+ * navigationStart (accurate to 1/1000 of a ms)
+ */
+ void reflow(in DOMHighResTimeStamp start,
+ in DOMHighResTimeStamp end);
+
+ /**
+ * Called when an interruptible reflow has occurred.
+ *
+ * @param start timestamp when reflow ended, in milliseconds since
+ * navigationStart (accurate to 1/1000 of a ms)
+ * @param end timestamp when reflow ended, in milliseconds since
+ * navigationStart (accurate to 1/1000 of a ms)
+ */
+ void reflowInterruptible(in DOMHighResTimeStamp start,
+ in DOMHighResTimeStamp end);
+};
diff --git a/docshell/base/nsIRefreshURI.idl b/docshell/base/nsIRefreshURI.idl
new file mode 100644
index 0000000000..a4a578a344
--- /dev/null
+++ b/docshell/base/nsIRefreshURI.idl
@@ -0,0 +1,52 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIChannel;
+interface nsIPrincipal;
+interface nsIURI;
+
+[scriptable, uuid(a5e61a3c-51bd-45be-ac0c-e87b71860656)]
+interface nsIRefreshURI : nsISupports {
+ /**
+ * Load a uri after waiting for aMillis milliseconds (as a result of a
+ * meta refresh). If the docshell is busy loading a page currently, the
+ * refresh request will be queued and executed when the current load
+ * finishes.
+ *
+ * @param aUri The uri to refresh.
+ * @param aPrincipal The triggeringPrincipal for the refresh load
+ * May be null, in which case the principal of current document will be
+ * applied.
+ * @param aMillis The number of milliseconds to wait.
+ */
+ void refreshURI(in nsIURI aURI, in nsIPrincipal aPrincipal,
+ in unsigned long aMillis);
+
+ /**
+ * Loads a URI immediately as if it were a meta refresh.
+ *
+ * @param aURI The URI to refresh.
+ * @param aPrincipal The triggeringPrincipal for the refresh load
+ * May be null, in which case the principal of current document will be
+ * applied.
+ * @param aMillis The number of milliseconds by which this refresh would
+ * be delayed if it were not being forced.
+ */
+ void forceRefreshURI(in nsIURI aURI, in nsIPrincipal aPrincipal,
+ in unsigned long aMillis);
+
+ /**
+ * Cancels all timer loads.
+ */
+ void cancelRefreshURITimers();
+
+ /**
+ * True when there are pending refreshes, false otherwise.
+ */
+ readonly attribute boolean refreshPending;
+};
diff --git a/docshell/base/nsIScrollObserver.h b/docshell/base/nsIScrollObserver.h
new file mode 100644
index 0000000000..9ff89002f0
--- /dev/null
+++ b/docshell/base/nsIScrollObserver.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIScrollObserver_h___
+#define nsIScrollObserver_h___
+
+#include "nsISupports.h"
+#include "Units.h"
+
+// Must be kept in sync with xpcom/rust/xpcom/src/interfaces/nonidl.rs
+#define NS_ISCROLLOBSERVER_IID \
+ { \
+ 0xaa5026eb, 0x2f88, 0x4026, { \
+ 0xa4, 0x6b, 0xf4, 0x59, 0x6b, 0x4e, 0xdf, 0x00 \
+ } \
+ }
+
+class nsIScrollObserver : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISCROLLOBSERVER_IID)
+
+ /**
+ * Called when the scroll position of some element has changed.
+ */
+ virtual void ScrollPositionChanged() = 0;
+
+ /**
+ * Called when an async panning/zooming transform has started being applied
+ * and passed the scroll offset
+ */
+ MOZ_CAN_RUN_SCRIPT virtual void AsyncPanZoomStarted(){};
+
+ /**
+ * Called when an async panning/zooming transform is no longer applied
+ * and passed the scroll offset
+ */
+ MOZ_CAN_RUN_SCRIPT virtual void AsyncPanZoomStopped(){};
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIScrollObserver, NS_ISCROLLOBSERVER_IID)
+
+#endif /* nsIScrollObserver_h___ */
diff --git a/docshell/base/nsITooltipListener.idl b/docshell/base/nsITooltipListener.idl
new file mode 100644
index 0000000000..0498aca57d
--- /dev/null
+++ b/docshell/base/nsITooltipListener.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * An optional interface for embedding clients wishing to receive
+ * notifications for when a tooltip should be displayed or removed.
+ * The embedder implements this interface on the web browser chrome
+ * object associated with the window that notifications are required
+ * for.
+ *
+ * @see nsITooltipTextProvider
+ */
+[scriptable, uuid(44b78386-1dd2-11b2-9ad2-e4eee2ca1916)]
+interface nsITooltipListener : nsISupports
+{
+ /**
+ * Called when a tooltip should be displayed.
+ *
+ * @param aXCoords The tooltip left edge X coordinate.
+ * @param aYCoords The tooltip top edge Y coordinate.
+ * @param aTipText The text to display in the tooltip, typically obtained
+ * from the TITLE attribute of the node (or containing parent)
+ * over which the pointer has been positioned.
+ * @param aTipDir The direction (ltr or rtl) in which to display the text
+ *
+ * @note
+ * Coordinates are specified in device pixels, relative to the top-left
+ * corner of the browser area.
+ *
+ * @return <code>NS_OK</code> if the tooltip was displayed.
+ */
+ void onShowTooltip(in long aXCoords, in long aYCoords, in AString aTipText,
+ in AString aTipDir);
+
+ /**
+ * Called when the tooltip should be hidden, either because the pointer
+ * has moved or the tooltip has timed out.
+ */
+ void onHideTooltip();
+};
diff --git a/docshell/base/nsITooltipTextProvider.idl b/docshell/base/nsITooltipTextProvider.idl
new file mode 100644
index 0000000000..3afaddbe2a
--- /dev/null
+++ b/docshell/base/nsITooltipTextProvider.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl Node;
+
+/**
+ * An interface implemented by a tooltip text provider service. This
+ * service is called to discover what tooltip text is associated
+ * with the node that the pointer is positioned over.
+ *
+ * Embedders may implement and register their own tooltip text provider
+ * service if they wish to provide different tooltip text.
+ *
+ * The default service returns the text stored in the TITLE
+ * attribute of the node or a containing parent.
+ *
+ * @note
+ * The tooltip text provider service is registered with the contract
+ * defined in NS_TOOLTIPTEXTPROVIDER_CONTRACTID.
+ *
+ * @see nsITooltipListener
+ * @see nsIComponentManager
+ * @see Node
+ */
+[scriptable, uuid(b128a1e6-44f3-4331-8fbe-5af360ff21ee)]
+interface nsITooltipTextProvider : nsISupports
+{
+ /**
+ * Called to obtain the tooltip text for a node.
+ *
+ * @arg aNode The node to obtain the text from.
+ * @arg aText The tooltip text.
+ * @arg aDirection The text direction (ltr or rtl) to use
+ *
+ * @return <CODE>PR_TRUE</CODE> if tooltip text is associated
+ * with the node and was returned in the aText argument;
+ * <CODE>PR_FALSE</CODE> otherwise.
+ */
+ boolean getNodeText(in Node aNode, out wstring aText, out wstring aDirection);
+};
diff --git a/docshell/base/nsIURIFixup.idl b/docshell/base/nsIURIFixup.idl
new file mode 100644
index 0000000000..2261c0cb40
--- /dev/null
+++ b/docshell/base/nsIURIFixup.idl
@@ -0,0 +1,204 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIInputStream;
+interface nsIDNSListener;
+webidl BrowsingContext;
+
+/**
+ * Interface indicating what we found/corrected when fixing up a URI
+ */
+[scriptable, uuid(4819f183-b532-4932-ac09-b309cd853be7)]
+interface nsIURIFixupInfo : nsISupports
+{
+ /**
+ * Consumer that asked for fixed up URI.
+ */
+ attribute BrowsingContext consumer;
+
+ /**
+ * Our best guess as to what URI the consumer will want. Might
+ * be null if we couldn't salvage anything (for instance, because
+ * the input was invalid as a URI and FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
+ * was not passed)
+ */
+ attribute nsIURI preferredURI;
+
+ /**
+ * The fixed-up original input, *never* using a keyword search.
+ * (might be null if the original input was not recoverable as
+ * a URL, e.g. "foo bar"!)
+ */
+ attribute nsIURI fixedURI;
+
+ /**
+ * The name of the keyword search provider used to provide a keyword search;
+ * empty string if no keyword search was done.
+ */
+ attribute AString keywordProviderName;
+
+ /**
+ * The keyword as used for the search (post trimming etc.)
+ * empty string if no keyword search was done.
+ */
+ attribute AString keywordAsSent;
+
+ /**
+ * Whether there was no protocol at all and we had to add one in the first place.
+ */
+ attribute boolean wasSchemelessInput;
+
+ /**
+ * Whether we changed the protocol instead of using one from the input as-is.
+ */
+ attribute boolean fixupChangedProtocol;
+
+ /**
+ * Whether we created an alternative URI. We might have added a prefix and/or
+ * suffix, the contents of which are controlled by the
+ * browser.fixup.alternate.prefix and .suffix prefs, with the defaults being
+ * "www." and ".com", respectively.
+ */
+ attribute boolean fixupCreatedAlternateURI;
+
+ /**
+ * The original input
+ */
+ attribute AUTF8String originalInput;
+
+ /**
+ * The POST data to submit with the returned URI (see nsISearchSubmission).
+ */
+ attribute nsIInputStream postData;
+};
+
+
+/**
+ * Interface implemented by objects capable of fixing up strings into URIs
+ */
+[scriptable, uuid(1da7e9d4-620b-4949-849a-1cd6077b1b2d)]
+interface nsIURIFixup : nsISupports
+{
+ /** No fixup flags. */
+ const unsigned long FIXUP_FLAG_NONE = 0;
+
+ /**
+ * Allow the fixup to use a keyword lookup service to complete the URI.
+ * The fixup object implementer should honour this flag and only perform
+ * any lengthy keyword (or search) operation if it is set.
+ */
+ const unsigned long FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP = 1;
+
+ /**
+ * Tell the fixup to make an alternate URI from the input URI, for example
+ * to turn foo into www.foo.com.
+ */
+ const unsigned long FIXUP_FLAGS_MAKE_ALTERNATE_URI = 2;
+
+ /*
+ * Set when the fixup happens in a private context, the used search engine
+ * may differ in this case. Not all consumers care about this, because they
+ * may not want the url to be transformed in a search.
+ */
+ const unsigned long FIXUP_FLAG_PRIVATE_CONTEXT = 4;
+
+ /*
+ * Fix common scheme typos.
+ */
+ const unsigned long FIXUP_FLAG_FIX_SCHEME_TYPOS = 8;
+
+ /**
+ * Tries to converts the specified string into a URI, first attempting
+ * to correct any errors in the syntax or other vagaries.
+ * It returns information about what it corrected
+ * (e.g. whether we could rescue the URI or "just" generated a keyword
+ * search URI instead).
+ *
+ * @param aURIText Candidate URI.
+ * @param aFixupFlags Flags that govern ways the URI may be fixed up.
+ * Defaults to FIXUP_FLAG_NONE.
+ */
+ nsIURIFixupInfo getFixupURIInfo(in AUTF8String aURIText,
+ [optional] in unsigned long aFixupFlags);
+
+ /**
+ * Convert load flags from nsIWebNavigation to URI fixup flags for use in
+ * getFixupURIInfo.
+ *
+ * @param aURIText Candidate URI; used for determining whether to
+ * allow keyword lookups.
+ * @param aDocShellFlags Load flags from nsIDocShell to convert.
+ */
+ unsigned long webNavigationFlagsToFixupFlags(
+ in AUTF8String aURIText, in unsigned long aDocShellFlags);
+
+ /**
+ * Converts the specified keyword string into a URI. Note that it's the
+ * caller's responsibility to check whether keywords are enabled and
+ * whether aKeyword is a sensible keyword.
+ *
+ * @param aKeyword The keyword string to convert into a URI
+ * @param aIsPrivateContext Whether this is invoked from a private context.
+ */
+ nsIURIFixupInfo keywordToURI(in AUTF8String aKeyword,
+ [optional] in boolean aIsPrivateContext);
+
+ /**
+ * Given a uri-like string with a protocol, attempt to fix and convert it
+ * into an instance of nsIURIFixupInfo.
+ *
+ * Differently from getFixupURIInfo, this assumes the input string is an
+ * http/https uri, and can add a prefix and/or suffix to its hostname.
+ *
+ * The scheme will be changed to the scheme defined in
+ * "browser.fixup.alternate.protocol", which is by default, https.
+ *
+ * If the prefix and suffix of the host are missing, it will add them to
+ * the host using the preferences "browser.fixup.alternate.prefix" and
+ * "browser.fixup.alternate.suffix" as references.
+ *
+ * If a hostname suffix is present, but the URI doesn't contain a prefix,
+ * it will add the prefix via "browser.fixup.alternate.prefix"
+ *
+ * @param aUriString The URI to fixup and convert.
+ * @returns nsIURIFixupInfo
+ * A nsIURIFixupInfo object with the property fixedURI
+ * which contains the modified URI.
+ * @throws NS_ERROR_FAILURE
+ * If aUriString is undefined, or the scheme is not
+ * http/https.
+ */
+ nsIURIFixupInfo forceHttpFixup(in AUTF8String aUriString);
+
+ /**
+ * With the host associated with the URI, use nsIDNSService to determine
+ * if an IP address can be found for this host. This method will ignore checking
+ * hosts that are IP addresses. If the host does not contain any periods, depending
+ * on the browser.urlbar.dnsResolveFullyQualifiedNames preference value, a period
+ * may be appended in order to make it a fully qualified domain name.
+ *
+ * @param aURI The URI to parse and pass into the DNS lookup.
+ * @param aListener The listener when the result from the lookup is available.
+ * @param aOriginAttributes The originAttributes to pass the DNS lookup.
+ * @throws NS_ERROR_FAILURE if aURI does not have a displayHost or asciiHost.
+ */
+ void checkHost(in nsIURI aURI,
+ in nsIDNSListener aListener,
+ [optional] in jsval aOriginAttributes);
+
+ /**
+ * Returns true if the specified domain is known and false otherwise.
+ * A known domain is relevant when we have a single word and can't be
+ * sure whether to treat the word as a host name or should instead be
+ * treated as a search term.
+ *
+ * @param aDomain A domain name to query.
+ */
+ bool isDomainKnown(in AUTF8String aDomain);
+};
diff --git a/docshell/base/nsIWebNavigation.idl b/docshell/base/nsIWebNavigation.idl
new file mode 100644
index 0000000000..1800e7312e
--- /dev/null
+++ b/docshell/base/nsIWebNavigation.idl
@@ -0,0 +1,415 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIInputStream;
+interface nsISHistory;
+interface nsIURI;
+interface nsIPrincipal;
+interface nsIChildSHistory;
+webidl Document;
+
+%{ C++
+#include "mozilla/dom/ChildSHistory.h"
+namespace mozilla {
+namespace dom {
+struct LoadURIOptions;
+} // namespace dom
+} // namespace mozilla
+%}
+
+[ref] native LoadURIOptionsRef(const mozilla::dom::LoadURIOptions);
+
+/**
+ * The nsIWebNavigation interface defines an interface for navigating the web.
+ * It provides methods and attributes to direct an object to navigate to a new
+ * location, stop or restart an in process load, or determine where the object
+ * has previously gone.
+ *
+ * Even though this is builtinclass, most of the interface is also implemented
+ * in RemoteWebNavigation, so if this interface changes, the implementation
+ * there may also need to change.
+ */
+[scriptable, builtinclass, uuid(3ade79d4-8cb9-4952-b18d-4f9b63ca0d31)]
+interface nsIWebNavigation : nsISupports
+{
+ /**
+ * Indicates if the object can go back. If true this indicates that
+ * there is back session history available for navigation.
+ */
+ readonly attribute boolean canGoBack;
+
+ /**
+ * Indicates if the object can go forward. If true this indicates that
+ * there is forward session history available for navigation
+ */
+ readonly attribute boolean canGoForward;
+
+ /**
+ * Tells the object to navigate to the previous session history item. When a
+ * page is loaded from session history, all content is loaded from the cache
+ * (if available) and page state (such as form values and scroll position) is
+ * restored.
+ *
+ * @param {boolean} aRequireUserInteraction
+ * Tells goBack to skip history items that did not record any user
+ * interaction on their corresponding document while they were active.
+ * This means in case of multiple entries mapping to the same document,
+ * each entry has to have been flagged with user interaction separately.
+ * If no items have user interaction, the function will fall back
+ * to the first session history entry.
+ *
+ * @param {boolean} aUserActivation
+ * Tells goBack that the call was triggered by a user action (e.g.:
+ * The user clicked the back button).
+ *
+ * @throw NS_ERROR_UNEXPECTED
+ * Indicates that the call was unexpected at this time, which implies
+ * that canGoBack is false.
+ */
+ void goBack([optional] in boolean aRequireUserInteraction, [optional] in boolean aUserActivation);
+
+ /**
+ * Tells the object to navigate to the next session history item. When a
+ * page is loaded from session history, all content is loaded from the cache
+ * (if available) and page state (such as form values and scroll position) is
+ * restored.
+ *
+ * @param {boolean} aRequireUserInteraction
+ * Tells goForward to skip history items that did not record any user
+ * interaction on their corresponding document while they were active.
+ * This means in case of multiple entries mapping to the same document,
+ * each entry has to have been flagged with user interaction separately.
+ * If no items have user interaction, the function will fall back
+ * to the latest session history entry.
+ *
+ * @param {boolean} aUserActivation
+ * Tells goForward that the call was triggered by a user action (e.g.:
+ * The user clicked the forward button).
+ *
+ * @throw NS_ERROR_UNEXPECTED
+ * Indicates that the call was unexpected at this time, which implies
+ * that canGoForward is false.
+ */
+ void goForward([optional] in boolean aRequireUserInteraction, [optional] in boolean aUserActivation);
+
+ /**
+ * Tells the object to navigate to the session history item at a given index.
+ *
+ * @param {boolean} aUserActivation
+ * Tells goForward that the call was triggered by a user action (e.g.:
+ * The user clicked the forward button).
+ *
+ * @throw NS_ERROR_UNEXPECTED
+ * Indicates that the call was unexpected at this time, which implies
+ * that session history entry at the given index does not exist.
+ */
+ void gotoIndex(in long index, [optional] in boolean aUserActivation);
+
+ /****************************************************************************
+ * The following flags may be bitwise combined to form the load flags
+ * parameter passed to either the loadURI or reload method. Some of these
+ * flags are only applicable to loadURI.
+ */
+
+ /**
+ * This flags defines the range of bits that may be specified. Flags
+ * outside this range may be used, but may not be passed to Reload().
+ */
+ const unsigned long LOAD_FLAGS_MASK = 0xffff;
+
+ /**
+ * This is the default value for the load flags parameter.
+ */
+ const unsigned long LOAD_FLAGS_NONE = 0x0000;
+
+ /**
+ * Flags 0x1, 0x2, 0x4, 0x8 are reserved for internal use by
+ * nsIWebNavigation implementations for now.
+ */
+
+ /**
+ * This flag specifies that the load should have the semantics of an HTML
+ * Meta-refresh tag (i.e., that the cache should be bypassed). This flag
+ * is only applicable to loadURI.
+ * XXX the meaning of this flag is poorly defined.
+ * XXX no one uses this, so we should probably deprecate and remove it.
+ */
+ const unsigned long LOAD_FLAGS_IS_REFRESH = 0x0010;
+
+ /**
+ * This flag specifies that the load should have the semantics of a link
+ * click. This flag is only applicable to loadURI.
+ * XXX the meaning of this flag is poorly defined.
+ */
+ const unsigned long LOAD_FLAGS_IS_LINK = 0x0020;
+
+ /**
+ * This flag specifies that history should not be updated. This flag is only
+ * applicable to loadURI.
+ */
+ const unsigned long LOAD_FLAGS_BYPASS_HISTORY = 0x0040;
+
+ /**
+ * This flag specifies that any existing history entry should be replaced.
+ * This flag is only applicable to loadURI.
+ */
+ const unsigned long LOAD_FLAGS_REPLACE_HISTORY = 0x0080;
+
+ /**
+ * This flag specifies that the local web cache should be bypassed, but an
+ * intermediate proxy cache could still be used to satisfy the load.
+ */
+ const unsigned long LOAD_FLAGS_BYPASS_CACHE = 0x0100;
+
+ /**
+ * This flag specifies that any intermediate proxy caches should be bypassed
+ * (i.e., that the content should be loaded from the origin server).
+ */
+ const unsigned long LOAD_FLAGS_BYPASS_PROXY = 0x0200;
+
+ /**
+ * This flag specifies that a reload was triggered as a result of detecting
+ * an incorrect character encoding while parsing a previously loaded
+ * document.
+ */
+ const unsigned long LOAD_FLAGS_CHARSET_CHANGE = 0x0400;
+
+ /**
+ * If this flag is set, Stop() will be called before the load starts
+ * and will stop both content and network activity (the default is to
+ * only stop network activity). Effectively, this passes the
+ * STOP_CONTENT flag to Stop(), in addition to the STOP_NETWORK flag.
+ */
+ const unsigned long LOAD_FLAGS_STOP_CONTENT = 0x0800;
+
+ /**
+ * A hint this load was prompted by an external program: take care!
+ */
+ const unsigned long LOAD_FLAGS_FROM_EXTERNAL = 0x1000;
+
+ /**
+ * This flag specifies that this is the first load in this object.
+ * Set with care, since setting incorrectly can cause us to assume that
+ * nothing was actually loaded in this object if the load ends up being
+ * handled by an external application. This flag must not be passed to
+ * Reload.
+ */
+ const unsigned long LOAD_FLAGS_FIRST_LOAD = 0x4000;
+
+ /**
+ * This flag specifies that the load should not be subject to popup
+ * blocking checks. This flag must not be passed to Reload.
+ */
+ const unsigned long LOAD_FLAGS_ALLOW_POPUPS = 0x8000;
+
+ /**
+ * This flag specifies that the URI classifier should not be checked for
+ * this load. This flag must not be passed to Reload.
+ */
+ const unsigned long LOAD_FLAGS_BYPASS_CLASSIFIER = 0x10000;
+
+ /**
+ * Force relevant cookies to be sent with this load even if normally they
+ * wouldn't be.
+ */
+ const unsigned long LOAD_FLAGS_FORCE_ALLOW_COOKIES = 0x20000;
+
+ /**
+ * Prevent the owner principal from being inherited for this load.
+ */
+ const unsigned long LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL = 0x40000;
+
+ /**
+ * Overwrite the returned error code with a specific result code
+ * when an error page is displayed.
+ */
+ const unsigned long LOAD_FLAGS_ERROR_LOAD_CHANGES_RV = 0x80000;
+
+ /**
+ * This flag specifies that the URI may be submitted to a third-party
+ * server for correction. This should only be applied to non-sensitive
+ * URIs entered by users. This flag must not be passed to Reload.
+ */
+ const unsigned long LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP = 0x100000;
+
+ /**
+ * This flag specifies that common scheme typos should be corrected.
+ */
+ const unsigned long LOAD_FLAGS_FIXUP_SCHEME_TYPOS = 0x200000;
+
+ /**
+ * Allows a top-level data: navigation to occur. E.g. view-image
+ * is an explicit user action which should be allowed.
+ */
+ const unsigned long LOAD_FLAGS_FORCE_ALLOW_DATA_URI = 0x400000;
+
+ /**
+ * This load is the result of an HTTP redirect.
+ */
+ const unsigned long LOAD_FLAGS_IS_REDIRECT = 0x800000;
+
+ /**
+ * These flags force TRR modes 1 or 3 for the load.
+ */
+ const unsigned long LOAD_FLAGS_DISABLE_TRR = 0x1000000;
+ const unsigned long LOAD_FLAGS_FORCE_TRR = 0x2000000;
+
+ /**
+ * This load should bypass the LoadURIDelegate.loadUri.
+ */
+ const unsigned long LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 0x4000000;
+
+ /**
+ * This load has a user activation. (e.g: reload button was clicked)
+ */
+ const unsigned long LOAD_FLAGS_USER_ACTIVATION = 0x8000000;
+
+ /**
+ * Loads a given URI. This will give priority to loading the requested URI
+ * in the object implementing this interface. If it can't be loaded here
+ * however, the URI dispatcher will go through its normal process of content
+ * loading.
+ *
+ * @param aURI
+ * The URI to load.
+ * @param aLoadURIOptions
+ * A JSObject defined in LoadURIOptions.webidl holding info like e.g.
+ * the triggeringPrincipal, the referrer info.
+ */
+ [implicit_jscontext, binaryname(LoadURIFromScript)]
+ void loadURI(in nsIURI aURI,
+ in jsval aLoadURIOptions);
+
+ /**
+ * Parse / fix up a URI out of the string and load it.
+ * This will give priority to loading the requested URI
+ * in the object implementing this interface. If it can't be loaded here
+ * however, the URI dispatcher will go through its normal process of content
+ * loading.
+ *
+ * @param aURIString
+ * The URI string to load. For HTTP and FTP URLs and possibly others,
+ * characters above U+007F will be converted to UTF-8 and then URL-
+ * escaped per the rules of RFC 2396.
+ * This method may use nsIURIFixup to try to fix up typos etc. in the
+ * input string based on the load flag arguments in aLoadURIOptions.
+ * It can even convert the input to a search results page using the
+ * default search service.
+ * If you have an nsIURI anyway, prefer calling `loadURI`, above.
+ * @param aLoadURIOptions
+ * A JSObject defined in LoadURIOptions.webidl holding info like e.g.
+ * the triggeringPrincipal, the referrer info.
+ */
+ [implicit_jscontext, binaryname(FixupAndLoadURIStringFromScript)]
+ void fixupAndLoadURIString(in AString aURIString,
+ in jsval aLoadURIOptions);
+
+ /**
+ * A C++ friendly version of loadURI
+ */
+ [nostdcall, binaryname(LoadURI)]
+ void binaryLoadURI(in nsIURI aURI,
+ in LoadURIOptionsRef aLoadURIOptions);
+
+ /**
+ * A C++ friendly version of fixupAndLoadURIString
+ */
+ [nostdcall, binaryname(FixupAndLoadURIString)]
+ void binaryFixupAndLoadURIString(in AString aURIString,
+ in LoadURIOptionsRef aLoadURIOptions);
+
+ /**
+ * Tells the Object to reload the current page. There may be cases where the
+ * user will be asked to confirm the reload (for example, when it is
+ * determined that the request is non-idempotent).
+ *
+ * @param aReloadFlags
+ * Flags modifying load behaviour. This parameter is a bitwise
+ * combination of the Load Flags defined above. (Undefined bits are
+ * reserved for future use.) Generally you will pass LOAD_FLAGS_NONE
+ * for this parameter.
+ *
+ * @throw NS_BINDING_ABORTED
+ * Indicating that the user canceled the reload.
+ */
+ void reload(in unsigned long aReloadFlags);
+
+ /****************************************************************************
+ * The following flags may be passed as the stop flags parameter to the stop
+ * method defined on this interface.
+ */
+
+ /**
+ * This flag specifies that all network activity should be stopped. This
+ * includes both active network loads and pending META-refreshes.
+ */
+ const unsigned long STOP_NETWORK = 0x01;
+
+ /**
+ * This flag specifies that all content activity should be stopped. This
+ * includes animated images, plugins and pending Javascript timeouts.
+ */
+ const unsigned long STOP_CONTENT = 0x02;
+
+ /**
+ * This flag specifies that all activity should be stopped.
+ */
+ const unsigned long STOP_ALL = 0x03;
+
+ /**
+ * Stops a load of a URI.
+ *
+ * @param aStopFlags
+ * This parameter is one of the stop flags defined above.
+ */
+ void stop(in unsigned long aStopFlags);
+
+ /**
+ * Retrieves the current DOM document for the frame, or lazily creates a
+ * blank document if there is none. This attribute never returns null except
+ * for unexpected error situations.
+ */
+ readonly attribute Document document;
+
+ /**
+ * The currently loaded URI or null.
+ */
+ readonly attribute nsIURI currentURI;
+
+ /**
+ * The session history object used by this web navigation instance. This
+ * object will be a mozilla::dom::ChildSHistory object, but is returned as
+ * nsISupports so it can be called from JS code.
+ */
+ [binaryname(SessionHistoryXPCOM)]
+ readonly attribute nsISupports sessionHistory;
+
+ %{ C++
+ /**
+ * Get the session history object used by this nsIWebNavigation instance.
+ * Use this method instead of the XPCOM method when getting the
+ * SessionHistory from C++ code.
+ */
+ already_AddRefed<mozilla::dom::ChildSHistory>
+ GetSessionHistory()
+ {
+ nsCOMPtr<nsISupports> history;
+ GetSessionHistoryXPCOM(getter_AddRefs(history));
+ return history.forget()
+ .downcast<mozilla::dom::ChildSHistory>();
+ }
+ %}
+
+ /**
+ * Resume a load which has been redirected from another process.
+ *
+ * A negative |aHistoryIndex| value corresponds to a non-history load being
+ * resumed.
+ */
+ void resumeRedirectedLoad(in unsigned long long aLoadIdentifier,
+ in long aHistoryIndex);
+};
diff --git a/docshell/base/nsIWebNavigationInfo.idl b/docshell/base/nsIWebNavigationInfo.idl
new file mode 100644
index 0000000000..0c3d07a8ed
--- /dev/null
+++ b/docshell/base/nsIWebNavigationInfo.idl
@@ -0,0 +1,55 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * The nsIWebNavigationInfo interface exposes a way to get information
+ * on the capabilities of Gecko webnavigation objects.
+ */
+[scriptable, uuid(62a93afb-93a1-465c-84c8-0432264229de)]
+interface nsIWebNavigationInfo : nsISupports
+{
+ /**
+ * Returned by isTypeSupported to indicate lack of support for a type.
+ * @note this is guaranteed not to change, so that boolean tests can be done
+ * on the return value if isTypeSupported to detect whether a type is
+ * supported at all.
+ */
+ const unsigned long UNSUPPORTED = 0;
+
+ /**
+ * Returned by isTypeSupported to indicate that a type is supported as an
+ * image.
+ */
+ const unsigned long IMAGE = 1;
+
+ /**
+ * Returned by isTypeSupported to indicate that a type is a special NPAPI
+ * plugin that render as a transparent region (we do not support NPAPI
+ * plugins).
+ */
+ const unsigned long FALLBACK = 2;
+
+ /**
+ * @note Other return types may be added here in the future as they become
+ * relevant.
+ */
+
+ /**
+ * Returned by isTypeSupported to indicate that a type is supported via some
+ * other means.
+ */
+ const unsigned long OTHER = 1 << 15;
+
+ /**
+ * Query whether aType is supported.
+ * @param aType the MIME type in question.
+ * @return an enum value indicating whether and how aType is supported.
+ * @note This method may rescan plugins to ensure that they're properly
+ * registered for the types they support.
+ */
+ unsigned long isTypeSupported(in ACString aType);
+};
diff --git a/docshell/base/nsIWebPageDescriptor.idl b/docshell/base/nsIWebPageDescriptor.idl
new file mode 100644
index 0000000000..866cb54e6b
--- /dev/null
+++ b/docshell/base/nsIWebPageDescriptor.idl
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "nsISupports.idl"
+interface nsIDocShell;
+
+/**
+ * The nsIWebPageDescriptor interface allows content being displayed in one
+ * window to be loaded into another window without refetching it from the
+ * network.
+ */
+
+[scriptable, uuid(6f30b676-3710-4c2c-80b1-0395fb26516e)]
+interface nsIWebPageDescriptor : nsISupports
+{
+ /**
+ * Tells the object to load the page that otherDocShell is currently loading,
+ * or has loaded already, as view source, with the url being `aURL`.
+ *
+ * @throws NS_ERROR_FAILURE - NS_ERROR_INVALID_POINTER
+ */
+ void loadPageAsViewSource(in nsIDocShell otherDocShell, in AString aURL);
+
+
+ /**
+ * Retrieves the page descriptor for the curent document.
+ * @note, currentDescriptor is currently always an nsISHEntry object or null.
+ */
+ readonly attribute nsISupports currentDescriptor;
+};
diff --git a/docshell/base/nsPingListener.cpp b/docshell/base/nsPingListener.cpp
new file mode 100644
index 0000000000..094074a0b9
--- /dev/null
+++ b/docshell/base/nsPingListener.cpp
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsPingListener.h"
+
+#include "mozilla/Encoding.h"
+#include "mozilla/Preferences.h"
+
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/Document.h"
+
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIInputStream.h"
+#include "nsIProtocolHandler.h"
+#include "nsIUploadChannel2.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsWhitespaceTokenizer.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_ISUPPORTS(nsPingListener, nsIStreamListener, nsIRequestObserver)
+
+//*****************************************************************************
+// <a ping> support
+//*****************************************************************************
+
+#define PREF_PINGS_ENABLED "browser.send_pings"
+#define PREF_PINGS_MAX_PER_LINK "browser.send_pings.max_per_link"
+#define PREF_PINGS_REQUIRE_SAME_HOST "browser.send_pings.require_same_host"
+
+// Check prefs to see if pings are enabled and if so what restrictions might
+// be applied.
+//
+// @param maxPerLink
+// This parameter returns the number of pings that are allowed per link click
+//
+// @param requireSameHost
+// This parameter returns true if pings are restricted to the same host as
+// the document in which the click occurs. If the same host restriction is
+// imposed, then we still allow for pings to cross over to different
+// protocols and ports for flexibility and because it is not possible to send
+// a ping via FTP.
+//
+// @returns
+// true if pings are enabled and false otherwise.
+//
+static bool PingsEnabled(int32_t* aMaxPerLink, bool* aRequireSameHost) {
+ bool allow = Preferences::GetBool(PREF_PINGS_ENABLED, false);
+
+ *aMaxPerLink = 1;
+ *aRequireSameHost = true;
+
+ if (allow) {
+ Preferences::GetInt(PREF_PINGS_MAX_PER_LINK, aMaxPerLink);
+ Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST, aRequireSameHost);
+ }
+
+ return allow;
+}
+
+// We wait this many milliseconds before killing the ping channel...
+#define PING_TIMEOUT 10000
+
+static void OnPingTimeout(nsITimer* aTimer, void* aClosure) {
+ nsILoadGroup* loadGroup = static_cast<nsILoadGroup*>(aClosure);
+ if (loadGroup) {
+ loadGroup->Cancel(NS_ERROR_ABORT);
+ }
+}
+
+struct MOZ_STACK_CLASS SendPingInfo {
+ int32_t numPings;
+ int32_t maxPings;
+ bool requireSameHost;
+ nsIURI* target;
+ nsIReferrerInfo* referrerInfo;
+ nsIDocShell* docShell;
+};
+
+static void SendPing(void* aClosure, nsIContent* aContent, nsIURI* aURI,
+ nsIIOService* aIOService) {
+ SendPingInfo* info = static_cast<SendPingInfo*>(aClosure);
+ if (info->maxPings > -1 && info->numPings >= info->maxPings) {
+ return;
+ }
+
+ Document* doc = aContent->OwnerDoc();
+
+ nsCOMPtr<nsIChannel> chan;
+ NS_NewChannel(getter_AddRefs(chan), aURI, doc,
+ info->requireSameHost
+ ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
+ : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_PING,
+ nullptr, // PerformanceStorage
+ nullptr, // aLoadGroup
+ nullptr, // aCallbacks
+ nsIRequest::LOAD_NORMAL, // aLoadFlags,
+ aIOService);
+
+ if (!chan) {
+ return;
+ }
+
+ // Don't bother caching the result of this URI load, but do not exempt
+ // it from Safe Browsing.
+ chan->SetLoadFlags(nsIRequest::INHIBIT_CACHING);
+
+ nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
+ if (!httpChan) {
+ return;
+ }
+
+ // This is needed in order for 3rd-party cookie blocking to work.
+ nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan);
+ nsresult rv;
+ if (httpInternal) {
+ rv = httpInternal->SetDocumentURI(doc->GetDocumentURI());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ rv = httpChan->SetRequestMethod("POST"_ns);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Remove extraneous request headers (to reduce request size)
+ rv = httpChan->SetRequestHeader("accept"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = httpChan->SetRequestHeader("accept-language"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = httpChan->SetRequestHeader("accept-encoding"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Always send a Ping-To header.
+ nsAutoCString pingTo;
+ if (NS_SUCCEEDED(info->target->GetSpec(pingTo))) {
+ rv = httpChan->SetRequestHeader("Ping-To"_ns, pingTo, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ nsCOMPtr<nsIScriptSecurityManager> sm =
+ do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+
+ if (sm && info->referrerInfo) {
+ nsCOMPtr<nsIURI> referrer = info->referrerInfo->GetOriginalReferrer();
+ bool referrerIsSecure = false;
+ uint32_t flags = nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY;
+ if (referrer) {
+ rv = NS_URIChainHasFlags(referrer, flags, &referrerIsSecure);
+ }
+
+ // Default to sending less data if NS_URIChainHasFlags() fails.
+ referrerIsSecure = NS_FAILED(rv) || referrerIsSecure;
+
+ bool isPrivateWin = false;
+ if (doc) {
+ isPrivateWin =
+ doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0;
+ }
+
+ bool sameOrigin = NS_SUCCEEDED(
+ sm->CheckSameOriginURI(referrer, aURI, false, isPrivateWin));
+
+ // If both the address of the document containing the hyperlink being
+ // audited and "ping URL" have the same origin or the document containing
+ // the hyperlink being audited was not retrieved over an encrypted
+ // connection, send a Ping-From header.
+ if (sameOrigin || !referrerIsSecure) {
+ nsAutoCString pingFrom;
+ if (NS_SUCCEEDED(referrer->GetSpec(pingFrom))) {
+ rv = httpChan->SetRequestHeader("Ping-From"_ns, pingFrom, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ // If the document containing the hyperlink being audited was not retrieved
+ // over an encrypted connection and its address does not have the same
+ // origin as "ping URL", send a referrer.
+ if (!sameOrigin && !referrerIsSecure && info->referrerInfo) {
+ rv = httpChan->SetReferrerInfo(info->referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(httpChan);
+ if (!uploadChan) {
+ return;
+ }
+
+ constexpr auto uploadData = "PING"_ns;
+
+ nsCOMPtr<nsIInputStream> uploadStream;
+ rv = NS_NewCStringInputStream(getter_AddRefs(uploadStream), uploadData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ uploadChan->ExplicitSetUploadStream(uploadStream, "text/ping"_ns,
+ uploadData.Length(), "POST"_ns, false);
+
+ // The channel needs to have a loadgroup associated with it, so that we can
+ // cancel the channel and any redirected channels it may create.
+ nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID);
+ if (!loadGroup) {
+ return;
+ }
+ nsCOMPtr<nsIInterfaceRequestor> callbacks = do_QueryInterface(info->docShell);
+ loadGroup->SetNotificationCallbacks(callbacks);
+ chan->SetLoadGroup(loadGroup);
+
+ RefPtr<nsPingListener> pingListener = new nsPingListener();
+ chan->AsyncOpen(pingListener);
+
+ // Even if AsyncOpen failed, we still count this as a successful ping. It's
+ // possible that AsyncOpen may have failed after triggering some background
+ // process that may have written something to the network.
+ info->numPings++;
+
+ // Prevent ping requests from stalling and never being garbage collected...
+ if (NS_FAILED(pingListener->StartTimeout(doc->GetDocGroup()))) {
+ // If we failed to setup the timer, then we should just cancel the channel
+ // because we won't be able to ensure that it goes away in a timely manner.
+ chan->Cancel(NS_ERROR_ABORT);
+ return;
+ }
+ // if the channel openend successfully, then make the pingListener hold
+ // a strong reference to the loadgroup which is released in ::OnStopRequest
+ pingListener->SetLoadGroup(loadGroup);
+}
+
+typedef void (*ForEachPingCallback)(void* closure, nsIContent* content,
+ nsIURI* uri, nsIIOService* ios);
+
+static void ForEachPing(nsIContent* aContent, ForEachPingCallback aCallback,
+ void* aClosure) {
+ // NOTE: Using nsIDOMHTMLAnchorElement::GetPing isn't really worth it here
+ // since we'd still need to parse the resulting string. Instead, we
+ // just parse the raw attribute. It might be nice if the content node
+ // implemented an interface that exposed an enumeration of nsIURIs.
+
+ // Make sure we are dealing with either an <A> or <AREA> element in the HTML
+ // or XHTML namespace.
+ if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area)) {
+ return;
+ }
+
+ nsAutoString value;
+ aContent->AsElement()->GetAttr(nsGkAtoms::ping, value);
+ if (value.IsEmpty()) {
+ return;
+ }
+
+ nsCOMPtr<nsIIOService> ios = do_GetIOService();
+ if (!ios) {
+ return;
+ }
+
+ Document* doc = aContent->OwnerDoc();
+ nsAutoCString charset;
+ doc->GetDocumentCharacterSet()->Name(charset);
+
+ nsWhitespaceTokenizer tokenizer(value);
+
+ while (tokenizer.hasMoreTokens()) {
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), tokenizer.nextToken(), charset.get(),
+ aContent->GetBaseURI());
+ // if we can't generate a valid URI, then there is nothing to do
+ if (!uri) {
+ continue;
+ }
+ // Explicitly not allow loading data: URIs
+ if (!net::SchemeIsData(uri)) {
+ aCallback(aClosure, aContent, uri, ios);
+ }
+ }
+}
+
+// Spec: http://whatwg.org/specs/web-apps/current-work/#ping
+/*static*/ void nsPingListener::DispatchPings(nsIDocShell* aDocShell,
+ nsIContent* aContent,
+ nsIURI* aTarget,
+ nsIReferrerInfo* aReferrerInfo) {
+ SendPingInfo info;
+
+ if (!PingsEnabled(&info.maxPings, &info.requireSameHost)) {
+ return;
+ }
+ if (info.maxPings == 0) {
+ return;
+ }
+
+ info.numPings = 0;
+ info.target = aTarget;
+ info.referrerInfo = aReferrerInfo;
+ info.docShell = aDocShell;
+
+ ForEachPing(aContent, SendPing, &info);
+}
+
+nsPingListener::~nsPingListener() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+nsresult nsPingListener::StartTimeout(DocGroup* aDocGroup) {
+ NS_ENSURE_ARG(aDocGroup);
+
+ return NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTimer), OnPingTimeout, mLoadGroup, PING_TIMEOUT,
+ nsITimer::TYPE_ONE_SHOT, "nsPingListener::StartTimeout",
+ GetMainThreadSerialEventTarget());
+}
+
+NS_IMETHODIMP
+nsPingListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
+
+NS_IMETHODIMP
+nsPingListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t result;
+ return aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result);
+}
+
+NS_IMETHODIMP
+nsPingListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ mLoadGroup = nullptr;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ return NS_OK;
+}
diff --git a/docshell/base/nsPingListener.h b/docshell/base/nsPingListener.h
new file mode 100644
index 0000000000..7cf6ff98b5
--- /dev/null
+++ b/docshell/base/nsPingListener.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsPingListener_h__
+#define nsPingListener_h__
+
+#include "nsIStreamListener.h"
+#include "nsIReferrerInfo.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+namespace dom {
+class DocGroup;
+}
+} // namespace mozilla
+
+class nsIContent;
+class nsIDocShell;
+class nsILoadGroup;
+class nsITimer;
+class nsIURI;
+
+class nsPingListener final : public nsIStreamListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ nsPingListener() {}
+
+ void SetLoadGroup(nsILoadGroup* aLoadGroup) { mLoadGroup = aLoadGroup; }
+
+ nsresult StartTimeout(mozilla::dom::DocGroup* aDocGroup);
+
+ static void DispatchPings(nsIDocShell* aDocShell, nsIContent* aContent,
+ nsIURI* aTarget, nsIReferrerInfo* aReferrerInfo);
+
+ private:
+ ~nsPingListener();
+
+ nsCOMPtr<nsILoadGroup> mLoadGroup;
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+#endif /* nsPingListener_h__ */
diff --git a/docshell/base/nsRefreshTimer.cpp b/docshell/base/nsRefreshTimer.cpp
new file mode 100644
index 0000000000..867e3ba1cb
--- /dev/null
+++ b/docshell/base/nsRefreshTimer.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsRefreshTimer.h"
+
+#include "nsIURI.h"
+#include "nsIPrincipal.h"
+
+#include "nsDocShell.h"
+
+NS_IMPL_ADDREF(nsRefreshTimer)
+NS_IMPL_RELEASE(nsRefreshTimer)
+
+NS_INTERFACE_MAP_BEGIN(nsRefreshTimer)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+nsRefreshTimer::nsRefreshTimer(nsDocShell* aDocShell, nsIURI* aURI,
+ nsIPrincipal* aPrincipal, int32_t aDelay)
+ : mDocShell(aDocShell),
+ mURI(aURI),
+ mPrincipal(aPrincipal),
+ mDelay(aDelay) {}
+
+nsRefreshTimer::~nsRefreshTimer() {}
+
+NS_IMETHODIMP
+nsRefreshTimer::Notify(nsITimer* aTimer) {
+ NS_ASSERTION(mDocShell, "DocShell is somehow null");
+
+ if (mDocShell && aTimer) {
+ // Get the delay count to determine load type
+ uint32_t delay = 0;
+ aTimer->GetDelay(&delay);
+ mDocShell->ForceRefreshURIFromTimer(mURI, mPrincipal, delay, aTimer);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRefreshTimer::GetName(nsACString& aName) {
+ aName.AssignLiteral("nsRefreshTimer");
+ return NS_OK;
+}
diff --git a/docshell/base/nsRefreshTimer.h b/docshell/base/nsRefreshTimer.h
new file mode 100644
index 0000000000..423f8807eb
--- /dev/null
+++ b/docshell/base/nsRefreshTimer.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsRefreshTimer_h__
+#define nsRefreshTimer_h__
+
+#include "nsINamed.h"
+#include "nsITimer.h"
+
+#include "nsCOMPtr.h"
+
+class nsDocShell;
+class nsIURI;
+class nsIPrincipal;
+
+class nsRefreshTimer : public nsITimerCallback, public nsINamed {
+ public:
+ nsRefreshTimer(nsDocShell* aDocShell, nsIURI* aURI, nsIPrincipal* aPrincipal,
+ int32_t aDelay);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ int32_t GetDelay() { return mDelay; }
+
+ RefPtr<nsDocShell> mDocShell;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ int32_t mDelay;
+
+ private:
+ virtual ~nsRefreshTimer();
+};
+
+#endif /* nsRefreshTimer_h__ */
diff --git a/docshell/base/nsWebNavigationInfo.cpp b/docshell/base/nsWebNavigationInfo.cpp
new file mode 100644
index 0000000000..cb989a33f1
--- /dev/null
+++ b/docshell/base/nsWebNavigationInfo.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsWebNavigationInfo.h"
+
+#include "nsServiceManagerUtils.h"
+#include "nsIDocumentLoaderFactory.h"
+#include "nsContentUtils.h"
+#include "imgLoader.h"
+
+NS_IMPL_ISUPPORTS(nsWebNavigationInfo, nsIWebNavigationInfo)
+
+NS_IMETHODIMP
+nsWebNavigationInfo::IsTypeSupported(const nsACString& aType,
+ uint32_t* aIsTypeSupported) {
+ MOZ_ASSERT(aIsTypeSupported, "null out param?");
+
+ *aIsTypeSupported = IsTypeSupported(aType);
+ return NS_OK;
+}
+
+uint32_t nsWebNavigationInfo::IsTypeSupported(const nsACString& aType) {
+ // We want to claim that the type for PDF documents is unsupported,
+ // so that the internal PDF viewer's stream converted will get used.
+ if (aType.LowerCaseEqualsLiteral("application/pdf") &&
+ nsContentUtils::IsPDFJSEnabled()) {
+ return nsIWebNavigationInfo::UNSUPPORTED;
+ }
+
+ const nsCString& flatType = PromiseFlatCString(aType);
+ return IsTypeSupportedInternal(flatType);
+}
+
+uint32_t nsWebNavigationInfo::IsTypeSupportedInternal(const nsCString& aType) {
+ nsContentUtils::DocumentViewerType vtype = nsContentUtils::TYPE_UNSUPPORTED;
+
+ nsCOMPtr<nsIDocumentLoaderFactory> docLoaderFactory =
+ nsContentUtils::FindInternalDocumentViewer(aType, &vtype);
+
+ switch (vtype) {
+ case nsContentUtils::TYPE_UNSUPPORTED:
+ return nsIWebNavigationInfo::UNSUPPORTED;
+
+ case nsContentUtils::TYPE_FALLBACK:
+ return nsIWebNavigationInfo::FALLBACK;
+
+ case nsContentUtils::TYPE_UNKNOWN:
+ return nsIWebNavigationInfo::OTHER;
+
+ case nsContentUtils::TYPE_CONTENT:
+ // XXXbz we only need this because images register for the same
+ // contractid as documents, so we can't tell them apart based on
+ // contractid.
+ if (imgLoader::SupportImageWithMimeType(aType)) {
+ return nsIWebNavigationInfo::IMAGE;
+ }
+ return nsIWebNavigationInfo::OTHER;
+ }
+
+ return nsIWebNavigationInfo::UNSUPPORTED;
+}
diff --git a/docshell/base/nsWebNavigationInfo.h b/docshell/base/nsWebNavigationInfo.h
new file mode 100644
index 0000000000..97b07c825e
--- /dev/null
+++ b/docshell/base/nsWebNavigationInfo.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsWebNavigationInfo_h__
+#define nsWebNavigationInfo_h__
+
+#include "nsIWebNavigationInfo.h"
+#include "nsCOMPtr.h"
+#include "nsICategoryManager.h"
+#include "nsStringFwd.h"
+#include "mozilla/Attributes.h"
+
+class nsWebNavigationInfo final : public nsIWebNavigationInfo {
+ public:
+ nsWebNavigationInfo() {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIWEBNAVIGATIONINFO
+
+ static uint32_t IsTypeSupported(const nsACString& aType);
+
+ private:
+ ~nsWebNavigationInfo() {}
+
+ // Check whether aType is supported, and returns an nsIWebNavigationInfo
+ // constant.
+ static uint32_t IsTypeSupportedInternal(const nsCString& aType);
+};
+
+#endif // nsWebNavigationInfo_h__
diff --git a/docshell/build/components.conf b/docshell/build/components.conf
new file mode 100644
index 0000000000..691277a0bb
--- /dev/null
+++ b/docshell/build/components.conf
@@ -0,0 +1,194 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+about_pages = [
+ 'about',
+ 'addons',
+ 'buildconfig',
+ 'certificate',
+ 'checkerboard',
+ 'config',
+ 'crashcontent',
+ 'crashparent',
+ 'crashgpu',
+ 'crashextensions',
+ 'credits',
+ 'httpsonlyerror',
+ 'license',
+ 'logging',
+ 'logo',
+ 'memory',
+ 'mozilla',
+ 'neterror',
+ 'networking',
+ 'performance',
+ 'processes',
+ 'serviceworkers',
+ 'srcdoc',
+ 'support',
+ 'telemetry',
+ 'translations',
+ 'url-classifier',
+ 'webrtc',
+]
+
+if defined('MOZ_CRASHREPORTER'):
+ about_pages.append('crashes')
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android':
+ about_pages.append('profiles')
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ about_pages.append('third-party')
+ about_pages.append('windows-messages')
+if not defined('MOZ_GLEAN_ANDROID'):
+ about_pages.append('glean')
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android' and buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'windows':
+ about_pages.append('webauthn')
+
+Headers = ['/docshell/build/nsDocShellModule.h']
+
+InitFunc = 'mozilla::InitDocShellModule'
+UnloadFunc = 'mozilla::UnloadDocShellModule'
+
+Classes = [
+ {
+ 'name': 'DocLoader',
+ 'cid': '{057b04d0-0ccf-11d2-beba-00805f8a66dc}',
+ 'contract_ids': ['@mozilla.org/docloaderservice;1'],
+ 'type': 'nsDocLoader',
+ 'headers': ['nsDocLoader.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'name': 'URIFixup',
+ 'js_name': 'uriFixup',
+ 'cid': '{c6cf88b7-452e-47eb-bdc9-86e3561648ef}',
+ 'contract_ids': ['@mozilla.org/docshell/uri-fixup;1'],
+ 'interfaces': ['nsIURIFixup'],
+ 'esModule': 'resource://gre/modules/URIFixup.sys.mjs',
+ 'singleton': True,
+ 'constructor': 'URIFixup',
+ },
+ {
+ 'cid': '{33d75835-722f-42c0-89cc-44f328e56a86}',
+ 'contract_ids': ['@mozilla.org/docshell/uri-fixup-info;1'],
+ 'esModule': 'resource://gre/modules/URIFixup.sys.mjs',
+ 'constructor': 'URIFixupInfo',
+ },
+ {
+ 'cid': '{56ebedd4-6ccf-48e8-bdae-adc77f044567}',
+ 'contract_ids': [
+ '@mozilla.org/network/protocol/about;1?what=%s' % path
+ for path in about_pages
+ ],
+ 'legacy_constructor': 'nsAboutRedirector::Create',
+ 'headers': ['/docshell/base/nsAboutRedirector.h'],
+ },
+ {
+ 'name': 'ExternalProtocolHandler',
+ 'cid': '{bd6390c8-fbea-11d4-98f6-001083010e9b}',
+ 'contract_ids': ['@mozilla.org/network/protocol;1?name=default'],
+ 'type': 'nsExternalProtocolHandler',
+ 'headers': ['/uriloader/exthandler/nsExternalProtocolHandler.h'],
+ 'protocol_config': {
+ 'scheme': 'default',
+ 'flags': [
+ 'URI_NORELATIVE',
+ 'URI_NOAUTH',
+ 'URI_LOADABLE_BY_ANYONE',
+ 'URI_NON_PERSISTABLE',
+ 'URI_DOES_NOT_RETURN_DATA',
+ ],
+ 'default_port': 0,
+ },
+ 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS,
+ },
+ {
+ 'cid': '{95790842-75a0-430d-98bf-f5ce3788ea6d}',
+ 'contract_ids': ['@mozilla.org/ospermissionrequest;1'],
+ 'type': 'nsOSPermissionRequest',
+ 'headers': ['nsOSPermissionRequest.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'name': 'Prefetch',
+ 'cid': '{6b8bdffc-3394-417d-be83-a81b7c0f63bf}',
+ 'contract_ids': ['@mozilla.org/prefetch-service;1'],
+ 'type': 'nsPrefetchService',
+ 'headers': ['/uriloader/prefetch/nsPrefetchService.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'cid': '{c4b6fb7c-bfb1-49dc-a65f-035796524b53}',
+ 'contract_ids': ['@mozilla.org/uriloader/handler-service;1'],
+ 'type': 'nsIHandlerService',
+ 'headers': ['ContentHandlerService.h'],
+ 'constructor': 'mozilla::dom::ContentHandlerService::Create',
+ },
+ {
+ 'cid': '{bc0017e3-2438-47be-a567-41db58f17627}',
+ 'contract_ids': ['@mozilla.org/uriloader/local-handler-app;1'],
+ 'type': 'PlatformLocalHandlerApp_t',
+ 'headers': ['/uriloader/exthandler/nsLocalHandlerApp.h'],
+ },
+ {
+ 'name': 'URILoader',
+ 'cid': '{9f6d5d40-90e7-11d3-af80-00a024ffc08c}',
+ 'contract_ids': ['@mozilla.org/uriloader;1'],
+ 'type': 'nsURILoader',
+ 'headers': ['nsURILoader.h'],
+ },
+ {
+ 'cid': '{f30bc0a2-958b-4287-bf62-ce38ba0c811e}',
+ 'contract_ids': ['@mozilla.org/webnavigation-info;1'],
+ 'type': 'nsWebNavigationInfo',
+ 'headers': ['/docshell/base/nsWebNavigationInfo.h'],
+ },
+]
+
+if defined('MOZ_ENABLE_DBUS'):
+ Classes += [
+ {
+ 'name': 'DBusHandlerApp',
+ 'cid': '{6c3c274b-4cbf-4bb5-a635-05ad2cbb6535}',
+ 'contract_ids': ['@mozilla.org/uriloader/dbus-handler-app;1'],
+ 'type': 'nsDBusHandlerApp',
+ 'headers': ['/uriloader/exthandler/nsDBusHandlerApp.h'],
+ },
+ ]
+
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'android':
+ Classes += [
+ # Android has its own externel-helper-app-service, so we omit
+ # that here for nsExternalHelperAppService.
+ {
+ 'cid': '{a7f800e0-4306-11d4-98d0-001083010e9b}',
+ 'contract_ids': [
+ '@mozilla.org/mime;1',
+ '@mozilla.org/uriloader/external-protocol-service;1',
+ ],
+ 'type': 'nsExternalHelperAppService',
+ 'constructor': 'nsExternalHelperAppService::GetSingleton',
+ 'headers': ['nsExternalHelperAppService.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS,
+ },
+ ]
+else:
+ Classes += [
+ {
+ 'cid': '{a7f800e0-4306-11d4-98d0-001083010e9b}',
+ 'contract_ids': [
+ '@mozilla.org/mime;1',
+ '@mozilla.org/uriloader/external-helper-app-service;1',
+ '@mozilla.org/uriloader/external-protocol-service;1',
+ ],
+ 'type': 'nsExternalHelperAppService',
+ 'constructor': 'nsExternalHelperAppService::GetSingleton',
+ 'headers': ['nsExternalHelperAppService.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS,
+ },
+ ]
diff --git a/docshell/build/moz.build b/docshell/build/moz.build
new file mode 100644
index 0000000000..d9fd81848e
--- /dev/null
+++ b/docshell/build/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "nsDocShellCID.h",
+]
+
+SOURCES += [
+ "nsDocShellModule.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/docshell/shistory",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/docshell/build/nsDocShellCID.h b/docshell/build/nsDocShellCID.h
new file mode 100644
index 0000000000..9a6a90d87a
--- /dev/null
+++ b/docshell/build/nsDocShellCID.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellCID_h__
+#define nsDocShellCID_h__
+
+/**
+ * A contract that can be used to get a service that provides
+ * meta-information about nsIWebNavigation objects' capabilities.
+ * @implements nsIWebNavigationInfo
+ */
+#define NS_WEBNAVIGATION_INFO_CONTRACTID "@mozilla.org/webnavigation-info;1"
+
+/**
+ * Contract ID to obtain the IHistory interface. This is a non-scriptable
+ * interface used to interact with history in an asynchronous manner.
+ */
+#define NS_IHISTORY_CONTRACTID "@mozilla.org/browser/history;1"
+
+/**
+ * An observer service topic that can be listened to to catch creation
+ * of content browsing areas (both toplevel ones and subframes). The
+ * subject of the notification will be the nsIWebNavigation being
+ * created. At this time the additional data wstring is not defined
+ * to be anything in particular.
+ */
+#define NS_WEBNAVIGATION_CREATE "webnavigation-create"
+
+/**
+ * An observer service topic that can be listened to to catch creation
+ * of chrome browsing areas (both toplevel ones and subframes). The
+ * subject of the notification will be the nsIWebNavigation being
+ * created. At this time the additional data wstring is not defined
+ * to be anything in particular.
+ */
+#define NS_CHROME_WEBNAVIGATION_CREATE "chrome-webnavigation-create"
+
+/**
+ * An observer service topic that can be listened to to catch destruction
+ * of content browsing areas (both toplevel ones and subframes). The
+ * subject of the notification will be the nsIWebNavigation being
+ * destroyed. At this time the additional data wstring is not defined
+ * to be anything in particular.
+ */
+#define NS_WEBNAVIGATION_DESTROY "webnavigation-destroy"
+
+/**
+ * An observer service topic that can be listened to to catch destruction
+ * of chrome browsing areas (both toplevel ones and subframes). The
+ * subject of the notification will be the nsIWebNavigation being
+ * destroyed. At this time the additional data wstring is not defined
+ * to be anything in particular.
+ */
+#define NS_CHROME_WEBNAVIGATION_DESTROY "chrome-webnavigation-destroy"
+
+#endif // nsDocShellCID_h__
diff --git a/docshell/build/nsDocShellModule.cpp b/docshell/build/nsDocShellModule.cpp
new file mode 100644
index 0000000000..8602497a57
--- /dev/null
+++ b/docshell/build/nsDocShellModule.cpp
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+
+// session history
+#include "nsSHEntryShared.h"
+#include "nsSHistory.h"
+
+namespace mozilla {
+
+// The one time initialization for this module
+nsresult InitDocShellModule() {
+ mozilla::dom::BrowsingContext::Init();
+
+ return NS_OK;
+}
+
+void UnloadDocShellModule() { nsSHistory::Shutdown(); }
+
+} // namespace mozilla
diff --git a/docshell/build/nsDocShellModule.h b/docshell/build/nsDocShellModule.h
new file mode 100644
index 0000000000..c64f3ad8a9
--- /dev/null
+++ b/docshell/build/nsDocShellModule.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDocShellModule_h
+#define nsDocShellModule_h
+
+#include "nscore.h"
+
+namespace mozilla {
+
+nsresult InitDocShellModule();
+
+void UnloadDocShellModule();
+
+} // namespace mozilla
+
+#endif
diff --git a/docshell/moz.build b/docshell/moz.build
new file mode 100644
index 0000000000..5a14b9dff8
--- /dev/null
+++ b/docshell/moz.build
@@ -0,0 +1,48 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Navigation")
+
+if CONFIG["MOZ_BUILD_APP"] == "browser":
+ DEFINES["MOZ_BUILD_APP_IS_BROWSER"] = True
+
+DIRS += [
+ "base",
+ "shistory",
+ "build",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "test/unit/xpcshell.toml",
+ "test/unit_ipc/xpcshell.toml",
+]
+
+MOCHITEST_MANIFESTS += [
+ "test/iframesandbox/mochitest.toml",
+ "test/mochitest/mochitest.toml",
+ "test/navigation/mochitest.toml",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "test/chrome/chrome.toml",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.toml",
+ "test/navigation/browser.toml",
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.docshell.test.chrome += [
+ "test/chrome/215405_nocache.html",
+ "test/chrome/215405_nocache.html^headers^",
+ "test/chrome/215405_nostore.html",
+ "test/chrome/215405_nostore.html^headers^",
+ "test/chrome/allowContentRetargeting.sjs",
+ "test/chrome/blue.png",
+ "test/chrome/bug89419.sjs",
+ "test/chrome/red.png",
+]
diff --git a/docshell/shistory/ChildSHistory.cpp b/docshell/shistory/ChildSHistory.cpp
new file mode 100644
index 0000000000..9148491718
--- /dev/null
+++ b/docshell/shistory/ChildSHistory.cpp
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ChildSHistory.h"
+#include "mozilla/dom/ChildSHistoryBinding.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentFrameMessageManager.h"
+#include "nsIXULRuntime.h"
+#include "nsComponentManagerUtils.h"
+#include "nsSHEntry.h"
+#include "nsSHistory.h"
+#include "nsDocShell.h"
+#include "nsXULAppAPI.h"
+
+extern mozilla::LazyLogModule gSHLog;
+
+namespace mozilla {
+namespace dom {
+
+ChildSHistory::ChildSHistory(BrowsingContext* aBrowsingContext)
+ : mBrowsingContext(aBrowsingContext) {}
+
+ChildSHistory::~ChildSHistory() {
+ if (mHistory) {
+ static_cast<nsSHistory*>(mHistory.get())->SetBrowsingContext(nullptr);
+ }
+}
+
+void ChildSHistory::SetBrowsingContext(BrowsingContext* aBrowsingContext) {
+ mBrowsingContext = aBrowsingContext;
+}
+
+void ChildSHistory::SetIsInProcess(bool aIsInProcess) {
+ if (!aIsInProcess) {
+ MOZ_ASSERT_IF(mozilla::SessionHistoryInParent(), !mHistory);
+ if (!mozilla::SessionHistoryInParent()) {
+ RemovePendingHistoryNavigations();
+ if (mHistory) {
+ static_cast<nsSHistory*>(mHistory.get())->SetBrowsingContext(nullptr);
+ mHistory = nullptr;
+ }
+ }
+
+ return;
+ }
+
+ if (mHistory || mozilla::SessionHistoryInParent()) {
+ return;
+ }
+
+ mHistory = new nsSHistory(mBrowsingContext);
+}
+
+int32_t ChildSHistory::Count() {
+ if (mozilla::SessionHistoryInParent()) {
+ uint32_t length = mLength;
+ for (uint32_t i = 0; i < mPendingSHistoryChanges.Length(); ++i) {
+ length += mPendingSHistoryChanges[i].mLengthDelta;
+ }
+
+ return length;
+ }
+ return mHistory->GetCount();
+}
+
+int32_t ChildSHistory::Index() {
+ if (mozilla::SessionHistoryInParent()) {
+ uint32_t index = mIndex;
+ for (uint32_t i = 0; i < mPendingSHistoryChanges.Length(); ++i) {
+ index += mPendingSHistoryChanges[i].mIndexDelta;
+ }
+
+ return index;
+ }
+ int32_t index;
+ mHistory->GetIndex(&index);
+ return index;
+}
+
+nsID ChildSHistory::AddPendingHistoryChange() {
+ int32_t indexDelta = 1;
+ int32_t lengthDelta = (Index() + indexDelta) - (Count() - 1);
+ return AddPendingHistoryChange(indexDelta, lengthDelta);
+}
+
+nsID ChildSHistory::AddPendingHistoryChange(int32_t aIndexDelta,
+ int32_t aLengthDelta) {
+ nsID changeID = nsID::GenerateUUID();
+ PendingSHistoryChange change = {changeID, aIndexDelta, aLengthDelta};
+ mPendingSHistoryChanges.AppendElement(change);
+ return changeID;
+}
+
+void ChildSHistory::SetIndexAndLength(uint32_t aIndex, uint32_t aLength,
+ const nsID& aChangeID) {
+ mIndex = aIndex;
+ mLength = aLength;
+ mPendingSHistoryChanges.RemoveElementsBy(
+ [aChangeID](const PendingSHistoryChange& aChange) {
+ return aChange.mChangeID == aChangeID;
+ });
+}
+
+void ChildSHistory::Reload(uint32_t aReloadFlags, ErrorResult& aRv) {
+ if (mozilla::SessionHistoryInParent()) {
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsISHistory> shistory =
+ mBrowsingContext->Canonical()->GetSessionHistory();
+ if (shistory) {
+ aRv = shistory->Reload(aReloadFlags);
+ }
+ } else {
+ ContentChild::GetSingleton()->SendHistoryReload(mBrowsingContext,
+ aReloadFlags);
+ }
+
+ return;
+ }
+ nsCOMPtr<nsISHistory> shistory = mHistory;
+ aRv = shistory->Reload(aReloadFlags);
+}
+
+bool ChildSHistory::CanGo(int32_t aOffset) {
+ CheckedInt<int32_t> index = Index();
+ index += aOffset;
+ if (!index.isValid()) {
+ return false;
+ }
+ return index.value() < Count() && index.value() >= 0;
+}
+
+void ChildSHistory::Go(int32_t aOffset, bool aRequireUserInteraction,
+ bool aUserActivation, ErrorResult& aRv) {
+ CheckedInt<int32_t> index = Index();
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ ("ChildSHistory::Go(%d), current index = %d", aOffset, index.value()));
+ if (aRequireUserInteraction && aOffset != -1 && aOffset != 1) {
+ NS_ERROR(
+ "aRequireUserInteraction may only be used with an offset of -1 or 1");
+ aRv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ while (true) {
+ index += aOffset;
+ if (!index.isValid()) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Check for user interaction if desired, except for the first and last
+ // history entries. We compare with >= to account for the case where
+ // aOffset >= Count().
+ if (!aRequireUserInteraction || index.value() >= Count() - 1 ||
+ index.value() <= 0) {
+ break;
+ }
+ if (mHistory && mHistory->HasUserInteractionAtIndex(index.value())) {
+ break;
+ }
+ }
+
+ GotoIndex(index.value(), aOffset, aRequireUserInteraction, aUserActivation,
+ aRv);
+}
+
+void ChildSHistory::AsyncGo(int32_t aOffset, bool aRequireUserInteraction,
+ bool aUserActivation, CallerType aCallerType,
+ ErrorResult& aRv) {
+ CheckedInt<int32_t> index = Index();
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("ChildSHistory::AsyncGo(%d), current index = %d", aOffset,
+ index.value()));
+ nsresult rv = mBrowsingContext->CheckLocationChangeRateLimit(aCallerType);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("Rejected"));
+ aRv.Throw(rv);
+ return;
+ }
+
+ RefPtr<PendingAsyncHistoryNavigation> asyncNav =
+ new PendingAsyncHistoryNavigation(this, aOffset, aRequireUserInteraction,
+ aUserActivation);
+ mPendingNavigations.insertBack(asyncNav);
+ NS_DispatchToCurrentThread(asyncNav.forget());
+}
+
+void ChildSHistory::GotoIndex(int32_t aIndex, int32_t aOffset,
+ bool aRequireUserInteraction,
+ bool aUserActivation, ErrorResult& aRv) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("ChildSHistory::GotoIndex(%d, %d), epoch %" PRIu64, aIndex, aOffset,
+ mHistoryEpoch));
+ if (mozilla::SessionHistoryInParent()) {
+ if (!mPendingEpoch) {
+ mPendingEpoch = true;
+ RefPtr<ChildSHistory> self(this);
+ NS_DispatchToCurrentThread(
+ NS_NewRunnableFunction("UpdateEpochRunnable", [self] {
+ self->mHistoryEpoch++;
+ self->mPendingEpoch = false;
+ }));
+ }
+
+ nsCOMPtr<nsISHistory> shistory = mHistory;
+ RefPtr<BrowsingContext> bc = mBrowsingContext;
+ bc->HistoryGo(
+ aOffset, mHistoryEpoch, aRequireUserInteraction, aUserActivation,
+ [shistory](Maybe<int32_t>&& aRequestedIndex) {
+ // FIXME Should probably only do this for non-fission.
+ if (aRequestedIndex.isSome() && shistory) {
+ shistory->InternalSetRequestedIndex(aRequestedIndex.value());
+ }
+ });
+ } else {
+ nsCOMPtr<nsISHistory> shistory = mHistory;
+ aRv = shistory->GotoIndex(aIndex, aUserActivation);
+ }
+}
+
+void ChildSHistory::RemovePendingHistoryNavigations() {
+ // Per the spec, this generally shouldn't remove all navigations - it
+ // depends if they're in the same document family or not. We don't do
+ // that. Also with SessionHistoryInParent, this can only abort AsyncGo's
+ // that have not yet been sent to the parent - see discussion of point
+ // 2.2 in comments in nsDocShell::UpdateURLAndHistory()
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("ChildSHistory::RemovePendingHistoryNavigations: %zu",
+ mPendingNavigations.length()));
+ mPendingNavigations.clear();
+}
+
+void ChildSHistory::EvictLocalDocumentViewers() {
+ if (!mozilla::SessionHistoryInParent()) {
+ mHistory->EvictAllDocumentViewers();
+ }
+}
+
+nsISHistory* ChildSHistory::GetLegacySHistory(ErrorResult& aError) {
+ if (mozilla::SessionHistoryInParent()) {
+ aError.ThrowTypeError(
+ "legacySHistory is not available with session history in the parent.");
+ return nullptr;
+ }
+
+ MOZ_RELEASE_ASSERT(mHistory);
+ return mHistory;
+}
+
+nsISHistory* ChildSHistory::LegacySHistory() {
+ IgnoredErrorResult ignore;
+ nsISHistory* shistory = GetLegacySHistory(ignore);
+ MOZ_RELEASE_ASSERT(shistory);
+ return shistory;
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChildSHistory)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ChildSHistory)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ChildSHistory)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ChildSHistory)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ChildSHistory)
+ if (tmp->mHistory) {
+ static_cast<nsSHistory*>(tmp->mHistory.get())->SetBrowsingContext(nullptr);
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowsingContext, mHistory)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ChildSHistory)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowsingContext, mHistory)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+JSObject* ChildSHistory::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ChildSHistory_Binding::Wrap(cx, this, aGivenProto);
+}
+
+nsISupports* ChildSHistory::GetParentObject() const {
+ return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/docshell/shistory/ChildSHistory.h b/docshell/shistory/ChildSHistory.h
new file mode 100644
index 0000000000..7187240471
--- /dev/null
+++ b/docshell/shistory/ChildSHistory.h
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * ChildSHistory represents a view of session history from a child process. It
+ * exposes getters for some cached history state, and mutators which are
+ * implemented by communicating with the actual history storage.
+ *
+ * NOTE: Currently session history is in transition, meaning that we're still
+ * using the legacy nsSHistory class internally. The API exposed from this class
+ * should be only the API which we expect to expose when this transition is
+ * complete, and special cases will need to call through the LegacySHistory()
+ * getters.
+ */
+
+#ifndef mozilla_dom_ChildSHistory_h
+#define mozilla_dom_ChildSHistory_h
+
+#include "nsCOMPtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsWrapperCache.h"
+#include "nsThreadUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/LinkedList.h"
+#include "nsID.h"
+
+class nsISHEntry;
+class nsISHistory;
+
+namespace mozilla::dom {
+
+class BrowsingContext;
+
+class ChildSHistory : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ChildSHistory)
+ nsISupports* GetParentObject() const;
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ explicit ChildSHistory(BrowsingContext* aBrowsingContext);
+
+ void SetBrowsingContext(BrowsingContext* aBrowsingContext);
+
+ // Create or destroy the session history implementation in the child process.
+ // This can be removed once session history is stored exclusively in the
+ // parent process.
+ void SetIsInProcess(bool aIsInProcess);
+ bool IsInProcess() { return !!mHistory; }
+
+ int32_t Count();
+ int32_t Index();
+
+ /**
+ * Reload the current entry in the session history.
+ */
+ void Reload(uint32_t aReloadFlags, ErrorResult& aRv);
+
+ /**
+ * The CanGo and Go methods are called with an offset from the current index.
+ * Positive numbers go forward in history, while negative numbers go
+ * backwards.
+ */
+ bool CanGo(int32_t aOffset);
+ void Go(int32_t aOffset, bool aRequireUserInteraction, bool aUserActivation,
+ ErrorResult& aRv);
+ void AsyncGo(int32_t aOffset, bool aRequireUserInteraction,
+ bool aUserActivation, CallerType aCallerType, ErrorResult& aRv);
+
+ // aIndex is the new index, and aOffset is the offset between new and current.
+ void GotoIndex(int32_t aIndex, int32_t aOffset, bool aRequireUserInteraction,
+ bool aUserActivation, ErrorResult& aRv);
+
+ void RemovePendingHistoryNavigations();
+
+ /**
+ * Evicts all content viewers within the current process.
+ */
+ void EvictLocalDocumentViewers();
+
+ // GetLegacySHistory and LegacySHistory have been deprecated. Don't
+ // use these, but instead handle the interaction with nsISHistory in
+ // the parent process.
+ nsISHistory* GetLegacySHistory(ErrorResult& aError);
+ nsISHistory* LegacySHistory();
+
+ void SetIndexAndLength(uint32_t aIndex, uint32_t aLength,
+ const nsID& aChangeId);
+ nsID AddPendingHistoryChange();
+ nsID AddPendingHistoryChange(int32_t aIndexDelta, int32_t aLengthDelta);
+
+ private:
+ virtual ~ChildSHistory();
+
+ class PendingAsyncHistoryNavigation
+ : public Runnable,
+ public mozilla::LinkedListElement<PendingAsyncHistoryNavigation> {
+ public:
+ PendingAsyncHistoryNavigation(ChildSHistory* aHistory, int32_t aOffset,
+ bool aRequireUserInteraction,
+ bool aUserActivation)
+ : Runnable("PendingAsyncHistoryNavigation"),
+ mHistory(aHistory),
+ mRequireUserInteraction(aRequireUserInteraction),
+ mUserActivation(aUserActivation),
+ mOffset(aOffset) {}
+
+ NS_IMETHOD Run() override {
+ if (isInList()) {
+ remove();
+ mHistory->Go(mOffset, mRequireUserInteraction, mUserActivation,
+ IgnoreErrors());
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<ChildSHistory> mHistory;
+ bool mRequireUserInteraction;
+ bool mUserActivation;
+ int32_t mOffset;
+ };
+
+ RefPtr<BrowsingContext> mBrowsingContext;
+ nsCOMPtr<nsISHistory> mHistory;
+ // Can be removed once history-in-parent is the only way
+ mozilla::LinkedList<PendingAsyncHistoryNavigation> mPendingNavigations;
+ int32_t mIndex = -1;
+ int32_t mLength = 0;
+
+ struct PendingSHistoryChange {
+ nsID mChangeID;
+ int32_t mIndexDelta;
+ int32_t mLengthDelta;
+ };
+ AutoTArray<PendingSHistoryChange, 2> mPendingSHistoryChanges;
+
+ // Needs to start 1 above default epoch in parent
+ uint64_t mHistoryEpoch = 1;
+ bool mPendingEpoch = false;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_ChildSHistory_h */
diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp
new file mode 100644
index 0000000000..692ab4fe44
--- /dev/null
+++ b/docshell/shistory/SessionHistoryEntry.cpp
@@ -0,0 +1,1831 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SessionHistoryEntry.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "nsDocShell.h"
+#include "nsDocShellLoadState.h"
+#include "nsFrameLoader.h"
+#include "nsIFormPOSTActionChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIUploadChannel2.h"
+#include "nsIXULRuntime.h"
+#include "nsSHEntryShared.h"
+#include "nsSHistory.h"
+#include "nsStreamUtils.h"
+#include "nsStructuredCloneContainer.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/PresState.h"
+#include "mozilla/StaticPrefs_fission.h"
+
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/CSPMessageUtils.h"
+#include "mozilla/dom/DocumentBinding.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/dom/nsCSPContext.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/dom/ReferrerInfoUtils.h"
+#include "mozilla/ipc/IPDLParamTraits.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/ipc/URIUtils.h"
+
+extern mozilla::LazyLogModule gSHLog;
+
+namespace mozilla {
+namespace dom {
+
+SessionHistoryInfo::SessionHistoryInfo(nsDocShellLoadState* aLoadState,
+ nsIChannel* aChannel)
+ : mURI(aLoadState->URI()),
+ mOriginalURI(aLoadState->OriginalURI()),
+ mResultPrincipalURI(aLoadState->ResultPrincipalURI()),
+ mUnstrippedURI(aLoadState->GetUnstrippedURI()),
+ mLoadType(aLoadState->LoadType()),
+ mSrcdocData(aLoadState->SrcdocData().IsVoid()
+ ? Nothing()
+ : Some(aLoadState->SrcdocData())),
+ mBaseURI(aLoadState->BaseURI()),
+ mLoadReplace(aLoadState->LoadReplace()),
+ mHasUserInteraction(false),
+ mHasUserActivation(aLoadState->HasValidUserGestureActivation()),
+ mSharedState(SharedState::Create(
+ aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(),
+ aLoadState->PartitionedPrincipalToInherit(), aLoadState->Csp(),
+ /* FIXME Is this correct? */
+ aLoadState->TypeHint())) {
+ // Pull the upload stream off of the channel instead of the load state, as
+ // ownership has already been transferred from the load state to the channel.
+ if (nsCOMPtr<nsIUploadChannel2> postChannel = do_QueryInterface(aChannel)) {
+ int64_t contentLength;
+ MOZ_ALWAYS_SUCCEEDS(postChannel->CloneUploadStream(
+ &contentLength, getter_AddRefs(mPostData)));
+ MOZ_ASSERT_IF(mPostData, NS_InputStreamIsCloneable(mPostData));
+ }
+
+ if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) {
+ mReferrerInfo = httpChannel->GetReferrerInfo();
+ }
+
+ MaybeUpdateTitleFromURI();
+}
+
+SessionHistoryInfo::SessionHistoryInfo(
+ const SessionHistoryInfo& aSharedStateFrom, nsIURI* aURI)
+ : mURI(aURI), mSharedState(aSharedStateFrom.mSharedState) {
+ MaybeUpdateTitleFromURI();
+}
+
+SessionHistoryInfo::SessionHistoryInfo(
+ nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, const nsACString& aContentType)
+ : mURI(aURI),
+ mSharedState(SharedState::Create(
+ aTriggeringPrincipal, aPrincipalToInherit,
+ aPartitionedPrincipalToInherit, aCsp, aContentType)) {
+ MaybeUpdateTitleFromURI();
+}
+
+SessionHistoryInfo::SessionHistoryInfo(
+ nsIChannel* aChannel, uint32_t aLoadType,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp) {
+ aChannel->GetURI(getter_AddRefs(mURI));
+ mLoadType = aLoadType;
+
+ nsCOMPtr<nsILoadInfo> loadInfo;
+ aChannel->GetLoadInfo(getter_AddRefs(loadInfo));
+
+ loadInfo->GetResultPrincipalURI(getter_AddRefs(mResultPrincipalURI));
+ loadInfo->GetUnstrippedURI(getter_AddRefs(mUnstrippedURI));
+ loadInfo->GetTriggeringPrincipal(
+ getter_AddRefs(mSharedState.Get()->mTriggeringPrincipal));
+ loadInfo->GetPrincipalToInherit(
+ getter_AddRefs(mSharedState.Get()->mPrincipalToInherit));
+
+ mSharedState.Get()->mPartitionedPrincipalToInherit =
+ aPartitionedPrincipalToInherit;
+ mSharedState.Get()->mCsp = aCsp;
+ aChannel->GetContentType(mSharedState.Get()->mContentType);
+ aChannel->GetOriginalURI(getter_AddRefs(mOriginalURI));
+
+ uint32_t loadFlags;
+ aChannel->GetLoadFlags(&loadFlags);
+ mLoadReplace = !!(loadFlags & nsIChannel::LOAD_REPLACE);
+
+ MaybeUpdateTitleFromURI();
+
+ if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) {
+ mReferrerInfo = httpChannel->GetReferrerInfo();
+ }
+}
+
+void SessionHistoryInfo::Reset(nsIURI* aURI, const nsID& aDocShellID,
+ bool aDynamicCreation,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ const nsACString& aContentType) {
+ mURI = aURI;
+ mOriginalURI = nullptr;
+ mResultPrincipalURI = nullptr;
+ mUnstrippedURI = nullptr;
+ mReferrerInfo = nullptr;
+ // Default title is the URL.
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(mURI->GetSpec(spec))) {
+ CopyUTF8toUTF16(spec, mTitle);
+ }
+ mPostData = nullptr;
+ mLoadType = 0;
+ mScrollPositionX = 0;
+ mScrollPositionY = 0;
+ mStateData = nullptr;
+ mSrcdocData = Nothing();
+ mBaseURI = nullptr;
+ mLoadReplace = false;
+ mURIWasModified = false;
+ mScrollRestorationIsManual = false;
+ mPersist = false;
+ mHasUserInteraction = false;
+ mHasUserActivation = false;
+
+ mSharedState.Get()->mTriggeringPrincipal = aTriggeringPrincipal;
+ mSharedState.Get()->mPrincipalToInherit = aPrincipalToInherit;
+ mSharedState.Get()->mPartitionedPrincipalToInherit =
+ aPartitionedPrincipalToInherit;
+ mSharedState.Get()->mCsp = aCsp;
+ mSharedState.Get()->mContentType = aContentType;
+ mSharedState.Get()->mLayoutHistoryState = nullptr;
+}
+
+void SessionHistoryInfo::MaybeUpdateTitleFromURI() {
+ if (mTitle.IsEmpty() && mURI) {
+ // Default title is the URL.
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(mURI->GetSpec(spec))) {
+ AppendUTF8toUTF16(spec, mTitle);
+ }
+ }
+}
+
+already_AddRefed<nsIInputStream> SessionHistoryInfo::GetPostData() const {
+ // Return a clone of our post data stream. Our caller will either be
+ // transferring this stream to a different SessionHistoryInfo, or passing it
+ // off to necko/another process which will consume it, and we want to preserve
+ // our local instance.
+ nsCOMPtr<nsIInputStream> postData;
+ if (mPostData) {
+ MOZ_ALWAYS_SUCCEEDS(
+ NS_CloneInputStream(mPostData, getter_AddRefs(postData)));
+ }
+ return postData.forget();
+}
+
+void SessionHistoryInfo::SetPostData(nsIInputStream* aPostData) {
+ MOZ_ASSERT_IF(aPostData, NS_InputStreamIsCloneable(aPostData));
+ mPostData = aPostData;
+}
+
+uint64_t SessionHistoryInfo::SharedId() const {
+ return mSharedState.Get()->mId;
+}
+
+nsILayoutHistoryState* SessionHistoryInfo::GetLayoutHistoryState() {
+ return mSharedState.Get()->mLayoutHistoryState;
+}
+
+void SessionHistoryInfo::SetLayoutHistoryState(nsILayoutHistoryState* aState) {
+ mSharedState.Get()->mLayoutHistoryState = aState;
+ if (mSharedState.Get()->mLayoutHistoryState) {
+ mSharedState.Get()->mLayoutHistoryState->SetScrollPositionOnly(
+ !mSharedState.Get()->mSaveLayoutState);
+ }
+}
+
+nsIPrincipal* SessionHistoryInfo::GetTriggeringPrincipal() const {
+ return mSharedState.Get()->mTriggeringPrincipal;
+}
+
+nsIPrincipal* SessionHistoryInfo::GetPrincipalToInherit() const {
+ return mSharedState.Get()->mPrincipalToInherit;
+}
+
+nsIPrincipal* SessionHistoryInfo::GetPartitionedPrincipalToInherit() const {
+ return mSharedState.Get()->mPartitionedPrincipalToInherit;
+}
+
+nsIContentSecurityPolicy* SessionHistoryInfo::GetCsp() const {
+ return mSharedState.Get()->mCsp;
+}
+
+uint32_t SessionHistoryInfo::GetCacheKey() const {
+ return mSharedState.Get()->mCacheKey;
+}
+
+void SessionHistoryInfo::SetCacheKey(uint32_t aCacheKey) {
+ mSharedState.Get()->mCacheKey = aCacheKey;
+}
+
+bool SessionHistoryInfo::IsSubFrame() const {
+ return mSharedState.Get()->mIsFrameNavigation;
+}
+
+void SessionHistoryInfo::SetSaveLayoutStateFlag(bool aSaveLayoutStateFlag) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ static_cast<SHEntrySharedParentState*>(mSharedState.Get())->mSaveLayoutState =
+ aSaveLayoutStateFlag;
+}
+
+void SessionHistoryInfo::FillLoadInfo(nsDocShellLoadState& aLoadState) const {
+ aLoadState.SetOriginalURI(mOriginalURI);
+ aLoadState.SetMaybeResultPrincipalURI(Some(mResultPrincipalURI));
+ aLoadState.SetUnstrippedURI(mUnstrippedURI);
+ aLoadState.SetLoadReplace(mLoadReplace);
+ nsCOMPtr<nsIInputStream> postData = GetPostData();
+ aLoadState.SetPostDataStream(postData);
+ aLoadState.SetReferrerInfo(mReferrerInfo);
+
+ aLoadState.SetTypeHint(mSharedState.Get()->mContentType);
+ aLoadState.SetTriggeringPrincipal(mSharedState.Get()->mTriggeringPrincipal);
+ aLoadState.SetPrincipalToInherit(mSharedState.Get()->mPrincipalToInherit);
+ aLoadState.SetPartitionedPrincipalToInherit(
+ mSharedState.Get()->mPartitionedPrincipalToInherit);
+ aLoadState.SetCsp(mSharedState.Get()->mCsp);
+
+ // Do not inherit principal from document (security-critical!);
+ uint32_t flags = nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_NONE;
+
+ // Passing nullptr as aSourceDocShell gives the same behaviour as before
+ // aSourceDocShell was introduced. According to spec we should be passing
+ // the source browsing context that was used when the history entry was
+ // first created. bug 947716 has been created to address this issue.
+ nsAutoString srcdoc;
+ nsCOMPtr<nsIURI> baseURI;
+ if (mSrcdocData) {
+ srcdoc = mSrcdocData.value();
+ baseURI = mBaseURI;
+ flags |= nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_IS_SRCDOC;
+ } else {
+ srcdoc = VoidString();
+ }
+ aLoadState.SetSrcdocData(srcdoc);
+ aLoadState.SetBaseURI(baseURI);
+ aLoadState.SetInternalLoadFlags(flags);
+
+ aLoadState.SetFirstParty(true);
+
+ // When we create a load state from the history info we already know if
+ // https-first was able to upgrade the request from http to https. There is no
+ // point in re-retrying to upgrade. On a reload we still want to check,
+ // because the exemptions set by the user could have changed.
+ if ((mLoadType & nsIDocShell::LOAD_CMD_RELOAD) == 0) {
+ aLoadState.SetIsExemptFromHTTPSFirstMode(true);
+ }
+}
+/* static */
+SessionHistoryInfo::SharedState SessionHistoryInfo::SharedState::Create(
+ nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, const nsACString& aContentType) {
+ if (XRE_IsParentProcess()) {
+ return SharedState(new SHEntrySharedParentState(
+ aTriggeringPrincipal, aPrincipalToInherit,
+ aPartitionedPrincipalToInherit, aCsp, aContentType));
+ }
+
+ return SharedState(MakeUnique<SHEntrySharedState>(
+ aTriggeringPrincipal, aPrincipalToInherit, aPartitionedPrincipalToInherit,
+ aCsp, aContentType));
+}
+
+SessionHistoryInfo::SharedState::SharedState() { Init(); }
+
+SessionHistoryInfo::SharedState::SharedState(
+ const SessionHistoryInfo::SharedState& aOther) {
+ Init(aOther);
+}
+
+SessionHistoryInfo::SharedState::SharedState(
+ const Maybe<const SessionHistoryInfo::SharedState&>& aOther) {
+ if (aOther.isSome()) {
+ Init(aOther.ref());
+ } else {
+ Init();
+ }
+}
+
+SessionHistoryInfo::SharedState::~SharedState() {
+ if (XRE_IsParentProcess()) {
+ mParent
+ .RefPtr<SHEntrySharedParentState>::~RefPtr<SHEntrySharedParentState>();
+ } else {
+ mChild.UniquePtr<SHEntrySharedState>::~UniquePtr<SHEntrySharedState>();
+ }
+}
+
+SessionHistoryInfo::SharedState& SessionHistoryInfo::SharedState::operator=(
+ const SessionHistoryInfo::SharedState& aOther) {
+ if (this != &aOther) {
+ if (XRE_IsParentProcess()) {
+ mParent = aOther.mParent;
+ } else {
+ mChild = MakeUnique<SHEntrySharedState>(*aOther.mChild);
+ }
+ }
+ return *this;
+}
+
+SHEntrySharedState* SessionHistoryInfo::SharedState::Get() const {
+ if (XRE_IsParentProcess()) {
+ return mParent;
+ }
+
+ return mChild.get();
+}
+
+void SessionHistoryInfo::SharedState::ChangeId(uint64_t aId) {
+ if (XRE_IsParentProcess()) {
+ mParent->ChangeId(aId);
+ } else {
+ mChild->mId = aId;
+ }
+}
+
+void SessionHistoryInfo::SharedState::Init() {
+ if (XRE_IsParentProcess()) {
+ new (&mParent)
+ RefPtr<SHEntrySharedParentState>(new SHEntrySharedParentState());
+ } else {
+ new (&mChild)
+ UniquePtr<SHEntrySharedState>(MakeUnique<SHEntrySharedState>());
+ }
+}
+
+void SessionHistoryInfo::SharedState::Init(
+ const SessionHistoryInfo::SharedState& aOther) {
+ if (XRE_IsParentProcess()) {
+ new (&mParent) RefPtr<SHEntrySharedParentState>(aOther.mParent);
+ } else {
+ new (&mChild) UniquePtr<SHEntrySharedState>(
+ MakeUnique<SHEntrySharedState>(*aOther.mChild));
+ }
+}
+
+static uint64_t gLoadingSessionHistoryInfoLoadId = 0;
+
+nsTHashMap<nsUint64HashKey, SessionHistoryEntry::LoadingEntry>*
+ SessionHistoryEntry::sLoadIdToEntry = nullptr;
+
+LoadingSessionHistoryInfo::LoadingSessionHistoryInfo(
+ SessionHistoryEntry* aEntry)
+ : mInfo(aEntry->Info()), mLoadId(++gLoadingSessionHistoryInfoLoadId) {
+ SessionHistoryEntry::SetByLoadId(mLoadId, aEntry);
+}
+
+LoadingSessionHistoryInfo::LoadingSessionHistoryInfo(
+ SessionHistoryEntry* aEntry, const LoadingSessionHistoryInfo* aInfo)
+ : mInfo(aEntry->Info()),
+ mLoadId(aInfo->mLoadId),
+ mLoadIsFromSessionHistory(aInfo->mLoadIsFromSessionHistory),
+ mOffset(aInfo->mOffset),
+ mLoadingCurrentEntry(aInfo->mLoadingCurrentEntry) {
+ MOZ_ASSERT(SessionHistoryEntry::GetByLoadId(mLoadId)->mEntry == aEntry);
+}
+
+LoadingSessionHistoryInfo::LoadingSessionHistoryInfo(
+ const SessionHistoryInfo& aInfo)
+ : mInfo(aInfo), mLoadId(UINT64_MAX) {}
+
+already_AddRefed<nsDocShellLoadState>
+LoadingSessionHistoryInfo::CreateLoadInfo() const {
+ RefPtr<nsDocShellLoadState> loadState(
+ new nsDocShellLoadState(mInfo.GetURI()));
+
+ mInfo.FillLoadInfo(*loadState);
+
+ loadState->SetLoadingSessionHistoryInfo(*this);
+
+ return loadState.forget();
+}
+
+static uint32_t gEntryID;
+
+SessionHistoryEntry::LoadingEntry* SessionHistoryEntry::GetByLoadId(
+ uint64_t aLoadId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!sLoadIdToEntry) {
+ return nullptr;
+ }
+
+ return sLoadIdToEntry->Lookup(aLoadId).DataPtrOrNull();
+}
+
+void SessionHistoryEntry::SetByLoadId(uint64_t aLoadId,
+ SessionHistoryEntry* aEntry) {
+ if (!sLoadIdToEntry) {
+ sLoadIdToEntry = new nsTHashMap<nsUint64HashKey, LoadingEntry>();
+ }
+
+ MOZ_LOG(
+ gSHLog, LogLevel::Verbose,
+ ("SessionHistoryEntry::SetByLoadId(%" PRIu64 " - %p)", aLoadId, aEntry));
+ sLoadIdToEntry->InsertOrUpdate(
+ aLoadId, LoadingEntry{
+ .mEntry = aEntry,
+ .mInfoSnapshotForValidation =
+ MakeUnique<SessionHistoryInfo>(aEntry->Info()),
+ });
+}
+
+void SessionHistoryEntry::RemoveLoadId(uint64_t aLoadId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!sLoadIdToEntry) {
+ return;
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Verbose,
+ ("SHEntry::RemoveLoadId(%" PRIu64 ")", aLoadId));
+ sLoadIdToEntry->Remove(aLoadId);
+}
+
+SessionHistoryEntry::SessionHistoryEntry()
+ : mInfo(new SessionHistoryInfo()), mID(++gEntryID) {
+ MOZ_ASSERT(mozilla::SessionHistoryInParent());
+}
+
+SessionHistoryEntry::SessionHistoryEntry(nsDocShellLoadState* aLoadState,
+ nsIChannel* aChannel)
+ : mInfo(new SessionHistoryInfo(aLoadState, aChannel)), mID(++gEntryID) {
+ MOZ_ASSERT(mozilla::SessionHistoryInParent());
+}
+
+SessionHistoryEntry::SessionHistoryEntry(SessionHistoryInfo* aInfo)
+ : mInfo(MakeUnique<SessionHistoryInfo>(*aInfo)), mID(++gEntryID) {
+ MOZ_ASSERT(mozilla::SessionHistoryInParent());
+}
+
+SessionHistoryEntry::SessionHistoryEntry(const SessionHistoryEntry& aEntry)
+ : mInfo(MakeUnique<SessionHistoryInfo>(*aEntry.mInfo)),
+ mParent(aEntry.mParent),
+ mID(aEntry.mID),
+ mBCHistoryLength(aEntry.mBCHistoryLength) {
+ MOZ_ASSERT(mozilla::SessionHistoryInParent());
+}
+
+SessionHistoryEntry::~SessionHistoryEntry() {
+ // Null out the mParent pointers on all our kids.
+ for (nsISHEntry* entry : mChildren) {
+ if (entry) {
+ entry->SetParent(nullptr);
+ }
+ }
+
+ if (sLoadIdToEntry) {
+ sLoadIdToEntry->RemoveIf(
+ [this](auto& aIter) { return aIter.Data().mEntry == this; });
+ if (sLoadIdToEntry->IsEmpty()) {
+ delete sLoadIdToEntry;
+ sLoadIdToEntry = nullptr;
+ }
+ }
+}
+
+NS_IMPL_ISUPPORTS(SessionHistoryEntry, nsISHEntry, SessionHistoryEntry,
+ nsISupportsWeakReference)
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetURI(nsIURI** aURI) {
+ nsCOMPtr<nsIURI> uri = mInfo->mURI;
+ uri.forget(aURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetURI(nsIURI* aURI) {
+ mInfo->mURI = aURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetOriginalURI(nsIURI** aOriginalURI) {
+ nsCOMPtr<nsIURI> originalURI = mInfo->mOriginalURI;
+ originalURI.forget(aOriginalURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetOriginalURI(nsIURI* aOriginalURI) {
+ mInfo->mOriginalURI = aOriginalURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetResultPrincipalURI(nsIURI** aResultPrincipalURI) {
+ nsCOMPtr<nsIURI> resultPrincipalURI = mInfo->mResultPrincipalURI;
+ resultPrincipalURI.forget(aResultPrincipalURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetResultPrincipalURI(nsIURI* aResultPrincipalURI) {
+ mInfo->mResultPrincipalURI = aResultPrincipalURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetUnstrippedURI(nsIURI** aUnstrippedURI) {
+ nsCOMPtr<nsIURI> unstrippedURI = mInfo->mUnstrippedURI;
+ unstrippedURI.forget(aUnstrippedURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetUnstrippedURI(nsIURI* aUnstrippedURI) {
+ mInfo->mUnstrippedURI = aUnstrippedURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetLoadReplace(bool* aLoadReplace) {
+ *aLoadReplace = mInfo->mLoadReplace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetLoadReplace(bool aLoadReplace) {
+ mInfo->mLoadReplace = aLoadReplace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetTitle(nsAString& aTitle) {
+ aTitle = mInfo->mTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetTitle(const nsAString& aTitle) {
+ mInfo->SetTitle(aTitle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetName(nsAString& aName) {
+ aName = mInfo->mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetName(const nsAString& aName) {
+ mInfo->mName = aName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetIsSubFrame(bool* aIsSubFrame) {
+ *aIsSubFrame = SharedInfo()->mIsFrameNavigation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetIsSubFrame(bool aIsSubFrame) {
+ SharedInfo()->mIsFrameNavigation = aIsSubFrame;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetHasUserInteraction(bool* aFlag) {
+ // The back button and menulist deal with root/top-level
+ // session history entries, thus we annotate only the root entry.
+ if (!mParent) {
+ *aFlag = mInfo->mHasUserInteraction;
+ } else {
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this);
+ root->GetHasUserInteraction(aFlag);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetHasUserInteraction(bool aFlag) {
+ // The back button and menulist deal with root/top-level
+ // session history entries, thus we annotate only the root entry.
+ if (!mParent) {
+ mInfo->mHasUserInteraction = aFlag;
+ } else {
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this);
+ root->SetHasUserInteraction(aFlag);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetHasUserActivation(bool* aFlag) {
+ *aFlag = mInfo->mHasUserActivation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetHasUserActivation(bool aFlag) {
+ mInfo->mHasUserActivation = aFlag;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) {
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = mInfo->mReferrerInfo;
+ referrerInfo.forget(aReferrerInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
+ mInfo->mReferrerInfo = aReferrerInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetDocumentViewer(nsIDocumentViewer** aDocumentViewer) {
+ *aDocumentViewer = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetDocumentViewer(nsIDocumentViewer* aDocumentViewer) {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetIsInBFCache(bool* aResult) {
+ *aResult = !!SharedInfo()->mFrameLoader;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetSticky(bool* aSticky) {
+ *aSticky = SharedInfo()->mSticky;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetSticky(bool aSticky) {
+ SharedInfo()->mSticky = aSticky;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetWindowState(nsISupports** aWindowState) {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetWindowState(nsISupports* aWindowState) {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetRefreshURIList(nsIMutableArray** aRefreshURIList) {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetRefreshURIList(nsIMutableArray* aRefreshURIList) {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetPostData(nsIInputStream** aPostData) {
+ *aPostData = mInfo->GetPostData().take();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetPostData(nsIInputStream* aPostData) {
+ mInfo->SetPostData(aPostData);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetHasPostData(bool* aResult) {
+ *aResult = mInfo->HasPostData();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetLayoutHistoryState(
+ nsILayoutHistoryState** aLayoutHistoryState) {
+ nsCOMPtr<nsILayoutHistoryState> layoutHistoryState =
+ SharedInfo()->mLayoutHistoryState;
+ layoutHistoryState.forget(aLayoutHistoryState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetLayoutHistoryState(
+ nsILayoutHistoryState* aLayoutHistoryState) {
+ SharedInfo()->mLayoutHistoryState = aLayoutHistoryState;
+ if (SharedInfo()->mLayoutHistoryState) {
+ SharedInfo()->mLayoutHistoryState->SetScrollPositionOnly(
+ !SharedInfo()->mSaveLayoutState);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetParent(nsISHEntry** aParent) {
+ nsCOMPtr<nsISHEntry> parent = do_QueryReferent(mParent);
+ parent.forget(aParent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetParent(nsISHEntry* aParent) {
+ mParent = do_GetWeakReference(aParent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetLoadType(uint32_t* aLoadType) {
+ *aLoadType = mInfo->mLoadType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetLoadType(uint32_t aLoadType) {
+ mInfo->mLoadType = aLoadType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetID(uint32_t* aID) {
+ *aID = mID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetID(uint32_t aID) {
+ mID = aID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetCacheKey(uint32_t* aCacheKey) {
+ *aCacheKey = SharedInfo()->mCacheKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetCacheKey(uint32_t aCacheKey) {
+ SharedInfo()->mCacheKey = aCacheKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetSaveLayoutStateFlag(bool* aSaveLayoutStateFlag) {
+ *aSaveLayoutStateFlag = SharedInfo()->mSaveLayoutState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetSaveLayoutStateFlag(bool aSaveLayoutStateFlag) {
+ SharedInfo()->mSaveLayoutState = aSaveLayoutStateFlag;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetContentType(nsACString& aContentType) {
+ aContentType = SharedInfo()->mContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetContentType(const nsACString& aContentType) {
+ SharedInfo()->mContentType = aContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetURIWasModified(bool* aURIWasModified) {
+ *aURIWasModified = mInfo->mURIWasModified;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetURIWasModified(bool aURIWasModified) {
+ mInfo->mURIWasModified = aURIWasModified;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetTriggeringPrincipal(
+ nsIPrincipal** aTriggeringPrincipal) {
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal =
+ SharedInfo()->mTriggeringPrincipal;
+ triggeringPrincipal.forget(aTriggeringPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetTriggeringPrincipal(
+ nsIPrincipal* aTriggeringPrincipal) {
+ SharedInfo()->mTriggeringPrincipal = aTriggeringPrincipal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetPrincipalToInherit(nsIPrincipal** aPrincipalToInherit) {
+ nsCOMPtr<nsIPrincipal> principalToInherit = SharedInfo()->mPrincipalToInherit;
+ principalToInherit.forget(aPrincipalToInherit);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetPrincipalToInherit(nsIPrincipal* aPrincipalToInherit) {
+ SharedInfo()->mPrincipalToInherit = aPrincipalToInherit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetPartitionedPrincipalToInherit(
+ nsIPrincipal** aPartitionedPrincipalToInherit) {
+ nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit =
+ SharedInfo()->mPartitionedPrincipalToInherit;
+ partitionedPrincipalToInherit.forget(aPartitionedPrincipalToInherit);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetPartitionedPrincipalToInherit(
+ nsIPrincipal* aPartitionedPrincipalToInherit) {
+ SharedInfo()->mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetCsp(nsIContentSecurityPolicy** aCsp) {
+ nsCOMPtr<nsIContentSecurityPolicy> csp = SharedInfo()->mCsp;
+ csp.forget(aCsp);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetCsp(nsIContentSecurityPolicy* aCsp) {
+ SharedInfo()->mCsp = aCsp;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetStateData(nsIStructuredCloneContainer** aStateData) {
+ RefPtr<nsStructuredCloneContainer> stateData = mInfo->mStateData;
+ stateData.forget(aStateData);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetStateData(nsIStructuredCloneContainer* aStateData) {
+ mInfo->mStateData = static_cast<nsStructuredCloneContainer*>(aStateData);
+ return NS_OK;
+}
+
+const nsID& SessionHistoryEntry::DocshellID() const {
+ return SharedInfo()->mDocShellID;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetDocshellID(nsID& aDocshellID) {
+ aDocshellID = DocshellID();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetDocshellID(const nsID& aDocshellID) {
+ SharedInfo()->mDocShellID = aDocshellID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetIsSrcdocEntry(bool* aIsSrcdocEntry) {
+ *aIsSrcdocEntry = mInfo->mSrcdocData.isSome();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetSrcdocData(nsAString& aSrcdocData) {
+ aSrcdocData = mInfo->mSrcdocData.valueOr(EmptyString());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetSrcdocData(const nsAString& aSrcdocData) {
+ mInfo->mSrcdocData = Some(nsString(aSrcdocData));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetBaseURI(nsIURI** aBaseURI) {
+ nsCOMPtr<nsIURI> baseURI = mInfo->mBaseURI;
+ baseURI.forget(aBaseURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetBaseURI(nsIURI* aBaseURI) {
+ mInfo->mBaseURI = aBaseURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetScrollRestorationIsManual(
+ bool* aScrollRestorationIsManual) {
+ *aScrollRestorationIsManual = mInfo->mScrollRestorationIsManual;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetScrollRestorationIsManual(
+ bool aScrollRestorationIsManual) {
+ mInfo->mScrollRestorationIsManual = aScrollRestorationIsManual;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetLoadedInThisProcess(bool* aLoadedInThisProcess) {
+ // FIXME
+ //*aLoadedInThisProcess = mInfo->mLoadedInThisProcess;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetShistory(nsISHistory** aShistory) {
+ nsCOMPtr<nsISHistory> sHistory = do_QueryReferent(SharedInfo()->mSHistory);
+ sHistory.forget(aShistory);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetShistory(nsISHistory* aShistory) {
+ nsWeakPtr shistory = do_GetWeakReference(aShistory);
+ // mSHistory can not be changed once it's set
+ MOZ_ASSERT(!SharedInfo()->mSHistory || (SharedInfo()->mSHistory == shistory));
+ SharedInfo()->mSHistory = shistory;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetLastTouched(uint32_t* aLastTouched) {
+ *aLastTouched = SharedInfo()->mLastTouched;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetLastTouched(uint32_t aLastTouched) {
+ SharedInfo()->mLastTouched = aLastTouched;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetChildCount(int32_t* aChildCount) {
+ *aChildCount = mChildren.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetPersist(bool* aPersist) {
+ *aPersist = mInfo->mPersist;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetPersist(bool aPersist) {
+ mInfo->mPersist = aPersist;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetScrollPosition(int32_t* aX, int32_t* aY) {
+ *aX = mInfo->mScrollPositionX;
+ *aY = mInfo->mScrollPositionY;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetScrollPosition(int32_t aX, int32_t aY) {
+ mInfo->mScrollPositionX = aX;
+ mInfo->mScrollPositionY = aY;
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::GetViewerBounds(nsIntRect& bounds) {
+ bounds = SharedInfo()->mViewerBounds;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::SetViewerBounds(const nsIntRect& bounds) {
+ SharedInfo()->mViewerBounds = bounds;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::AddChildShell(nsIDocShellTreeItem* shell) {
+ MOZ_CRASH("This lives in the child process");
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::ChildShellAt(int32_t index,
+ nsIDocShellTreeItem** _retval) {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::ClearChildShells() {
+ MOZ_CRASH("This lives in the child process");
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::SyncPresentationState() {
+ MOZ_CRASH("This lives in the child process");
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::InitLayoutHistoryState(
+ nsILayoutHistoryState** aLayoutHistoryState) {
+ if (!SharedInfo()->mLayoutHistoryState) {
+ nsCOMPtr<nsILayoutHistoryState> historyState;
+ historyState = NS_NewLayoutHistoryState();
+ SetLayoutHistoryState(historyState);
+ }
+
+ return GetLayoutHistoryState(aLayoutHistoryState);
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::Create(
+ nsIURI* aURI, const nsAString& aTitle, nsIInputStream* aInputStream,
+ uint32_t aCacheKey, const nsACString& aContentType,
+ nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, const nsID& aDocshellID,
+ bool aDynamicCreation, nsIURI* aOriginalURI, nsIURI* aResultPrincipalURI,
+ nsIURI* aUnstrippedURI, bool aLoadReplace, nsIReferrerInfo* aReferrerInfo,
+ const nsAString& aSrcdoc, bool aSrcdocEntry, nsIURI* aBaseURI,
+ bool aSaveLayoutState, bool aExpired, bool aUserActivation) {
+ MOZ_CRASH("Might need to implement this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::Clone(nsISHEntry** aEntry) {
+ RefPtr<SessionHistoryEntry> entry = new SessionHistoryEntry(*this);
+
+ // These are not copied for some reason, we're not sure why.
+ entry->mInfo->mLoadType = 0;
+ entry->mInfo->mScrollPositionX = 0;
+ entry->mInfo->mScrollPositionY = 0;
+ entry->mInfo->mScrollRestorationIsManual = false;
+
+ entry->mInfo->mHasUserInteraction = false;
+
+ entry.forget(aEntry);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(nsDocShellEditorData*)
+SessionHistoryEntry::ForgetEditorData() {
+ MOZ_CRASH("This lives in the child process");
+ return nullptr;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::SetEditorData(nsDocShellEditorData* aData) {
+ NS_WARNING("This lives in the child process");
+}
+
+NS_IMETHODIMP_(bool)
+SessionHistoryEntry::HasDetachedEditor() {
+ NS_WARNING("This lives in the child process");
+ return false;
+}
+
+NS_IMETHODIMP_(bool)
+SessionHistoryEntry::IsDynamicallyAdded() {
+ return SharedInfo()->mDynamicallyCreated;
+}
+
+void SessionHistoryEntry::SetWireframe(const Maybe<Wireframe>& aWireframe) {
+ mWireframe = aWireframe;
+}
+
+void SessionHistoryEntry::SetIsDynamicallyAdded(bool aDynamic) {
+ MOZ_ASSERT_IF(SharedInfo()->mDynamicallyCreated, aDynamic);
+ SharedInfo()->mDynamicallyCreated = aDynamic;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::HasDynamicallyAddedChild(bool* aHasDynamicallyAddedChild) {
+ for (const auto& child : mChildren) {
+ if (child && child->IsDynamicallyAdded()) {
+ *aHasDynamicallyAddedChild = true;
+ return NS_OK;
+ }
+ }
+ *aHasDynamicallyAddedChild = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(bool)
+SessionHistoryEntry::HasBFCacheEntry(SHEntrySharedParentState* aEntry) {
+ return SharedInfo() == aEntry;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::AdoptBFCacheEntry(nsISHEntry* aEntry) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry);
+ NS_ENSURE_STATE(she && she->mInfo->mSharedState.Get());
+
+ mInfo->mSharedState =
+ static_cast<SessionHistoryEntry*>(aEntry)->mInfo->mSharedState;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::AbandonBFCacheEntry() {
+ MOZ_CRASH("This lives in the child process");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SharesDocumentWith(nsISHEntry* aEntry,
+ bool* aSharesDocumentWith) {
+ SessionHistoryEntry* entry = static_cast<SessionHistoryEntry*>(aEntry);
+
+ MOZ_ASSERT_IF(entry->SharedInfo() != SharedInfo(),
+ entry->SharedInfo()->GetId() != SharedInfo()->GetId());
+
+ *aSharesDocumentWith = entry->SharedInfo() == SharedInfo();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetLoadTypeAsHistory() {
+ mInfo->mLoadType = LOAD_HISTORY;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::AddChild(nsISHEntry* aChild, int32_t aOffset,
+ bool aUseRemoteSubframes) {
+ nsCOMPtr<SessionHistoryEntry> child = do_QueryInterface(aChild);
+ MOZ_ASSERT_IF(aChild, child);
+ AddChild(child, aOffset, aUseRemoteSubframes);
+
+ return NS_OK;
+}
+
+void SessionHistoryEntry::AddChild(SessionHistoryEntry* aChild, int32_t aOffset,
+ bool aUseRemoteSubframes) {
+ if (aChild) {
+ aChild->SetParent(this);
+ }
+
+ if (aOffset < 0) {
+ mChildren.AppendElement(aChild);
+ return;
+ }
+
+ //
+ // Bug 52670: Ensure children are added in order.
+ //
+ // Later frames in the child list may load faster and get appended
+ // before earlier frames, causing session history to be scrambled.
+ // By growing the list here, they are added to the right position.
+
+ int32_t length = mChildren.Length();
+
+ // Assert that aOffset will not be so high as to grow us a lot.
+ NS_ASSERTION(aOffset < length + 1023, "Large frames array!\n");
+
+ // If the new child is dynamically added, try to add it to aOffset, but if
+ // there are non-dynamically added children, the child must be after those.
+ if (aChild && aChild->IsDynamicallyAdded()) {
+ int32_t lastNonDyn = aOffset - 1;
+ for (int32_t i = aOffset; i < length; ++i) {
+ SessionHistoryEntry* entry = mChildren[i];
+ if (entry) {
+ if (entry->IsDynamicallyAdded()) {
+ break;
+ }
+
+ lastNonDyn = i;
+ }
+ }
+
+ // If aOffset is larger than Length(), we must first truncate the array.
+ if (aOffset > length) {
+ mChildren.SetLength(aOffset);
+ }
+
+ mChildren.InsertElementAt(lastNonDyn + 1, aChild);
+
+ return;
+ }
+
+ // If the new child isn't dynamically added, it should be set to aOffset.
+ // If there are dynamically added children before that, those must be moved
+ // to be after aOffset.
+ if (length > 0) {
+ int32_t start = std::min(length - 1, aOffset);
+ int32_t dynEntryIndex = -1;
+ DebugOnly<SessionHistoryEntry*> dynEntry = nullptr;
+ for (int32_t i = start; i >= 0; --i) {
+ SessionHistoryEntry* entry = mChildren[i];
+ if (entry) {
+ if (!entry->IsDynamicallyAdded()) {
+ break;
+ }
+
+ dynEntryIndex = i;
+ dynEntry = entry;
+ }
+ }
+
+ if (dynEntryIndex >= 0) {
+ mChildren.InsertElementsAt(dynEntryIndex, aOffset - dynEntryIndex + 1);
+ NS_ASSERTION(mChildren[aOffset + 1] == dynEntry, "Whaat?");
+ }
+ }
+
+ // Make sure there isn't anything at aOffset.
+ if ((uint32_t)aOffset < mChildren.Length()) {
+ SessionHistoryEntry* oldChild = mChildren[aOffset];
+ if (oldChild && oldChild != aChild) {
+ // Under Fission, this can happen when a network-created iframe starts
+ // out in-process, moves out-of-process, and then switches back. At that
+ // point, we'll create a new network-created DocShell at the same index
+ // where we already have an entry for the original network-created
+ // DocShell.
+ //
+ // This should ideally stop being an issue once the Fission-aware
+ // session history rewrite is complete.
+ NS_ASSERTION(
+ aUseRemoteSubframes || NS_IsAboutBlank(oldChild->Info().GetURI()),
+ "Adding a child where we already have a child? This may misbehave");
+ oldChild->SetParent(nullptr);
+ }
+ } else {
+ mChildren.SetLength(aOffset + 1);
+ }
+
+ mChildren.ReplaceElementAt(aOffset, aChild);
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::RemoveChild(nsISHEntry* aChild) {
+ NS_ENSURE_TRUE(aChild, NS_ERROR_FAILURE);
+
+ nsCOMPtr<SessionHistoryEntry> child = do_QueryInterface(aChild);
+ MOZ_ASSERT(child);
+ RemoveChild(child);
+
+ return NS_OK;
+}
+
+void SessionHistoryEntry::RemoveChild(SessionHistoryEntry* aChild) {
+ bool childRemoved = false;
+ if (aChild->IsDynamicallyAdded()) {
+ childRemoved = mChildren.RemoveElement(aChild);
+ } else {
+ int32_t index = mChildren.IndexOf(aChild);
+ if (index >= 0) {
+ // Other alive non-dynamic child docshells still keep mChildOffset,
+ // so we don't want to change the indices here.
+ mChildren.ReplaceElementAt(index, nullptr);
+ childRemoved = true;
+ }
+ }
+
+ if (childRemoved) {
+ aChild->SetParent(nullptr);
+
+ // reduce the child count, i.e. remove empty children at the end
+ for (int32_t i = mChildren.Length() - 1; i >= 0 && !mChildren[i]; --i) {
+ mChildren.RemoveElementAt(i);
+ }
+ }
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetChildAt(int32_t aIndex, nsISHEntry** aChild) {
+ nsCOMPtr<nsISHEntry> child = mChildren.SafeElementAt(aIndex);
+ child.forget(aChild);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::GetChildSHEntryIfHasNoDynamicallyAddedChild(
+ int32_t aChildOffset, nsISHEntry** aChild) {
+ *aChild = nullptr;
+
+ bool dynamicallyAddedChild = false;
+ HasDynamicallyAddedChild(&dynamicallyAddedChild);
+ if (dynamicallyAddedChild) {
+ return;
+ }
+
+ // If the user did a shift-reload on this frameset page,
+ // we don't want to load the subframes from history.
+ if (IsForceReloadType(mInfo->mLoadType) || mInfo->mLoadType == LOAD_REFRESH) {
+ return;
+ }
+
+ /* Before looking for the subframe's url, check
+ * the expiration status of the parent. If the parent
+ * has expired from cache, then subframes will not be
+ * loaded from history in certain situations.
+ * If the user pressed reload and the parent frame has expired
+ * from cache, we do not want to load the child frame from history.
+ */
+ if (SharedInfo()->mExpired && (mInfo->mLoadType == LOAD_RELOAD_NORMAL)) {
+ // The parent has expired. Return null.
+ *aChild = nullptr;
+ return;
+ }
+ // Get the child subframe from session history.
+ GetChildAt(aChildOffset, aChild);
+ if (*aChild) {
+ // Set the parent's Load Type on the child
+ (*aChild)->SetLoadType(mInfo->mLoadType);
+ }
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::ReplaceChild(nsISHEntry* aNewChild) {
+ NS_ENSURE_STATE(aNewChild);
+
+ nsCOMPtr<SessionHistoryEntry> newChild = do_QueryInterface(aNewChild);
+ MOZ_ASSERT(newChild);
+ return ReplaceChild(newChild) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool SessionHistoryEntry::ReplaceChild(SessionHistoryEntry* aNewChild) {
+ const nsID& docshellID = aNewChild->DocshellID();
+
+ for (uint32_t i = 0; i < mChildren.Length(); ++i) {
+ if (mChildren[i] && docshellID == mChildren[i]->DocshellID()) {
+ mChildren[i]->SetParent(nullptr);
+ mChildren.ReplaceElementAt(i, aNewChild);
+ aNewChild->SetParent(this);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::ClearEntry() {
+ int32_t childCount = GetChildCount();
+ // Remove all children of this entry
+ for (int32_t i = childCount; i > 0; --i) {
+ nsCOMPtr<nsISHEntry> child;
+ GetChildAt(i - 1, getter_AddRefs(child));
+ RemoveChild(child);
+ }
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::CreateLoadInfo(nsDocShellLoadState** aLoadState) {
+ NS_WARNING("We shouldn't be calling this!");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetBfcacheID(uint64_t* aBfcacheID) {
+ *aBfcacheID = SharedInfo()->mId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::GetWireframe(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aOut) {
+ if (mWireframe.isNothing()) {
+ aOut.set(JS::NullValue());
+ } else if (NS_WARN_IF(!mWireframe->ToObjectInternal(aCx, aOut))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetWireframe(JSContext* aCx, JS::Handle<JS::Value> aArg) {
+ if (aArg.isNullOrUndefined()) {
+ mWireframe = Nothing();
+ return NS_OK;
+ }
+
+ Wireframe wireframe;
+ if (aArg.isObject() && wireframe.Init(aCx, aArg)) {
+ mWireframe = Some(std::move(wireframe));
+ return NS_OK;
+ }
+
+ return NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP_(void)
+SessionHistoryEntry::SyncTreesForSubframeNavigation(
+ nsISHEntry* aEntry, mozilla::dom::BrowsingContext* aTopBC,
+ mozilla::dom::BrowsingContext* aIgnoreBC) {
+ // XXX Keep this in sync with nsSHEntry::SyncTreesForSubframeNavigation.
+ //
+ // We need to sync up the browsing context and session history trees for
+ // subframe navigation. If the load was in a subframe, we forward up to
+ // the top browsing context, which will then recursively sync up all browsing
+ // contexts to their corresponding entries in the new session history tree. If
+ // we don't do this, then we can cache a content viewer on the wrong cloned
+ // entry, and subsequently restore it at the wrong time.
+ nsCOMPtr<nsISHEntry> newRootEntry = nsSHistory::GetRootSHEntry(aEntry);
+ if (newRootEntry) {
+ // newRootEntry is now the new root entry.
+ // Find the old root entry as well.
+
+ // Need a strong ref. on |oldRootEntry| so it isn't destroyed when
+ // SetChildHistoryEntry() does SwapHistoryEntries() (bug 304639).
+ nsCOMPtr<nsISHEntry> oldRootEntry = nsSHistory::GetRootSHEntry(this);
+
+ if (oldRootEntry) {
+ nsSHistory::SwapEntriesData data = {aIgnoreBC, newRootEntry, nullptr};
+ nsSHistory::SetChildHistoryEntry(oldRootEntry, aTopBC, 0, &data);
+ }
+ }
+}
+
+void SessionHistoryEntry::ReplaceWith(const SessionHistoryEntry& aSource) {
+ mInfo = MakeUnique<SessionHistoryInfo>(*aSource.mInfo);
+ mChildren.Clear();
+}
+
+SHEntrySharedParentState* SessionHistoryEntry::SharedInfo() const {
+ return static_cast<SHEntrySharedParentState*>(mInfo->mSharedState.Get());
+}
+
+void SessionHistoryEntry::SetFrameLoader(nsFrameLoader* aFrameLoader) {
+ MOZ_DIAGNOSTIC_ASSERT(!aFrameLoader || !SharedInfo()->mFrameLoader);
+ // If the pref is disabled, we still allow evicting the existing entries.
+ MOZ_RELEASE_ASSERT(!aFrameLoader || mozilla::BFCacheInParent());
+ SharedInfo()->SetFrameLoader(aFrameLoader);
+ if (aFrameLoader) {
+ if (BrowsingContext* bc = aFrameLoader->GetMaybePendingBrowsingContext()) {
+ bc->PreOrderWalk([&](BrowsingContext* aContext) {
+ if (BrowserParent* bp = aContext->Canonical()->GetBrowserParent()) {
+ bp->Deactivated();
+ }
+ });
+ }
+
+ // When a new frameloader is stored, try to evict some older
+ // frameloaders. Non-SHIP session history has a similar call in
+ // nsDocumentViewer::Show.
+ nsCOMPtr<nsISHistory> shistory;
+ GetShistory(getter_AddRefs(shistory));
+ if (shistory) {
+ int32_t index = 0;
+ shistory->GetIndex(&index);
+ shistory->EvictOutOfRangeDocumentViewers(index);
+ }
+ }
+}
+
+nsFrameLoader* SessionHistoryEntry::GetFrameLoader() {
+ return SharedInfo()->mFrameLoader;
+}
+
+void SessionHistoryEntry::SetInfo(SessionHistoryInfo* aInfo) {
+ // FIXME Assert that we're not changing shared state!
+ mInfo = MakeUnique<SessionHistoryInfo>(*aInfo);
+}
+
+} // namespace dom
+
+namespace ipc {
+
+void IPDLParamTraits<dom::SessionHistoryInfo>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::SessionHistoryInfo& aParam) {
+ nsCOMPtr<nsIInputStream> postData = aParam.GetPostData();
+
+ Maybe<std::tuple<uint32_t, dom::ClonedMessageData>> stateData;
+ if (aParam.mStateData) {
+ stateData.emplace();
+ // FIXME: We should fail more aggressively if this fails, as currently we'll
+ // just early return and the deserialization will break.
+ NS_ENSURE_SUCCESS_VOID(
+ aParam.mStateData->GetFormatVersion(&std::get<0>(*stateData)));
+ NS_ENSURE_TRUE_VOID(
+ aParam.mStateData->BuildClonedMessageData(std::get<1>(*stateData)));
+ }
+
+ WriteIPDLParam(aWriter, aActor, aParam.mURI);
+ WriteIPDLParam(aWriter, aActor, aParam.mOriginalURI);
+ WriteIPDLParam(aWriter, aActor, aParam.mResultPrincipalURI);
+ WriteIPDLParam(aWriter, aActor, aParam.mUnstrippedURI);
+ WriteIPDLParam(aWriter, aActor, aParam.mReferrerInfo);
+ WriteIPDLParam(aWriter, aActor, aParam.mTitle);
+ WriteIPDLParam(aWriter, aActor, aParam.mName);
+ WriteIPDLParam(aWriter, aActor, postData);
+ WriteIPDLParam(aWriter, aActor, aParam.mLoadType);
+ WriteIPDLParam(aWriter, aActor, aParam.mScrollPositionX);
+ WriteIPDLParam(aWriter, aActor, aParam.mScrollPositionY);
+ WriteIPDLParam(aWriter, aActor, stateData);
+ WriteIPDLParam(aWriter, aActor, aParam.mSrcdocData);
+ WriteIPDLParam(aWriter, aActor, aParam.mBaseURI);
+ WriteIPDLParam(aWriter, aActor, aParam.mLoadReplace);
+ WriteIPDLParam(aWriter, aActor, aParam.mURIWasModified);
+ WriteIPDLParam(aWriter, aActor, aParam.mScrollRestorationIsManual);
+ WriteIPDLParam(aWriter, aActor, aParam.mPersist);
+ WriteIPDLParam(aWriter, aActor, aParam.mHasUserInteraction);
+ WriteIPDLParam(aWriter, aActor, aParam.mHasUserActivation);
+ WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mId);
+ WriteIPDLParam(aWriter, aActor,
+ aParam.mSharedState.Get()->mTriggeringPrincipal);
+ WriteIPDLParam(aWriter, aActor,
+ aParam.mSharedState.Get()->mPrincipalToInherit);
+ WriteIPDLParam(aWriter, aActor,
+ aParam.mSharedState.Get()->mPartitionedPrincipalToInherit);
+ WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mCsp);
+ WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mContentType);
+ WriteIPDLParam(aWriter, aActor,
+ aParam.mSharedState.Get()->mLayoutHistoryState);
+ WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mCacheKey);
+ WriteIPDLParam(aWriter, aActor,
+ aParam.mSharedState.Get()->mIsFrameNavigation);
+ WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mSaveLayoutState);
+}
+
+bool IPDLParamTraits<dom::SessionHistoryInfo>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::SessionHistoryInfo* aResult) {
+ Maybe<std::tuple<uint32_t, dom::ClonedMessageData>> stateData;
+ uint64_t sharedId;
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mURI) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mOriginalURI) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mResultPrincipalURI) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mUnstrippedURI) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mReferrerInfo) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mTitle) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mName) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mPostData) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mLoadType) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mScrollPositionX) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mScrollPositionY) ||
+ !ReadIPDLParam(aReader, aActor, &stateData) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mSrcdocData) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mBaseURI) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mLoadReplace) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mURIWasModified) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mScrollRestorationIsManual) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mPersist) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mHasUserInteraction) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mHasUserActivation) ||
+ !ReadIPDLParam(aReader, aActor, &sharedId)) {
+ aActor->FatalError("Error reading fields for SessionHistoryInfo");
+ return false;
+ }
+
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ nsCOMPtr<nsIPrincipal> principalToInherit;
+ nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit;
+ nsCOMPtr<nsIContentSecurityPolicy> csp;
+ nsCString contentType;
+ if (!ReadIPDLParam(aReader, aActor, &triggeringPrincipal) ||
+ !ReadIPDLParam(aReader, aActor, &principalToInherit) ||
+ !ReadIPDLParam(aReader, aActor, &partitionedPrincipalToInherit) ||
+ !ReadIPDLParam(aReader, aActor, &csp) ||
+ !ReadIPDLParam(aReader, aActor, &contentType)) {
+ aActor->FatalError("Error reading fields for SessionHistoryInfo");
+ return false;
+ }
+
+ // We should always see a cloneable input stream passed to SessionHistoryInfo.
+ // This is because it will be cloneable when first read in the parent process
+ // from the nsHttpChannel (which forces streams to be cloneable), and future
+ // streams in content will be wrapped in
+ // nsMIMEInputStream(RemoteLazyInputStream) which is also cloneable.
+ if (aResult->mPostData && !NS_InputStreamIsCloneable(aResult->mPostData)) {
+ aActor->FatalError(
+ "Unexpected non-cloneable postData for SessionHistoryInfo");
+ return false;
+ }
+
+ dom::SHEntrySharedParentState* sharedState = nullptr;
+ if (XRE_IsParentProcess()) {
+ sharedState = dom::SHEntrySharedParentState::Lookup(sharedId);
+ }
+
+ if (sharedState) {
+ aResult->mSharedState.Set(sharedState);
+
+ MOZ_ASSERT(triggeringPrincipal
+ ? triggeringPrincipal->Equals(
+ aResult->mSharedState.Get()->mTriggeringPrincipal)
+ : !aResult->mSharedState.Get()->mTriggeringPrincipal,
+ "We don't expect this to change!");
+ MOZ_ASSERT(principalToInherit
+ ? principalToInherit->Equals(
+ aResult->mSharedState.Get()->mPrincipalToInherit)
+ : !aResult->mSharedState.Get()->mPrincipalToInherit,
+ "We don't expect this to change!");
+ MOZ_ASSERT(
+ partitionedPrincipalToInherit
+ ? partitionedPrincipalToInherit->Equals(
+ aResult->mSharedState.Get()->mPartitionedPrincipalToInherit)
+ : !aResult->mSharedState.Get()->mPartitionedPrincipalToInherit,
+ "We don't expect this to change!");
+ MOZ_ASSERT(
+ csp ? nsCSPContext::Equals(csp, aResult->mSharedState.Get()->mCsp)
+ : !aResult->mSharedState.Get()->mCsp,
+ "We don't expect this to change!");
+ MOZ_ASSERT(contentType.Equals(aResult->mSharedState.Get()->mContentType),
+ "We don't expect this to change!");
+ } else {
+ aResult->mSharedState.ChangeId(sharedId);
+ aResult->mSharedState.Get()->mTriggeringPrincipal =
+ triggeringPrincipal.forget();
+ aResult->mSharedState.Get()->mPrincipalToInherit =
+ principalToInherit.forget();
+ aResult->mSharedState.Get()->mPartitionedPrincipalToInherit =
+ partitionedPrincipalToInherit.forget();
+ aResult->mSharedState.Get()->mCsp = csp.forget();
+ aResult->mSharedState.Get()->mContentType = contentType;
+ }
+
+ if (!ReadIPDLParam(aReader, aActor,
+ &aResult->mSharedState.Get()->mLayoutHistoryState) ||
+ !ReadIPDLParam(aReader, aActor,
+ &aResult->mSharedState.Get()->mCacheKey) ||
+ !ReadIPDLParam(aReader, aActor,
+ &aResult->mSharedState.Get()->mIsFrameNavigation) ||
+ !ReadIPDLParam(aReader, aActor,
+ &aResult->mSharedState.Get()->mSaveLayoutState)) {
+ aActor->FatalError("Error reading fields for SessionHistoryInfo");
+ return false;
+ }
+
+ if (stateData.isSome()) {
+ uint32_t version = std::get<0>(*stateData);
+ aResult->mStateData = new nsStructuredCloneContainer(version);
+ aResult->mStateData->StealFromClonedMessageData(std::get<1>(*stateData));
+ }
+ MOZ_ASSERT_IF(stateData.isNothing(), !aResult->mStateData);
+ return true;
+}
+
+void IPDLParamTraits<dom::LoadingSessionHistoryInfo>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::LoadingSessionHistoryInfo& aParam) {
+ WriteIPDLParam(aWriter, aActor, aParam.mInfo);
+ WriteIPDLParam(aWriter, aActor, aParam.mLoadId);
+ WriteIPDLParam(aWriter, aActor, aParam.mLoadIsFromSessionHistory);
+ WriteIPDLParam(aWriter, aActor, aParam.mOffset);
+ WriteIPDLParam(aWriter, aActor, aParam.mLoadingCurrentEntry);
+ WriteIPDLParam(aWriter, aActor, aParam.mForceMaybeResetName);
+}
+
+bool IPDLParamTraits<dom::LoadingSessionHistoryInfo>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::LoadingSessionHistoryInfo* aResult) {
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mInfo) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mLoadId) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mLoadIsFromSessionHistory) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mOffset) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mLoadingCurrentEntry) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mForceMaybeResetName)) {
+ aActor->FatalError("Error reading fields for LoadingSessionHistoryInfo");
+ return false;
+ }
+
+ return true;
+}
+
+void IPDLParamTraits<nsILayoutHistoryState*>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ nsILayoutHistoryState* aParam) {
+ if (aParam) {
+ WriteIPDLParam(aWriter, aActor, true);
+ bool scrollPositionOnly = false;
+ nsTArray<nsCString> keys;
+ nsTArray<mozilla::PresState> states;
+ aParam->GetContents(&scrollPositionOnly, keys, states);
+ WriteIPDLParam(aWriter, aActor, scrollPositionOnly);
+ WriteIPDLParam(aWriter, aActor, keys);
+ WriteIPDLParam(aWriter, aActor, states);
+ } else {
+ WriteIPDLParam(aWriter, aActor, false);
+ }
+}
+
+bool IPDLParamTraits<nsILayoutHistoryState*>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<nsILayoutHistoryState>* aResult) {
+ bool hasLayoutHistoryState = false;
+ if (!ReadIPDLParam(aReader, aActor, &hasLayoutHistoryState)) {
+ aActor->FatalError("Error reading fields for nsILayoutHistoryState");
+ return false;
+ }
+
+ if (hasLayoutHistoryState) {
+ bool scrollPositionOnly = false;
+ nsTArray<nsCString> keys;
+ nsTArray<mozilla::PresState> states;
+ if (!ReadIPDLParam(aReader, aActor, &scrollPositionOnly) ||
+ !ReadIPDLParam(aReader, aActor, &keys) ||
+ !ReadIPDLParam(aReader, aActor, &states)) {
+ aActor->FatalError("Error reading fields for nsILayoutHistoryState");
+ }
+
+ if (keys.Length() != states.Length()) {
+ aActor->FatalError("Error reading fields for nsILayoutHistoryState");
+ return false;
+ }
+
+ *aResult = NS_NewLayoutHistoryState();
+ (*aResult)->SetScrollPositionOnly(scrollPositionOnly);
+ for (uint32_t i = 0; i < keys.Length(); ++i) {
+ PresState& state = states[i];
+ UniquePtr<PresState> newState = MakeUnique<PresState>(state);
+ (*aResult)->AddState(keys[i], std::move(newState));
+ }
+ }
+ return true;
+}
+
+void IPDLParamTraits<mozilla::dom::Wireframe>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const mozilla::dom::Wireframe& aParam) {
+ WriteParam(aWriter, aParam.mCanvasBackground);
+ WriteParam(aWriter, aParam.mRects);
+}
+
+bool IPDLParamTraits<mozilla::dom::Wireframe>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ mozilla::dom::Wireframe* aResult) {
+ return ReadParam(aReader, &aResult->mCanvasBackground) &&
+ ReadParam(aReader, &aResult->mRects);
+}
+
+} // namespace ipc
+} // namespace mozilla
+
+namespace IPC {
+// Allow sending mozilla::dom::WireframeRectType enums over IPC.
+template <>
+struct ParamTraits<mozilla::dom::WireframeRectType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::WireframeRectType,
+ mozilla::dom::WireframeRectType::Image,
+ mozilla::dom::WireframeRectType::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::WireframeTaggedRect> {
+ static void Write(MessageWriter* aWriter,
+ const mozilla::dom::WireframeTaggedRect& aParam);
+ static bool Read(MessageReader* aReader,
+ mozilla::dom::WireframeTaggedRect* aResult);
+};
+
+void ParamTraits<mozilla::dom::WireframeTaggedRect>::Write(
+ MessageWriter* aWriter, const mozilla::dom::WireframeTaggedRect& aParam) {
+ WriteParam(aWriter, aParam.mColor);
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mX);
+ WriteParam(aWriter, aParam.mY);
+ WriteParam(aWriter, aParam.mWidth);
+ WriteParam(aWriter, aParam.mHeight);
+}
+
+bool ParamTraits<mozilla::dom::WireframeTaggedRect>::Read(
+ IPC::MessageReader* aReader, mozilla::dom::WireframeTaggedRect* aResult) {
+ return ReadParam(aReader, &aResult->mColor) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mX) && ReadParam(aReader, &aResult->mY) &&
+ ReadParam(aReader, &aResult->mWidth) &&
+ ReadParam(aReader, &aResult->mHeight);
+}
+} // namespace IPC
diff --git a/docshell/shistory/SessionHistoryEntry.h b/docshell/shistory/SessionHistoryEntry.h
new file mode 100644
index 0000000000..c7f1fea5e9
--- /dev/null
+++ b/docshell/shistory/SessionHistoryEntry.h
@@ -0,0 +1,510 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SessionHistoryEntry_h
+#define mozilla_dom_SessionHistoryEntry_h
+
+#include "mozilla/dom/DocumentBinding.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "nsILayoutHistoryState.h"
+#include "nsISHEntry.h"
+#include "nsSHEntryShared.h"
+#include "nsStructuredCloneContainer.h"
+#include "nsTHashMap.h"
+#include "nsWeakReference.h"
+
+class nsDocShellLoadState;
+class nsIChannel;
+class nsIInputStream;
+class nsIReferrerInfo;
+class nsISHistory;
+class nsIURI;
+
+namespace mozilla::ipc {
+template <typename P>
+struct IPDLParamTraits;
+}
+
+namespace mozilla {
+namespace dom {
+
+struct LoadingSessionHistoryInfo;
+class SessionHistoryEntry;
+class SHEntrySharedParentState;
+
+// SessionHistoryInfo stores session history data for a load. It can be sent
+// over IPC and is used in both the parent and the child processes.
+class SessionHistoryInfo {
+ public:
+ SessionHistoryInfo() = default;
+ SessionHistoryInfo(const SessionHistoryInfo& aInfo) = default;
+ SessionHistoryInfo(nsDocShellLoadState* aLoadState, nsIChannel* aChannel);
+ SessionHistoryInfo(const SessionHistoryInfo& aSharedStateFrom, nsIURI* aURI);
+ SessionHistoryInfo(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ const nsACString& aContentType);
+ SessionHistoryInfo(nsIChannel* aChannel, uint32_t aLoadType,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp);
+
+ void Reset(nsIURI* aURI, const nsID& aDocShellID, bool aDynamicCreation,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, const nsACString& aContentType);
+
+ bool operator==(const SessionHistoryInfo& aInfo) const {
+ return false; // FIXME
+ }
+
+ nsIURI* GetURI() const { return mURI; }
+ void SetURI(nsIURI* aURI) { mURI = aURI; }
+
+ nsIURI* GetOriginalURI() const { return mOriginalURI; }
+ void SetOriginalURI(nsIURI* aOriginalURI) { mOriginalURI = aOriginalURI; }
+
+ nsIURI* GetUnstrippedURI() const { return mUnstrippedURI; }
+ void SetUnstrippedURI(nsIURI* aUnstrippedURI) {
+ mUnstrippedURI = aUnstrippedURI;
+ }
+
+ nsIURI* GetResultPrincipalURI() const { return mResultPrincipalURI; }
+ void SetResultPrincipalURI(nsIURI* aResultPrincipalURI) {
+ mResultPrincipalURI = aResultPrincipalURI;
+ }
+
+ nsCOMPtr<nsIReferrerInfo> GetReferrerInfo() { return mReferrerInfo; }
+ void SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
+ mReferrerInfo = aReferrerInfo;
+ }
+
+ bool HasPostData() const { return mPostData; }
+ already_AddRefed<nsIInputStream> GetPostData() const;
+ void SetPostData(nsIInputStream* aPostData);
+
+ void GetScrollPosition(int32_t* aScrollPositionX, int32_t* aScrollPositionY) {
+ *aScrollPositionX = mScrollPositionX;
+ *aScrollPositionY = mScrollPositionY;
+ }
+
+ void SetScrollPosition(int32_t aScrollPositionX, int32_t aScrollPositionY) {
+ mScrollPositionX = aScrollPositionX;
+ mScrollPositionY = aScrollPositionY;
+ }
+
+ bool GetScrollRestorationIsManual() const {
+ return mScrollRestorationIsManual;
+ }
+ const nsAString& GetTitle() { return mTitle; }
+ void SetTitle(const nsAString& aTitle) {
+ mTitle = aTitle;
+ MaybeUpdateTitleFromURI();
+ }
+
+ const nsAString& GetName() { return mName; }
+ void SetName(const nsAString& aName) { mName = aName; }
+
+ void SetScrollRestorationIsManual(bool aIsManual) {
+ mScrollRestorationIsManual = aIsManual;
+ }
+
+ nsStructuredCloneContainer* GetStateData() const { return mStateData; }
+ void SetStateData(nsStructuredCloneContainer* aStateData) {
+ mStateData = aStateData;
+ }
+
+ void SetLoadReplace(bool aLoadReplace) { mLoadReplace = aLoadReplace; }
+
+ void SetURIWasModified(bool aURIWasModified) {
+ mURIWasModified = aURIWasModified;
+ }
+ bool GetURIWasModified() const { return mURIWasModified; }
+
+ void SetHasUserInteraction(bool aHasUserInteraction) {
+ mHasUserInteraction = aHasUserInteraction;
+ }
+ bool GetHasUserInteraction() const { return mHasUserInteraction; }
+
+ uint64_t SharedId() const;
+
+ nsILayoutHistoryState* GetLayoutHistoryState();
+ void SetLayoutHistoryState(nsILayoutHistoryState* aState);
+
+ nsIPrincipal* GetTriggeringPrincipal() const;
+
+ nsIPrincipal* GetPrincipalToInherit() const;
+
+ nsIPrincipal* GetPartitionedPrincipalToInherit() const;
+
+ nsIContentSecurityPolicy* GetCsp() const;
+
+ uint32_t GetCacheKey() const;
+ void SetCacheKey(uint32_t aCacheKey);
+
+ bool IsSubFrame() const;
+
+ bool SharesDocumentWith(const SessionHistoryInfo& aOther) const {
+ return SharedId() == aOther.SharedId();
+ }
+
+ void FillLoadInfo(nsDocShellLoadState& aLoadState) const;
+
+ uint32_t LoadType() { return mLoadType; }
+
+ void SetSaveLayoutStateFlag(bool aSaveLayoutStateFlag);
+
+ bool GetPersist() const { return mPersist; }
+
+ private:
+ friend class SessionHistoryEntry;
+ friend struct mozilla::ipc::IPDLParamTraits<SessionHistoryInfo>;
+
+ void MaybeUpdateTitleFromURI();
+
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIURI> mOriginalURI;
+ nsCOMPtr<nsIURI> mResultPrincipalURI;
+ nsCOMPtr<nsIURI> mUnstrippedURI;
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+ nsString mTitle;
+ nsString mName;
+ nsCOMPtr<nsIInputStream> mPostData;
+ uint32_t mLoadType = 0;
+ int32_t mScrollPositionX = 0;
+ int32_t mScrollPositionY = 0;
+ RefPtr<nsStructuredCloneContainer> mStateData;
+ Maybe<nsString> mSrcdocData;
+ nsCOMPtr<nsIURI> mBaseURI;
+
+ bool mLoadReplace = false;
+ bool mURIWasModified = false;
+ bool mScrollRestorationIsManual = false;
+ bool mPersist = true;
+ bool mHasUserInteraction = false;
+ bool mHasUserActivation = false;
+
+ union SharedState {
+ SharedState();
+ explicit SharedState(const SharedState& aOther);
+ explicit SharedState(const Maybe<const SharedState&>& aOther);
+ ~SharedState();
+
+ SharedState& operator=(const SharedState& aOther);
+
+ SHEntrySharedState* Get() const;
+
+ void Set(SHEntrySharedParentState* aState) { mParent = aState; }
+
+ void ChangeId(uint64_t aId);
+
+ static SharedState Create(nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ const nsACString& aContentType);
+
+ private:
+ explicit SharedState(SHEntrySharedParentState* aParent)
+ : mParent(aParent) {}
+ explicit SharedState(UniquePtr<SHEntrySharedState>&& aChild)
+ : mChild(std::move(aChild)) {}
+
+ void Init();
+ void Init(const SharedState& aOther);
+
+ // In the parent process this holds a strong reference to the refcounted
+ // SHEntrySharedParentState. In the child processes this holds an owning
+ // pointer to a SHEntrySharedState.
+ RefPtr<SHEntrySharedParentState> mParent;
+ UniquePtr<SHEntrySharedState> mChild;
+ };
+
+ SharedState mSharedState;
+};
+
+struct LoadingSessionHistoryInfo {
+ LoadingSessionHistoryInfo() = default;
+ explicit LoadingSessionHistoryInfo(SessionHistoryEntry* aEntry);
+ // Initializes mInfo using aEntry and otherwise copies the values from aInfo.
+ LoadingSessionHistoryInfo(SessionHistoryEntry* aEntry,
+ const LoadingSessionHistoryInfo* aInfo);
+ // For about:blank only.
+ explicit LoadingSessionHistoryInfo(const SessionHistoryInfo& aInfo);
+
+ already_AddRefed<nsDocShellLoadState> CreateLoadInfo() const;
+
+ SessionHistoryInfo mInfo;
+
+ uint64_t mLoadId = 0;
+
+ // The following three member variables are used to inform about a load from
+ // the session history. The session-history-in-child approach has just
+ // an nsISHEntry in the nsDocShellLoadState and access to the nsISHistory,
+ // but session-history-in-parent needs to pass needed information explicitly
+ // to the relevant child process.
+ bool mLoadIsFromSessionHistory = false;
+ // mOffset and mLoadingCurrentEntry are relevant only if
+ // mLoadIsFromSessionHistory is true.
+ int32_t mOffset = 0;
+ // If we're loading from the current entry we want to treat it as not a
+ // same-document navigation (see nsDocShell::IsSameDocumentNavigation).
+ bool mLoadingCurrentEntry = false;
+ // If mForceMaybeResetName.isSome() is true then the parent process has
+ // determined whether the BC's name should be cleared and stored in session
+ // history (see https://html.spec.whatwg.org/#history-traversal step 4.2).
+ // This is used when we're replacing the BC for BFCache in the parent. In
+ // other cases mForceMaybeResetName.isSome() will be false and the child
+ // process should be able to make that determination itself.
+ Maybe<bool> mForceMaybeResetName;
+};
+
+// HistoryEntryCounterForBrowsingContext is used to count the number of entries
+// which are added to the session history for a particular browsing context.
+// If a SessionHistoryEntry is cloned because of navigation in some other
+// browsing context, that doesn't cause the counter value to be increased.
+// The browsing context specific counter is needed to make it easier to
+// synchronously update history.length value in a child process when
+// an iframe is removed from DOM.
+class HistoryEntryCounterForBrowsingContext {
+ public:
+ HistoryEntryCounterForBrowsingContext()
+ : mCounter(new RefCountedCounter()), mHasModified(false) {
+ ++(*this);
+ }
+
+ HistoryEntryCounterForBrowsingContext(
+ const HistoryEntryCounterForBrowsingContext& aOther)
+ : mCounter(aOther.mCounter), mHasModified(false) {}
+
+ HistoryEntryCounterForBrowsingContext(
+ HistoryEntryCounterForBrowsingContext&& aOther) = delete;
+
+ ~HistoryEntryCounterForBrowsingContext() {
+ if (mHasModified) {
+ --(*mCounter);
+ }
+ }
+
+ void CopyValueFrom(const HistoryEntryCounterForBrowsingContext& aOther) {
+ if (mHasModified) {
+ --(*mCounter);
+ }
+ mCounter = aOther.mCounter;
+ mHasModified = false;
+ }
+
+ HistoryEntryCounterForBrowsingContext& operator=(
+ const HistoryEntryCounterForBrowsingContext& aOther) = delete;
+
+ HistoryEntryCounterForBrowsingContext& operator++() {
+ mHasModified = true;
+ ++(*mCounter);
+ return *this;
+ }
+
+ operator uint32_t() const { return *mCounter; }
+
+ bool Modified() { return mHasModified; }
+
+ void SetModified(bool aModified) { mHasModified = aModified; }
+
+ void Reset() {
+ if (mHasModified) {
+ --(*mCounter);
+ }
+ mCounter = new RefCountedCounter();
+ mHasModified = false;
+ }
+
+ private:
+ class RefCountedCounter {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(
+ mozilla::dom::HistoryEntryCounterForBrowsingContext::RefCountedCounter)
+
+ RefCountedCounter& operator++() {
+ ++mCounter;
+ return *this;
+ }
+
+ RefCountedCounter& operator--() {
+ --mCounter;
+ return *this;
+ }
+
+ operator uint32_t() const { return mCounter; }
+
+ private:
+ ~RefCountedCounter() = default;
+
+ uint32_t mCounter = 0;
+ };
+
+ RefPtr<RefCountedCounter> mCounter;
+ bool mHasModified;
+};
+
+// SessionHistoryEntry is used to store session history data in the parent
+// process. It holds a SessionHistoryInfo, some state shared amongst multiple
+// SessionHistoryEntries, a parent and children.
+#define NS_SESSIONHISTORYENTRY_IID \
+ { \
+ 0x5b66a244, 0x8cec, 0x4caa, { \
+ 0xaa, 0x0a, 0x78, 0x92, 0xfd, 0x17, 0xa6, 0x67 \
+ } \
+ }
+
+class SessionHistoryEntry : public nsISHEntry, public nsSupportsWeakReference {
+ public:
+ SessionHistoryEntry(nsDocShellLoadState* aLoadState, nsIChannel* aChannel);
+ SessionHistoryEntry();
+ explicit SessionHistoryEntry(SessionHistoryInfo* aInfo);
+ explicit SessionHistoryEntry(const SessionHistoryEntry& aEntry);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHENTRY
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_SESSIONHISTORYENTRY_IID)
+
+ bool IsInSessionHistory() {
+ SessionHistoryEntry* entry = this;
+ while (nsCOMPtr<SessionHistoryEntry> parent =
+ do_QueryReferent(entry->mParent)) {
+ entry = parent;
+ }
+ return entry->SharedInfo()->mSHistory &&
+ entry->SharedInfo()->mSHistory->IsAlive();
+ }
+
+ void ReplaceWith(const SessionHistoryEntry& aSource);
+
+ const SessionHistoryInfo& Info() const { return *mInfo; }
+
+ SHEntrySharedParentState* SharedInfo() const;
+
+ void SetFrameLoader(nsFrameLoader* aFrameLoader);
+ nsFrameLoader* GetFrameLoader();
+
+ void AddChild(SessionHistoryEntry* aChild, int32_t aOffset,
+ bool aUseRemoteSubframes);
+ void RemoveChild(SessionHistoryEntry* aChild);
+ // Finds the child with the same docshell ID as aNewChild, replaces it with
+ // aNewChild and returns true. If there is no child with the same docshell ID
+ // then it returns false.
+ bool ReplaceChild(SessionHistoryEntry* aNewChild);
+
+ void SetInfo(SessionHistoryInfo* aInfo);
+
+ bool ForInitialLoad() { return mForInitialLoad; }
+ void SetForInitialLoad(bool aForInitialLoad) {
+ mForInitialLoad = aForInitialLoad;
+ }
+
+ const nsID& DocshellID() const;
+
+ HistoryEntryCounterForBrowsingContext& BCHistoryLength() {
+ return mBCHistoryLength;
+ }
+
+ void SetBCHistoryLength(HistoryEntryCounterForBrowsingContext& aCounter) {
+ mBCHistoryLength.CopyValueFrom(aCounter);
+ }
+
+ void ClearBCHistoryLength() { mBCHistoryLength.Reset(); }
+
+ void SetIsDynamicallyAdded(bool aDynamic);
+
+ void SetWireframe(const Maybe<Wireframe>& aWireframe);
+
+ struct LoadingEntry {
+ // A pointer to the entry being loaded. Will be cleared by the
+ // SessionHistoryEntry destructor, at latest.
+ SessionHistoryEntry* mEntry;
+ // Snapshot of the entry's SessionHistoryInfo when the load started, to be
+ // used for validation purposes only.
+ UniquePtr<SessionHistoryInfo> mInfoSnapshotForValidation;
+ };
+
+ // Get an entry based on LoadingSessionHistoryInfo's mLoadId. Parent process
+ // only.
+ static LoadingEntry* GetByLoadId(uint64_t aLoadId);
+ static void SetByLoadId(uint64_t aLoadId, SessionHistoryEntry* aEntry);
+ static void RemoveLoadId(uint64_t aLoadId);
+
+ const nsTArray<RefPtr<SessionHistoryEntry>>& Children() { return mChildren; }
+
+ private:
+ friend struct LoadingSessionHistoryInfo;
+ virtual ~SessionHistoryEntry();
+
+ UniquePtr<SessionHistoryInfo> mInfo;
+ nsWeakPtr mParent;
+ uint32_t mID;
+ nsTArray<RefPtr<SessionHistoryEntry>> mChildren;
+ Maybe<Wireframe> mWireframe;
+
+ bool mForInitialLoad = false;
+
+ HistoryEntryCounterForBrowsingContext mBCHistoryLength;
+
+ static nsTHashMap<nsUint64HashKey, LoadingEntry>* sLoadIdToEntry;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(SessionHistoryEntry, NS_SESSIONHISTORYENTRY_IID)
+
+} // namespace dom
+
+namespace ipc {
+
+class IProtocol;
+
+// Allow sending SessionHistoryInfo objects over IPC.
+template <>
+struct IPDLParamTraits<dom::SessionHistoryInfo> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::SessionHistoryInfo& aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::SessionHistoryInfo* aResult);
+};
+
+// Allow sending LoadingSessionHistoryInfo objects over IPC.
+template <>
+struct IPDLParamTraits<dom::LoadingSessionHistoryInfo> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const dom::LoadingSessionHistoryInfo& aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ dom::LoadingSessionHistoryInfo* aResult);
+};
+
+// Allow sending nsILayoutHistoryState objects over IPC.
+template <>
+struct IPDLParamTraits<nsILayoutHistoryState*> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ nsILayoutHistoryState* aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<nsILayoutHistoryState>* aResult);
+};
+
+// Allow sending dom::Wireframe objects over IPC.
+template <>
+struct IPDLParamTraits<mozilla::dom::Wireframe> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const mozilla::dom::Wireframe& aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ mozilla::dom::Wireframe* aResult);
+};
+
+} // namespace ipc
+
+} // namespace mozilla
+
+inline nsISupports* ToSupports(mozilla::dom::SessionHistoryEntry* aEntry) {
+ return static_cast<nsISHEntry*>(aEntry);
+}
+
+#endif /* mozilla_dom_SessionHistoryEntry_h */
diff --git a/docshell/shistory/moz.build b/docshell/shistory/moz.build
new file mode 100644
index 0000000000..f3b86c207b
--- /dev/null
+++ b/docshell/shistory/moz.build
@@ -0,0 +1,42 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsIBFCacheEntry.idl",
+ "nsISHEntry.idl",
+ "nsISHistory.idl",
+ "nsISHistoryListener.idl",
+]
+
+XPIDL_MODULE = "shistory"
+
+EXPORTS += [
+ "nsSHEntry.h",
+ "nsSHEntryShared.h",
+ "nsSHistory.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "ChildSHistory.h",
+ "SessionHistoryEntry.h",
+]
+
+UNIFIED_SOURCES += [
+ "ChildSHistory.cpp",
+ "nsSHEntry.cpp",
+ "nsSHEntryShared.cpp",
+ "nsSHistory.cpp",
+ "SessionHistoryEntry.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/docshell/base",
+ "/dom/base",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/docshell/shistory/nsIBFCacheEntry.idl b/docshell/shistory/nsIBFCacheEntry.idl
new file mode 100644
index 0000000000..2e24c67e35
--- /dev/null
+++ b/docshell/shistory/nsIBFCacheEntry.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This interface lets you evict a document from the back/forward cache.
+ */
+[scriptable, builtinclass, uuid(a576060e-c7df-4d81-aa8c-5b52bd6ad25d)]
+interface nsIBFCacheEntry : nsISupports
+{
+ void RemoveFromBFCacheSync();
+ void RemoveFromBFCacheAsync();
+};
diff --git a/docshell/shistory/nsISHEntry.idl b/docshell/shistory/nsISHEntry.idl
new file mode 100644
index 0000000000..1d7427f581
--- /dev/null
+++ b/docshell/shistory/nsISHEntry.idl
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The interface to nsISHentry. Each document or subframe in
+ * Session History will have a nsISHEntry associated with it which will
+ * hold all information required to recreate the document from history
+ */
+
+#include "nsISupports.idl"
+
+interface nsIContentSecurityPolicy;
+interface nsIMutableArray;
+interface nsILayoutHistoryState;
+interface nsIDocumentViewer;
+interface nsIURI;
+interface nsIInputStream;
+interface nsIDocShellTreeItem;
+interface nsIStructuredCloneContainer;
+interface nsIBFCacheEntry;
+interface nsIPrincipal;
+interface nsISHistory;
+interface nsIReferrerInfo;
+
+%{C++
+#include "nsRect.h"
+class nsDocShellEditorData;
+
+namespace mozilla {
+namespace dom {
+
+class SHEntrySharedParentState;
+
+}
+}
+class nsSHEntryShared;
+class nsDocShellLoadState;
+struct EntriesAndBrowsingContextData;
+%}
+[ref] native nsIntRect(nsIntRect);
+[ptr] native nsDocShellEditorDataPtr(nsDocShellEditorData);
+[ptr] native nsDocShellLoadStatePtr(nsDocShellLoadState);
+[ptr] native SHEntrySharedParentStatePtr(mozilla::dom::SHEntrySharedParentState);
+webidl BrowsingContext;
+
+[builtinclass, scriptable, uuid(0dad26b8-a259-42c7-93f1-2fa7fc076e45)]
+interface nsISHEntry : nsISupports
+{
+ /**
+ * The URI of the current entry.
+ */
+ [infallible] attribute nsIURI URI;
+
+ /**
+ * The original URI of the current entry. If an entry is the result of a
+ * redirect this attribute holds the original URI.
+ */
+ [infallible] attribute nsIURI originalURI;
+
+ /**
+ * URL as stored from nsILoadInfo.resultPrincipalURI. See nsILoadInfo
+ * for more details.
+ */
+ [infallible] attribute nsIURI resultPrincipalURI;
+
+ /**
+ * If non-null, the URI as it was before query stripping was performed.
+ */
+ [infallible] attribute nsIURI unstrippedURI;
+
+ /**
+ * This flag remembers whether channel has LOAD_REPLACE set.
+ */
+ [infallible] attribute boolean loadReplace;
+
+ /**
+ * The title of the current entry.
+ */
+ // XXX: make it [infallible] when AString supports that (bug 1491187).
+ attribute AString title;
+
+ /**
+ * The name of the browsing context.
+ */
+ attribute AString name;
+
+ /**
+ * Was the entry created as a result of a subframe navigation?
+ * - Will be 'false' when a frameset page is visited for the first time.
+ * - Will be 'true' for all history entries created as a result of a
+ * subframe navigation.
+ */
+ [infallible] attribute boolean isSubFrame;
+
+ /**
+ * Whether the user interacted with the page while this entry was active.
+ * This includes interactions with subframe documents associated with
+ * child entries that are rooted at this entry.
+ * This field will only be set on top-level entries.
+ */
+ [infallible] attribute boolean hasUserInteraction;
+
+ /**
+ * Whether the load that created this entry was triggered by user activation.
+ * (e.g.: The user clicked a link)
+ * Remembering this flag enables replaying the sec-fetch-* headers.
+ */
+ [infallible] attribute boolean hasUserActivation;
+
+ /** Referrer Info*/
+ [infallible] attribute nsIReferrerInfo referrerInfo;
+
+ /** Document viewer, for fast restoration of presentation */
+ [infallible] attribute nsIDocumentViewer documentViewer;
+
+ [infallible] readonly attribute boolean isInBFCache;
+
+ /** Whether the content viewer is marked "sticky" */
+ [infallible] attribute boolean sticky;
+
+ /** Saved state of the global window object */
+ [infallible] attribute nsISupports windowState;
+
+ /** Saved refresh URI list for the content viewer */
+ [infallible] attribute nsIMutableArray refreshURIList;
+
+ /** Post Data for the document */
+ [infallible] attribute nsIInputStream postData;
+ [infallible] readonly attribute boolean hasPostData;
+
+ /** LayoutHistoryState for scroll position and form values */
+ [infallible] attribute nsILayoutHistoryState layoutHistoryState;
+
+ /** parent of this entry */
+ [infallible] attribute nsISHEntry parent;
+
+ /**
+ * The loadType for this entry. This is typically loadHistory except
+ * when reload is pressed, it has the appropriate reload flag
+ */
+ [infallible] attribute unsigned long loadType;
+
+ /**
+ * An ID to help identify this entry from others during
+ * subframe navigation
+ */
+ [infallible] attribute unsigned long ID;
+
+ /** The cache key for the entry */
+ [infallible] attribute unsigned long cacheKey;
+
+ /** Should the layoutHistoryState be saved? */
+ [infallible] attribute boolean saveLayoutStateFlag;
+
+ /**
+ * attribute to indicate the content-type of the document that this
+ * is a session history entry for
+ */
+ // XXX: make it [infallible] when ACString supports that (bug 1491187).
+ attribute ACString contentType;
+
+ /**
+ * If we created this SHEntry via history.pushState or modified it via
+ * history.replaceState, and if we changed the SHEntry's URI via the
+ * push/replaceState call, and if the SHEntry's new URI differs from its
+ * old URI by more than just the hash, then we set this field to true.
+ *
+ * Additionally, if this SHEntry was created by calling pushState from a
+ * SHEntry whose URI was modified, this SHEntry's URIWasModified field is
+ * true.
+ */
+ [infallible] attribute boolean URIWasModified;
+
+ /**
+ * Get the principal, if any, that was associated with the channel
+ * that the document that was loaded to create this history entry
+ * came from.
+ */
+ [infallible] attribute nsIPrincipal triggeringPrincipal;
+
+ /**
+ * Get the principal, if any, that is used when the inherit flag
+ * is set.
+ */
+ [infallible] attribute nsIPrincipal principalToInherit;
+
+ /**
+ * Get the storage principal, if any, that is used when the inherit flag is
+ * set.
+ */
+ [infallible] attribute nsIPrincipal partitionedPrincipalToInherit;
+
+ /**
+ * Get the csp, if any, that was used for this document load. That
+ * is not the CSP that was applied to subresource loads within the
+ * document, but the CSP that was applied to this document load.
+ */
+ [infallible] attribute nsIContentSecurityPolicy csp;
+
+ /**
+ * Get/set data associated with this history state via a pushState() call,
+ * serialized using structured clone.
+ **/
+ [infallible] attribute nsIStructuredCloneContainer stateData;
+
+ /**
+ * The history ID of the docshell.
+ */
+ // Would be [infallible], but we don't support that property for nsIDPtr.
+ attribute nsIDRef docshellID;
+
+ /**
+ * True if this SHEntry corresponds to a document created by a srcdoc
+ * iframe. Set when a value is assigned to srcdocData.
+ */
+ [infallible] readonly attribute boolean isSrcdocEntry;
+
+ /**
+ * Contents of the srcdoc attribute in a srcdoc iframe to be loaded instead
+ * of the URI. Similar to a Data URI, this information is needed to
+ * recreate the document at a later stage.
+ * Setting this sets isSrcdocEntry to true
+ */
+ // XXX: make it [infallible] when AString supports that (bug 1491187).
+ attribute AString srcdocData;
+
+ /**
+ * When isSrcdocEntry is true, this contains the baseURI of the srcdoc
+ * document for use in situations where it cannot otherwise be determined,
+ * for example with view-source.
+ */
+ [infallible] attribute nsIURI baseURI;
+
+ /**
+ * Sets/gets the current scroll restoration state,
+ * if true == "manual", false == "auto".
+ */
+ [infallible] attribute boolean scrollRestorationIsManual;
+
+ /**
+ * Flag to indicate that the history entry was originally loaded in the
+ * current process. This flag does not survive a browser process switch.
+ */
+ [infallible] readonly attribute boolean loadedInThisProcess;
+
+ /**
+ * The session history it belongs to. This is set only on the root entries.
+ */
+ [noscript, infallible] attribute nsISHistory shistory;
+
+ /**
+ * A number that is assigned by the sHistory when the entry is activated
+ */
+ [noscript, infallible] attribute unsigned long lastTouched;
+
+ /**
+ * The current number of nsISHEntries which are immediate children of this
+ * SHEntry.
+ */
+ [infallible] readonly attribute long childCount;
+
+ /**
+ * When an entry is serving is within nsISHistory's array of entries, this
+ * property specifies if it should persist. If not it will be replaced by
+ * new additions to the list.
+ */
+ [infallible] attribute boolean persist;
+
+ /**
+ * Set/Get the visual viewport scroll position if session history is
+ * changed through anchor navigation or pushState.
+ */
+ void setScrollPosition(in long x, in long y);
+ void getScrollPosition(out long x, out long y);
+
+ /**
+ * Saved position and dimensions of the content viewer; we must adjust the
+ * root view's widget accordingly if this has changed when the presentation
+ * is restored.
+ */
+ [noscript, notxpcom] void getViewerBounds(in nsIntRect bounds);
+ [noscript, notxpcom] void setViewerBounds([const] in nsIntRect bounds);
+
+ /**
+ * Saved child docshells corresponding to documentViewer. The child shells
+ * are restored as children of the parent docshell, in this order, when the
+ * parent docshell restores a saved presentation.
+ */
+
+ /** Append a child shell to the end of our list. */
+ [noscript, notxpcom] void addChildShell(in nsIDocShellTreeItem shell);
+
+ /**
+ * Get the child shell at |index|; returns null if |index| is out of bounds.
+ */
+ [noscript] nsIDocShellTreeItem childShellAt(in long index);
+
+ /**
+ * Clear the child shell list.
+ */
+ [noscript, notxpcom] void clearChildShells();
+
+ /**
+ * Ensure that the cached presentation members are self-consistent.
+ * If either |documentViewer| or |windowState| are null, then all of the
+ * following members are cleared/reset:
+ * documentViewer, sticky, windowState, viewerBounds, childShells,
+ * refreshURIList.
+ */
+ [noscript, notxpcom] void syncPresentationState();
+
+ /**
+ * Initialises `layoutHistoryState` if it doesn't already exist
+ * and returns a reference to it.
+ */
+ nsILayoutHistoryState initLayoutHistoryState();
+
+ /** Additional ways to create an entry */
+ [noscript] void create(in nsIURI URI, in AString title,
+ in nsIInputStream inputStream,
+ in unsigned long cacheKey,
+ in ACString contentType,
+ in nsIPrincipal triggeringPrincipal,
+ in nsIPrincipal principalToInherit,
+ in nsIPrincipal partitionedPrincipalToInherit,
+ in nsIContentSecurityPolicy aCsp,
+ in nsIDRef docshellID,
+ in boolean dynamicCreation,
+ in nsIURI originalURI,
+ in nsIURI resultPrincipalURI,
+ in nsIURI unstrippedURI,
+ in bool loadReplace,
+ in nsIReferrerInfo referrerInfo,
+ in AString srcdoc,
+ in bool srcdocEntry,
+ in nsIURI baseURI,
+ in bool saveLayoutState,
+ in bool expired,
+ in bool userActivation);
+
+ nsISHEntry clone();
+
+ /**
+ * Gets the owning pointer to the editor data assosicated with
+ * this shistory entry. This forgets its pointer, so free it when
+ * you're done.
+ */
+ [noscript, notxpcom] nsDocShellEditorDataPtr forgetEditorData();
+
+ /**
+ * Sets the owning pointer to the editor data assosicated with
+ * this shistory entry. Unless forgetEditorData() is called, this
+ * shentry will destroy the editor data when it's destroyed.
+ */
+ [noscript, notxpcom] void setEditorData(in nsDocShellEditorDataPtr aData);
+
+ /** Returns true if this shistory entry is storing a detached editor. */
+ [noscript, notxpcom] boolean hasDetachedEditor();
+
+ /**
+ * Returns true if the related docshell was added because of
+ * dynamic addition of an iframe/frame.
+ */
+ [noscript, notxpcom] boolean isDynamicallyAdded();
+
+ /**
+ * Returns true if any of the child entries returns true
+ * when isDynamicallyAdded is called on it.
+ */
+ boolean hasDynamicallyAddedChild();
+
+ /**
+ * Does this SHEntry point to the given BFCache entry? If so, evicting
+ * the BFCache entry will evict the SHEntry, since the two entries
+ * correspond to the same document.
+ */
+ [noscript, notxpcom]
+ boolean hasBFCacheEntry(in SHEntrySharedParentStatePtr aEntry);
+
+ /**
+ * Adopt aEntry's BFCacheEntry, so now both this and aEntry point to
+ * aEntry's BFCacheEntry.
+ */
+ void adoptBFCacheEntry(in nsISHEntry aEntry);
+
+ /**
+ * Create a new BFCache entry and drop our reference to our old one. This
+ * call unlinks this SHEntry from any other SHEntries for its document.
+ */
+ void abandonBFCacheEntry();
+
+ /**
+ * Does this SHEntry correspond to the same document as aEntry? This is
+ * true iff the two SHEntries have the same BFCacheEntry. So in particular,
+ * sharesDocumentWith(aEntry) is guaranteed to return true if it's
+ * preceded by a call to adoptBFCacheEntry(aEntry).
+ */
+ boolean sharesDocumentWith(in nsISHEntry aEntry);
+
+ /**
+ * Sets an SHEntry to reflect that it is a history type load. This is the
+ * equivalent to doing
+ *
+ * shEntry.loadType = 4;
+ *
+ * in js, but is easier to maintain and less opaque.
+ */
+ void setLoadTypeAsHistory();
+
+ /**
+ * Add a new child SHEntry. If offset is -1 adds to the end of the list.
+ */
+ void AddChild(in nsISHEntry aChild, in long aOffset,
+ [optional,default(false)] in bool aUseRemoteSubframes);
+
+ /**
+ * Remove a child SHEntry.
+ */
+ [noscript] void RemoveChild(in nsISHEntry aChild);
+
+ /**
+ * Get child at an index.
+ */
+ nsISHEntry GetChildAt(in long aIndex);
+
+ /**
+ * If this entry has no dynamically added child, get the child SHEntry
+ * at the given offset. The loadtype of the returned entry is set
+ * to its parent's loadtype.
+ */
+ [notxpcom] void GetChildSHEntryIfHasNoDynamicallyAddedChild(in long aChildOffset,
+ out nsISHEntry aChild);
+
+ /**
+ * Replaces a child which is for the same docshell as aNewChild
+ * with aNewChild.
+ * @throw if nothing was replaced.
+ */
+ [noscript] void ReplaceChild(in nsISHEntry aNewChild);
+
+ /**
+ * Remove all children of this entry and call abandonBFCacheEntry.
+ */
+ [notxpcom] void ClearEntry();
+
+ /**
+ * Create nsDocShellLoadState and fill it with information.
+ * Don't set nsSHEntry here to avoid serializing it.
+ */
+ [noscript] nsDocShellLoadStatePtr CreateLoadInfo();
+
+ [infallible] readonly attribute unsigned long long bfcacheID;
+
+ /**
+ * Sync up the docshell and session history trees for subframe navigation.
+ *
+ * @param aEntry new entry
+ * @param aTopBC top BC corresponding to the root ancestor
+ of the docshell that called this method
+ * @param aIgnoreBC current BC
+ */
+ [notxpcom] void SyncTreesForSubframeNavigation(in nsISHEntry aEntry,
+ in BrowsingContext aTopBC,
+ in BrowsingContext aIgnoreBC);
+
+ /**
+ * If browser.history.collectWireframes is true, this will get populated
+ * with a Wireframe upon document navigation / pushState. This will only
+ * be set for nsISHEntry's accessed in the parent process with
+ * sessionHistoryInParent enabled. See Document.webidl for more details on
+ * what a Wireframe is.
+ */
+ [implicit_jscontext] attribute jsval wireframe;
+};
diff --git a/docshell/shistory/nsISHistory.idl b/docshell/shistory/nsISHistory.idl
new file mode 100644
index 0000000000..9ca237c670
--- /dev/null
+++ b/docshell/shistory/nsISHistory.idl
@@ -0,0 +1,291 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIBFCacheEntry;
+interface nsISHEntry;
+interface nsISHistoryListener;
+interface nsIURI;
+webidl BrowsingContext;
+
+%{C++
+#include "nsTArrayForwardDeclare.h"
+#include "mozilla/Maybe.h"
+struct EntriesAndBrowsingContextData;
+namespace mozilla {
+namespace dom {
+class SHEntrySharedParentState;
+} // namespace dom
+} // namespace mozilla
+%}
+
+[ref] native nsDocshellIDArray(nsTArray<nsID>);
+native MaybeInt32(mozilla::Maybe<int32_t>);
+[ptr] native SHEntrySharedParentStatePtr(mozilla::dom::SHEntrySharedParentState);
+/**
+ * An interface to the primary properties of the Session History
+ * component. In an embedded browser environment, the nsIWebBrowser
+ * object creates an instance of session history for each open window.
+ * A handle to the session history object can be obtained from
+ * nsIWebNavigation. In a non-embedded situation, the owner of the
+ * session history component must create a instance of it and set
+ * it in the nsIWebNavigation object.
+ * This interface is accessible from javascript.
+ */
+
+[builtinclass, scriptable, uuid(7b807041-e60a-4384-935f-af3061d8b815)]
+interface nsISHistory: nsISupports
+{
+ /**
+ * A readonly property of the interface that returns
+ * the number of toplevel documents currently available
+ * in session history.
+ */
+ [infallible] readonly attribute long count;
+
+ /**
+ * The index of the current document in session history. Not infallible
+ * because setting can fail if the assigned value is out of range.
+ */
+ attribute long index;
+
+ /**
+ * A readonly property of the interface that returns
+ * the index of the last document that started to load and
+ * didn't finished yet. When document finishes the loading
+ * value -1 is returned.
+ */
+ [infallible] readonly attribute long requestedIndex;
+
+ /**
+ * Artifically set the |requestedIndex| for this nsISHEntry to the given
+ * index. This is used when resuming a cross-process load from a different
+ * process.
+ */
+ [noscript, notxpcom]
+ void internalSetRequestedIndex(in long aRequestedIndex);
+
+ /**
+ * Get the history entry at a given index. Returns non-null on success.
+ *
+ * @param index The index value whose entry is requested.
+ * The oldest entry is located at index == 0.
+ * @return The found entry; never null.
+ */
+ nsISHEntry getEntryAtIndex(in long aIndex);
+
+ /**
+ * Called to purge older documents from history.
+ * Documents can be removed from session history for various
+ * reasons. For example to control memory usage of the browser, to
+ * prevent users from loading documents from history, to erase evidence of
+ * prior page loads etc...
+ *
+ * @param numEntries The number of toplevel documents to be
+ * purged from history. During purge operation,
+ * the latest documents are maintained and older
+ * 'numEntries' documents are removed from history.
+ * @throws <code>NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA</code>
+ * Purge was vetod.
+ * @throws <code>NS_ERROR_FAILURE</code> numEntries is
+ * invalid or out of bounds with the size of history.
+ */
+ void purgeHistory(in long aNumEntries);
+
+ /**
+ * Called to register a listener for the session history component.
+ * Listeners are notified when pages are loaded or purged from history.
+ *
+ * @param aListener Listener object to be notified for all
+ * page loads that initiate in session history.
+ *
+ * @note A listener object must implement
+ * nsISHistoryListener and nsSupportsWeakReference
+ *
+ * @see nsISHistoryListener
+ * @see nsSupportsWeakReference
+ */
+ void addSHistoryListener(in nsISHistoryListener aListener);
+
+ /**
+ * Called to remove a listener for the session history component.
+ * Listeners are notified when pages are loaded from history.
+ *
+ * @param aListener Listener object to be removed from
+ * session history.
+ *
+ * @note A listener object must implement
+ * nsISHistoryListener and nsSupportsWeakReference
+ * @see nsISHistoryListener
+ * @see nsSupportsWeakReference
+ */
+ void removeSHistoryListener(in nsISHistoryListener aListener);
+
+ void reloadCurrentEntry();
+
+ /**
+ * Load the entry at the particular index.
+ */
+ [noscript]
+ void gotoIndex(in long aIndex, in boolean aUserActivation);
+
+ /**
+ * If an element exists at the particular index and
+ * whether it has user interaction.
+ */
+ [noscript,notxpcom]
+ boolean hasUserInteractionAtIndex(in long aIndex);
+
+ /**
+ * Called to obtain the index to a given history entry.
+ *
+ * @param aEntry The entry to obtain the index of.
+ *
+ * @return <code>NS_OK</code> index for the history entry
+ * is obtained successfully.
+ * <code>NS_ERROR_FAILURE</code> Error in obtaining
+ * index for the given history entry.
+ */
+ [noscript, notxpcom]
+ long getIndexOfEntry(in nsISHEntry aEntry);
+
+ /**
+ * Add a new Entry to the History List.
+ *
+ * @param aEntry The entry to add.
+ * @param aPersist If true this specifies that the entry should
+ * persist in the list. If false, this means that
+ * when new entries are added this element will not
+ * appear in the session history list.
+ */
+ void addEntry(in nsISHEntry aEntry, in boolean aPersist);
+
+ /**
+ * Update the index maintained by sessionHistory
+ */
+ void updateIndex();
+
+ /**
+ * Replace the nsISHEntry at a particular index
+ *
+ * @param aIndex The index at which the entry should be replaced.
+ * @param aReplaceEntry The replacement entry for the index.
+ */
+ void replaceEntry(in long aIndex, in nsISHEntry aReplaceEntry);
+
+ /**
+ * Notifies all registered session history listeners about an impending
+ * reload.
+ *
+ * @return Whether the operation can proceed.
+ */
+ boolean notifyOnHistoryReload();
+
+ /**
+ * Evict content viewers which don't lie in the "safe" range around aIndex.
+ * In practice, this should leave us with no more than gHistoryMaxViewers
+ * viewers associated with this SHistory object.
+ *
+ * Also make sure that the total number of content viewers in all windows is
+ * not greater than our global max; if it is, evict viewers as appropriate.
+ *
+ * @param aIndex The index around which the "safe" range is
+ * centered. In general, if you just navigated the
+ * history, aIndex should be the index history was
+ * navigated to.
+ */
+ void evictOutOfRangeDocumentViewers(in long aIndex);
+
+ /**
+ * Evict the content viewer associated with a bfcache entry that has timed
+ * out.
+ */
+ [noscript, notxpcom]
+ void evictExpiredDocumentViewerForEntry(in SHEntrySharedParentStatePtr aEntry);
+
+ /**
+ * Evict all the content viewers in this session history
+ */
+ void evictAllDocumentViewers();
+
+ /**
+ * Add a BFCache entry to expiration tracker so it gets evicted on
+ * expiration.
+ */
+ [noscript, notxpcom]
+ void addToExpirationTracker(in SHEntrySharedParentStatePtr aEntry);
+
+ /**
+ * Remove a BFCache entry from expiration tracker.
+ */
+ [noscript, notxpcom]
+ void removeFromExpirationTracker(in SHEntrySharedParentStatePtr aEntry);
+
+ /**
+ * Remove dynamic entries found at given index.
+ *
+ * @param aIndex Index to remove dynamic entries from. It will be
+ * passed to RemoveEntries as aStartIndex.
+ * @param aEntry (optional) The entry to start looking in for dynamic
+ * entries. Only the dynamic descendants of the
+ * entry will be removed. If not given, all dynamic
+ * entries at the index will be removed.
+ */
+ [noscript, notxpcom]
+ void RemoveDynEntries(in long aIndex, in nsISHEntry aEntry);
+
+ /**
+ * Similar to RemoveDynEntries, but instead of specifying an index, use the
+ * given BFCacheEntry to find the index and remove dynamic entries from the
+ * index.
+ *
+ * The method takes no effect if the bfcache entry is not or no longer hold
+ * by the SHistory instance.
+ *
+ * @param aEntry The bfcache entry to look up for index to remove
+ * dynamic entries from.
+ */
+ [noscript, notxpcom]
+ void RemoveDynEntriesForBFCacheEntry(in nsIBFCacheEntry aEntry);
+
+ /**
+ * Removes entries from the history if their docshellID is in
+ * aIDs array.
+ */
+ [noscript, notxpcom]
+ void RemoveEntries(in nsDocshellIDArray aIDs, in long aStartIndex);
+
+ /**
+ * Collect docshellIDs from aEntry's children and remove those
+ * entries from history.
+ *
+ * @param aEntry Children docshellID's will be collected from
+ * this entry and passed to RemoveEntries as aIDs.
+ */
+ [noscript, notxpcom]
+ void RemoveFrameEntries(in nsISHEntry aEntry);
+
+ [noscript]
+ void Reload(in unsigned long aReloadFlags);
+
+ [notxpcom] void EnsureCorrectEntryAtCurrIndex(in nsISHEntry aEntry);
+
+ [notxpcom] void EvictDocumentViewersOrReplaceEntry(in nsISHEntry aNewSHEntry, in bool aReplace);
+
+ nsISHEntry createEntry();
+
+ [noscript] void AddToRootSessionHistory(in bool aCloneChildren, in nsISHEntry aOSHE,
+ in BrowsingContext aRootBC, in nsISHEntry aEntry,
+ in unsigned long aLoadType,
+ in bool aShouldPersist,
+ out MaybeInt32 aPreviousEntryIndex,
+ out MaybeInt32 aLoadedEntryIndex);
+
+ [noscript] void AddChildSHEntryHelper(in nsISHEntry aCloneRef, in nsISHEntry aNewEntry,
+ in BrowsingContext aRootBC, in bool aCloneChildren);
+
+ [noscript, notxpcom] boolean isEmptyOrHasEntriesForSingleTopLevelPage();
+};
diff --git a/docshell/shistory/nsISHistoryListener.idl b/docshell/shistory/nsISHistoryListener.idl
new file mode 100644
index 0000000000..81fc444009
--- /dev/null
+++ b/docshell/shistory/nsISHistoryListener.idl
@@ -0,0 +1,88 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * nsISHistoryListener defines the interface one can implement to receive
+ * notifications about activities in session history and (for reloads) to be
+ * able to cancel them.
+ *
+ * A session history listener will be notified when pages are added, removed
+ * and loaded from session history. In the case of reloads, it can prevent them
+ * from happening by returning false from the corresponding callback method.
+ *
+ * A session history listener can be registered on a particular nsISHistory
+ * instance via the nsISHistory::addSHistoryListener() method.
+ *
+ * Listener methods should not alter the session history. Things are likely to
+ * go haywire if they do.
+ */
+[scriptable, uuid(125c0833-746a-400e-9b89-d2d18545c08a)]
+interface nsISHistoryListener : nsISupports
+{
+ /**
+ * Called when a new document is added to session history. New documents are
+ * added to session history by docshell when new pages are loaded in a frame
+ * or content area, for example via nsIWebNavigation::loadURI()
+ *
+ * @param aNewURI The URI of the document to be added to session history.
+ * @param aOldIndex The index of the current history item before the
+ * operation.
+ */
+ void OnHistoryNewEntry(in nsIURI aNewURI, in long aOldIndex);
+
+ /**
+ * Called before the current document is reloaded, for example due to a
+ * nsIWebNavigation::reload() call.
+ */
+ boolean OnHistoryReload();
+
+ /**
+ * Called before navigating to a session history entry by index, for example,
+ * when nsIWebNavigation::gotoIndex() is called.
+ */
+ void OnHistoryGotoIndex();
+
+ /**
+ * Called before entries are removed from the start of session history.
+ * Entries can be removed from session history for various reasons, for
+ * example to control the memory usage of the browser, to prevent users from
+ * loading documents from history, to erase evidence of prior page loads, etc.
+ *
+ * To purge documents from session history call nsISHistory::PurgeHistory().
+ *
+ * @param aNumEntries The number of entries being removed.
+ */
+ void OnHistoryPurge(in long aNumEntries);
+
+ /**
+ * Called before entries are removed from the end of session history. This
+ * occurs when navigating to a new page while on a previous session entry.
+ *
+ * @param aNumEntries The number of entries being removed.
+ */
+ void OnHistoryTruncate(in long aNumEntries);
+
+ /**
+ * Called before an entry is replaced in the session history. Entries are
+ * replaced when navigating away from non-persistent history entries (such as
+ * about pages) and when history.replaceState is called.
+ */
+ void OnHistoryReplaceEntry();
+
+
+ /**
+ * Called whenever a content viewer is evicted. A content viewer is evicted
+ * whenever a bfcache entry has timed out or the number of total content
+ * viewers has exceeded the global max. This is used for testing only.
+ *
+ * @param aNumEvicted - number of content viewers evicted
+ */
+ void OnDocumentViewerEvicted(in unsigned long aNumEvicted);
+};
diff --git a/docshell/shistory/nsSHEntry.cpp b/docshell/shistory/nsSHEntry.cpp
new file mode 100644
index 0000000000..8fc4f74844
--- /dev/null
+++ b/docshell/shistory/nsSHEntry.cpp
@@ -0,0 +1,1131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSHEntry.h"
+
+#include <algorithm>
+
+#include "nsDocShell.h"
+#include "nsDocShellEditorData.h"
+#include "nsDocShellLoadState.h"
+#include "nsDocShellLoadTypes.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIDocumentViewer.h"
+#include "nsIInputStream.h"
+#include "nsILayoutHistoryState.h"
+#include "nsIMutableArray.h"
+#include "nsIStructuredCloneContainer.h"
+#include "nsIURI.h"
+#include "nsSHEntryShared.h"
+#include "nsSHistory.h"
+
+#include "mozilla/Logging.h"
+#include "nsIReferrerInfo.h"
+
+extern mozilla::LazyLogModule gPageCacheLog;
+
+static uint32_t gEntryID = 0;
+
+nsSHEntry::nsSHEntry()
+ : mShared(new nsSHEntryShared()),
+ mLoadType(0),
+ mID(++gEntryID), // SessionStore has special handling for 0 values.
+ mScrollPositionX(0),
+ mScrollPositionY(0),
+ mLoadReplace(false),
+ mURIWasModified(false),
+ mIsSrcdocEntry(false),
+ mScrollRestorationIsManual(false),
+ mLoadedInThisProcess(false),
+ mPersist(true),
+ mHasUserInteraction(false),
+ mHasUserActivation(false) {}
+
+nsSHEntry::nsSHEntry(const nsSHEntry& aOther)
+ : mShared(aOther.mShared),
+ mURI(aOther.mURI),
+ mOriginalURI(aOther.mOriginalURI),
+ mResultPrincipalURI(aOther.mResultPrincipalURI),
+ mUnstrippedURI(aOther.mUnstrippedURI),
+ mReferrerInfo(aOther.mReferrerInfo),
+ mTitle(aOther.mTitle),
+ mPostData(aOther.mPostData),
+ mLoadType(0), // XXX why not copy?
+ mID(aOther.mID),
+ mScrollPositionX(0), // XXX why not copy?
+ mScrollPositionY(0), // XXX why not copy?
+ mParent(aOther.mParent),
+ mStateData(aOther.mStateData),
+ mSrcdocData(aOther.mSrcdocData),
+ mBaseURI(aOther.mBaseURI),
+ mLoadReplace(aOther.mLoadReplace),
+ mURIWasModified(aOther.mURIWasModified),
+ mIsSrcdocEntry(aOther.mIsSrcdocEntry),
+ mScrollRestorationIsManual(false),
+ mLoadedInThisProcess(aOther.mLoadedInThisProcess),
+ mPersist(aOther.mPersist),
+ mHasUserInteraction(false),
+ mHasUserActivation(aOther.mHasUserActivation) {}
+
+nsSHEntry::~nsSHEntry() {
+ // Null out the mParent pointers on all our kids.
+ for (nsISHEntry* entry : mChildren) {
+ if (entry) {
+ entry->SetParent(nullptr);
+ }
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsSHEntry, nsISHEntry, nsISupportsWeakReference)
+
+NS_IMETHODIMP
+nsSHEntry::SetScrollPosition(int32_t aX, int32_t aY) {
+ mScrollPositionX = aX;
+ mScrollPositionY = aY;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetScrollPosition(int32_t* aX, int32_t* aY) {
+ *aX = mScrollPositionX;
+ *aY = mScrollPositionY;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetURIWasModified(bool* aOut) {
+ *aOut = mURIWasModified;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetURIWasModified(bool aIn) {
+ mURIWasModified = aIn;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetURI(nsIURI** aURI) {
+ *aURI = mURI;
+ NS_IF_ADDREF(*aURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetURI(nsIURI* aURI) {
+ mURI = aURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetOriginalURI(nsIURI** aOriginalURI) {
+ *aOriginalURI = mOriginalURI;
+ NS_IF_ADDREF(*aOriginalURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetOriginalURI(nsIURI* aOriginalURI) {
+ mOriginalURI = aOriginalURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetResultPrincipalURI(nsIURI** aResultPrincipalURI) {
+ *aResultPrincipalURI = mResultPrincipalURI;
+ NS_IF_ADDREF(*aResultPrincipalURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetResultPrincipalURI(nsIURI* aResultPrincipalURI) {
+ mResultPrincipalURI = aResultPrincipalURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetUnstrippedURI(nsIURI** aUnstrippedURI) {
+ *aUnstrippedURI = mUnstrippedURI;
+ NS_IF_ADDREF(*aUnstrippedURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetUnstrippedURI(nsIURI* aUnstrippedURI) {
+ mUnstrippedURI = aUnstrippedURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetLoadReplace(bool* aLoadReplace) {
+ *aLoadReplace = mLoadReplace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetLoadReplace(bool aLoadReplace) {
+ mLoadReplace = aLoadReplace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) {
+ *aReferrerInfo = mReferrerInfo;
+ NS_IF_ADDREF(*aReferrerInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
+ mReferrerInfo = aReferrerInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetSticky(bool aSticky) {
+ mShared->mSticky = aSticky;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetSticky(bool* aSticky) {
+ *aSticky = mShared->mSticky;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetTitle(nsAString& aTitle) {
+ // Check for empty title...
+ if (mTitle.IsEmpty() && mURI) {
+ // Default title is the URL.
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(mURI->GetSpec(spec))) {
+ AppendUTF8toUTF16(spec, mTitle);
+ }
+ }
+
+ aTitle = mTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetTitle(const nsAString& aTitle) {
+ mTitle = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetName(nsAString& aName) {
+ aName = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetName(const nsAString& aName) {
+ mName = aName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetPostData(nsIInputStream** aResult) {
+ *aResult = mPostData;
+ NS_IF_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetPostData(nsIInputStream* aPostData) {
+ mPostData = aPostData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetHasPostData(bool* aResult) {
+ *aResult = !!mPostData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetLayoutHistoryState(nsILayoutHistoryState** aResult) {
+ *aResult = mShared->mLayoutHistoryState;
+ NS_IF_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetLayoutHistoryState(nsILayoutHistoryState* aState) {
+ mShared->mLayoutHistoryState = aState;
+ if (mShared->mLayoutHistoryState) {
+ mShared->mLayoutHistoryState->SetScrollPositionOnly(
+ !mShared->mSaveLayoutState);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::InitLayoutHistoryState(nsILayoutHistoryState** aState) {
+ if (!mShared->mLayoutHistoryState) {
+ nsCOMPtr<nsILayoutHistoryState> historyState;
+ historyState = NS_NewLayoutHistoryState();
+ SetLayoutHistoryState(historyState);
+ }
+
+ nsCOMPtr<nsILayoutHistoryState> state = GetLayoutHistoryState();
+ state.forget(aState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetLoadType(uint32_t* aResult) {
+ *aResult = mLoadType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetLoadType(uint32_t aLoadType) {
+ mLoadType = aLoadType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetID(uint32_t* aResult) {
+ *aResult = mID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetID(uint32_t aID) {
+ mID = aID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetIsSubFrame(bool* aFlag) {
+ *aFlag = mShared->mIsFrameNavigation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetIsSubFrame(bool aFlag) {
+ mShared->mIsFrameNavigation = aFlag;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetHasUserInteraction(bool* aFlag) {
+ // The back button and menulist deal with root/top-level
+ // session history entries, thus we annotate only the root entry.
+ if (!mParent) {
+ *aFlag = mHasUserInteraction;
+ } else {
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this);
+ root->GetHasUserInteraction(aFlag);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetHasUserInteraction(bool aFlag) {
+ // The back button and menulist deal with root/top-level
+ // session history entries, thus we annotate only the root entry.
+ if (!mParent) {
+ mHasUserInteraction = aFlag;
+ } else {
+ nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this);
+ root->SetHasUserInteraction(aFlag);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetHasUserActivation(bool* aFlag) {
+ *aFlag = mHasUserActivation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetHasUserActivation(bool aFlag) {
+ mHasUserActivation = aFlag;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetCacheKey(uint32_t* aResult) {
+ *aResult = mShared->mCacheKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetCacheKey(uint32_t aCacheKey) {
+ mShared->mCacheKey = aCacheKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetContentType(nsACString& aContentType) {
+ aContentType = mShared->mContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetContentType(const nsACString& aContentType) {
+ mShared->mContentType = aContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::Create(
+ nsIURI* aURI, const nsAString& aTitle, nsIInputStream* aInputStream,
+ uint32_t aCacheKey, const nsACString& aContentType,
+ nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, const nsID& aDocShellID,
+ bool aDynamicCreation, nsIURI* aOriginalURI, nsIURI* aResultPrincipalURI,
+ nsIURI* aUnstrippedURI, bool aLoadReplace, nsIReferrerInfo* aReferrerInfo,
+ const nsAString& aSrcdocData, bool aSrcdocEntry, nsIURI* aBaseURI,
+ bool aSaveLayoutState, bool aExpired, bool aUserActivation) {
+ MOZ_ASSERT(
+ aTriggeringPrincipal,
+ "need a valid triggeringPrincipal to create a session history entry");
+
+ mURI = aURI;
+ mTitle = aTitle;
+ mPostData = aInputStream;
+
+ // Set the LoadType by default to loadHistory during creation
+ mLoadType = LOAD_HISTORY;
+
+ mShared->mCacheKey = aCacheKey;
+ mShared->mContentType = aContentType;
+ mShared->mTriggeringPrincipal = aTriggeringPrincipal;
+ mShared->mPrincipalToInherit = aPrincipalToInherit;
+ mShared->mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit;
+ mShared->mCsp = aCsp;
+ mShared->mDocShellID = aDocShellID;
+ mShared->mDynamicallyCreated = aDynamicCreation;
+
+ // By default all entries are set false for subframe flag.
+ // nsDocShell::CloneAndReplace() which creates entries for
+ // all subframe navigations, sets the flag to true.
+ mShared->mIsFrameNavigation = false;
+
+ mHasUserInteraction = false;
+
+ mShared->mExpired = aExpired;
+
+ mIsSrcdocEntry = aSrcdocEntry;
+ mSrcdocData = aSrcdocData;
+
+ mBaseURI = aBaseURI;
+
+ mLoadedInThisProcess = true;
+
+ mOriginalURI = aOriginalURI;
+ mResultPrincipalURI = aResultPrincipalURI;
+ mUnstrippedURI = aUnstrippedURI;
+ mLoadReplace = aLoadReplace;
+ mReferrerInfo = aReferrerInfo;
+
+ mHasUserActivation = aUserActivation;
+
+ mShared->mLayoutHistoryState = nullptr;
+
+ mShared->mSaveLayoutState = aSaveLayoutState;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetParent(nsISHEntry** aResult) {
+ nsCOMPtr<nsISHEntry> parent = do_QueryReferent(mParent);
+ parent.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetParent(nsISHEntry* aParent) {
+ mParent = do_GetWeakReference(aParent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHEntry::SetViewerBounds(const nsIntRect& aBounds) {
+ mShared->mViewerBounds = aBounds;
+}
+
+NS_IMETHODIMP_(void)
+nsSHEntry::GetViewerBounds(nsIntRect& aBounds) {
+ aBounds = mShared->mViewerBounds;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetTriggeringPrincipal(nsIPrincipal** aTriggeringPrincipal) {
+ NS_IF_ADDREF(*aTriggeringPrincipal = mShared->mTriggeringPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetTriggeringPrincipal(nsIPrincipal* aTriggeringPrincipal) {
+ mShared->mTriggeringPrincipal = aTriggeringPrincipal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetPrincipalToInherit(nsIPrincipal** aPrincipalToInherit) {
+ NS_IF_ADDREF(*aPrincipalToInherit = mShared->mPrincipalToInherit);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetPrincipalToInherit(nsIPrincipal* aPrincipalToInherit) {
+ mShared->mPrincipalToInherit = aPrincipalToInherit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetPartitionedPrincipalToInherit(
+ nsIPrincipal** aPartitionedPrincipalToInherit) {
+ NS_IF_ADDREF(*aPartitionedPrincipalToInherit =
+ mShared->mPartitionedPrincipalToInherit);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetPartitionedPrincipalToInherit(
+ nsIPrincipal* aPartitionedPrincipalToInherit) {
+ mShared->mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetCsp(nsIContentSecurityPolicy** aCsp) {
+ NS_IF_ADDREF(*aCsp = mShared->mCsp);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetCsp(nsIContentSecurityPolicy* aCsp) {
+ mShared->mCsp = aCsp;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::AdoptBFCacheEntry(nsISHEntry* aEntry) {
+ nsSHEntryShared* shared = static_cast<nsSHEntry*>(aEntry)->mShared;
+ NS_ENSURE_STATE(shared);
+
+ mShared = shared;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SharesDocumentWith(nsISHEntry* aEntry, bool* aOut) {
+ NS_ENSURE_ARG_POINTER(aOut);
+
+ *aOut = mShared == static_cast<nsSHEntry*>(aEntry)->mShared;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetIsSrcdocEntry(bool* aIsSrcdocEntry) {
+ *aIsSrcdocEntry = mIsSrcdocEntry;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetSrcdocData(nsAString& aSrcdocData) {
+ aSrcdocData = mSrcdocData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetSrcdocData(const nsAString& aSrcdocData) {
+ mSrcdocData = aSrcdocData;
+ mIsSrcdocEntry = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetBaseURI(nsIURI** aBaseURI) {
+ *aBaseURI = mBaseURI;
+ NS_IF_ADDREF(*aBaseURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetBaseURI(nsIURI* aBaseURI) {
+ mBaseURI = aBaseURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetScrollRestorationIsManual(bool* aIsManual) {
+ *aIsManual = mScrollRestorationIsManual;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetScrollRestorationIsManual(bool aIsManual) {
+ mScrollRestorationIsManual = aIsManual;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetLoadedInThisProcess(bool* aLoadedInThisProcess) {
+ *aLoadedInThisProcess = mLoadedInThisProcess;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetChildCount(int32_t* aCount) {
+ *aCount = mChildren.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::AddChild(nsISHEntry* aChild, int32_t aOffset,
+ bool aUseRemoteSubframes) {
+ if (aChild) {
+ NS_ENSURE_SUCCESS(aChild->SetParent(this), NS_ERROR_FAILURE);
+ }
+
+ if (aOffset < 0) {
+ mChildren.AppendObject(aChild);
+ return NS_OK;
+ }
+
+ //
+ // Bug 52670: Ensure children are added in order.
+ //
+ // Later frames in the child list may load faster and get appended
+ // before earlier frames, causing session history to be scrambled.
+ // By growing the list here, they are added to the right position.
+ //
+ // Assert that aOffset will not be so high as to grow us a lot.
+ //
+ NS_ASSERTION(aOffset < (mChildren.Count() + 1023), "Large frames array!\n");
+
+ bool newChildIsDyn = aChild ? aChild->IsDynamicallyAdded() : false;
+
+ // If the new child is dynamically added, try to add it to aOffset, but if
+ // there are non-dynamically added children, the child must be after those.
+ if (newChildIsDyn) {
+ int32_t lastNonDyn = aOffset - 1;
+ for (int32_t i = aOffset; i < mChildren.Count(); ++i) {
+ nsISHEntry* entry = mChildren[i];
+ if (entry) {
+ if (entry->IsDynamicallyAdded()) {
+ break;
+ } else {
+ lastNonDyn = i;
+ }
+ }
+ }
+ // InsertObjectAt allows only appending one object.
+ // If aOffset is larger than Count(), we must first manually
+ // set the capacity.
+ if (aOffset > mChildren.Count()) {
+ mChildren.SetCount(aOffset);
+ }
+ if (!mChildren.InsertObjectAt(aChild, lastNonDyn + 1)) {
+ NS_WARNING("Adding a child failed!");
+ aChild->SetParent(nullptr);
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ // If the new child isn't dynamically added, it should be set to aOffset.
+ // If there are dynamically added children before that, those must be
+ // moved to be after aOffset.
+ if (mChildren.Count() > 0) {
+ int32_t start = std::min(mChildren.Count() - 1, aOffset);
+ int32_t dynEntryIndex = -1;
+ nsISHEntry* dynEntry = nullptr;
+ for (int32_t i = start; i >= 0; --i) {
+ nsISHEntry* entry = mChildren[i];
+ if (entry) {
+ if (entry->IsDynamicallyAdded()) {
+ dynEntryIndex = i;
+ dynEntry = entry;
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (dynEntry) {
+ nsCOMArray<nsISHEntry> tmp;
+ tmp.SetCount(aOffset - dynEntryIndex + 1);
+ mChildren.InsertObjectsAt(tmp, dynEntryIndex);
+ NS_ASSERTION(mChildren[aOffset + 1] == dynEntry, "Whaat?");
+ }
+ }
+
+ // Make sure there isn't anything at aOffset.
+ if (aOffset < mChildren.Count()) {
+ nsISHEntry* oldChild = mChildren[aOffset];
+ if (oldChild && oldChild != aChild) {
+ // Under Fission, this can happen when a network-created iframe starts
+ // out in-process, moves out-of-process, and then switches back. At that
+ // point, we'll create a new network-created DocShell at the same index
+ // where we already have an entry for the original network-created
+ // DocShell.
+ //
+ // This should ideally stop being an issue once the Fission-aware
+ // session history rewrite is complete.
+ NS_ASSERTION(
+ aUseRemoteSubframes,
+ "Adding a child where we already have a child? This may misbehave");
+ oldChild->SetParent(nullptr);
+ }
+ }
+
+ mChildren.ReplaceObjectAt(aChild, aOffset);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::RemoveChild(nsISHEntry* aChild) {
+ NS_ENSURE_TRUE(aChild, NS_ERROR_FAILURE);
+ bool childRemoved = false;
+ if (aChild->IsDynamicallyAdded()) {
+ childRemoved = mChildren.RemoveObject(aChild);
+ } else {
+ int32_t index = mChildren.IndexOfObject(aChild);
+ if (index >= 0) {
+ // Other alive non-dynamic child docshells still keep mChildOffset,
+ // so we don't want to change the indices here.
+ mChildren.ReplaceObjectAt(nullptr, index);
+ childRemoved = true;
+ }
+ }
+ if (childRemoved) {
+ aChild->SetParent(nullptr);
+
+ // reduce the child count, i.e. remove empty children at the end
+ for (int32_t i = mChildren.Count() - 1; i >= 0 && !mChildren[i]; --i) {
+ if (!mChildren.RemoveObjectAt(i)) {
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetChildAt(int32_t aIndex, nsISHEntry** aResult) {
+ if (aIndex >= 0 && aIndex < mChildren.Count()) {
+ *aResult = mChildren[aIndex];
+ // yes, mChildren can have holes in it. AddChild's offset parameter makes
+ // that possible.
+ NS_IF_ADDREF(*aResult);
+ } else {
+ *aResult = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHEntry::GetChildSHEntryIfHasNoDynamicallyAddedChild(int32_t aChildOffset,
+ nsISHEntry** aChild) {
+ *aChild = nullptr;
+
+ bool dynamicallyAddedChild = false;
+ HasDynamicallyAddedChild(&dynamicallyAddedChild);
+ if (dynamicallyAddedChild) {
+ return;
+ }
+
+ // If the user did a shift-reload on this frameset page,
+ // we don't want to load the subframes from history.
+ if (IsForceReloadType(mLoadType) || mLoadType == LOAD_REFRESH) {
+ return;
+ }
+
+ /* Before looking for the subframe's url, check
+ * the expiration status of the parent. If the parent
+ * has expired from cache, then subframes will not be
+ * loaded from history in certain situations.
+ * If the user pressed reload and the parent frame has expired
+ * from cache, we do not want to load the child frame from history.
+ */
+ if (mShared->mExpired && (mLoadType == LOAD_RELOAD_NORMAL)) {
+ // The parent has expired. Return null.
+ *aChild = nullptr;
+ return;
+ }
+ // Get the child subframe from session history.
+ GetChildAt(aChildOffset, aChild);
+ if (*aChild) {
+ // Set the parent's Load Type on the child
+ (*aChild)->SetLoadType(mLoadType);
+ }
+}
+
+NS_IMETHODIMP
+nsSHEntry::ReplaceChild(nsISHEntry* aNewEntry) {
+ NS_ENSURE_STATE(aNewEntry);
+
+ nsID docshellID;
+ aNewEntry->GetDocshellID(docshellID);
+
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]) {
+ nsID childDocshellID;
+ nsresult rv = mChildren[i]->GetDocshellID(childDocshellID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (docshellID == childDocshellID) {
+ mChildren[i]->SetParent(nullptr);
+ mChildren.ReplaceObjectAt(aNewEntry, i);
+ return aNewEntry->SetParent(this);
+ }
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP_(void) nsSHEntry::ClearEntry() {
+ int32_t childCount = GetChildCount();
+ // Remove all children of this entry
+ for (int32_t i = childCount - 1; i >= 0; i--) {
+ nsCOMPtr<nsISHEntry> child;
+ GetChildAt(i, getter_AddRefs(child));
+ RemoveChild(child);
+ }
+ AbandonBFCacheEntry();
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetStateData(nsIStructuredCloneContainer** aContainer) {
+ NS_IF_ADDREF(*aContainer = mStateData);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetStateData(nsIStructuredCloneContainer* aContainer) {
+ mStateData = aContainer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(bool)
+nsSHEntry::IsDynamicallyAdded() { return mShared->mDynamicallyCreated; }
+
+NS_IMETHODIMP
+nsSHEntry::HasDynamicallyAddedChild(bool* aAdded) {
+ *aAdded = false;
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ nsISHEntry* entry = mChildren[i];
+ if (entry) {
+ *aAdded = entry->IsDynamicallyAdded();
+ if (*aAdded) {
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetDocshellID(nsID& aID) {
+ aID = mShared->mDocShellID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetDocshellID(const nsID& aID) {
+ mShared->mDocShellID = aID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetLastTouched(uint32_t* aLastTouched) {
+ *aLastTouched = mShared->mLastTouched;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetLastTouched(uint32_t aLastTouched) {
+ mShared->mLastTouched = aLastTouched;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetShistory(nsISHistory** aSHistory) {
+ nsCOMPtr<nsISHistory> shistory(do_QueryReferent(mShared->mSHistory));
+ shistory.forget(aSHistory);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetShistory(nsISHistory* aSHistory) {
+ nsWeakPtr shistory = do_GetWeakReference(aSHistory);
+ // mSHistory can not be changed once it's set
+ MOZ_ASSERT(!mShared->mSHistory || (mShared->mSHistory == shistory));
+ mShared->mSHistory = shistory;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetLoadTypeAsHistory() {
+ // Set the LoadType by default to loadHistory during creation
+ mLoadType = LOAD_HISTORY;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetPersist(bool* aPersist) {
+ *aPersist = mPersist;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetPersist(bool aPersist) {
+ mPersist = aPersist;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::CreateLoadInfo(nsDocShellLoadState** aLoadState) {
+ nsCOMPtr<nsIURI> uri = GetURI();
+ RefPtr<nsDocShellLoadState> loadState(new nsDocShellLoadState(uri));
+
+ nsCOMPtr<nsIURI> originalURI = GetOriginalURI();
+ loadState->SetOriginalURI(originalURI);
+
+ mozilla::Maybe<nsCOMPtr<nsIURI>> emplacedResultPrincipalURI;
+ nsCOMPtr<nsIURI> resultPrincipalURI = GetResultPrincipalURI();
+ emplacedResultPrincipalURI.emplace(std::move(resultPrincipalURI));
+ loadState->SetMaybeResultPrincipalURI(emplacedResultPrincipalURI);
+
+ nsCOMPtr<nsIURI> unstrippedURI = GetUnstrippedURI();
+ loadState->SetUnstrippedURI(unstrippedURI);
+
+ loadState->SetLoadReplace(GetLoadReplace());
+ nsCOMPtr<nsIInputStream> postData = GetPostData();
+ loadState->SetPostDataStream(postData);
+
+ nsAutoCString contentType;
+ GetContentType(contentType);
+ loadState->SetTypeHint(contentType);
+
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal = GetTriggeringPrincipal();
+ loadState->SetTriggeringPrincipal(triggeringPrincipal);
+ nsCOMPtr<nsIPrincipal> principalToInherit = GetPrincipalToInherit();
+ loadState->SetPrincipalToInherit(principalToInherit);
+ nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit =
+ GetPartitionedPrincipalToInherit();
+ loadState->SetPartitionedPrincipalToInherit(partitionedPrincipalToInherit);
+ nsCOMPtr<nsIContentSecurityPolicy> csp = GetCsp();
+ loadState->SetCsp(csp);
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = GetReferrerInfo();
+ loadState->SetReferrerInfo(referrerInfo);
+
+ // Do not inherit principal from document (security-critical!);
+ uint32_t flags = nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_NONE;
+
+ // Passing nullptr as aSourceDocShell gives the same behaviour as before
+ // aSourceDocShell was introduced. According to spec we should be passing
+ // the source browsing context that was used when the history entry was
+ // first created. bug 947716 has been created to address this issue.
+ nsAutoString srcdoc;
+ nsCOMPtr<nsIURI> baseURI;
+ if (GetIsSrcdocEntry()) {
+ GetSrcdocData(srcdoc);
+ baseURI = GetBaseURI();
+ flags |= nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_IS_SRCDOC;
+ } else {
+ srcdoc = VoidString();
+ }
+ loadState->SetSrcdocData(srcdoc);
+ loadState->SetBaseURI(baseURI);
+ loadState->SetInternalLoadFlags(flags);
+
+ loadState->SetFirstParty(true);
+
+ loadState->SetHasValidUserGestureActivation(GetHasUserActivation());
+
+ loadState->SetSHEntry(this);
+
+ // When we create a load state from the history entry we already know if
+ // https-first was able to upgrade the request from http to https. There is no
+ // point in re-retrying to upgrade.
+ loadState->SetIsExemptFromHTTPSFirstMode(true);
+
+ loadState.forget(aLoadState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHEntry::SyncTreesForSubframeNavigation(
+ nsISHEntry* aEntry, mozilla::dom::BrowsingContext* aTopBC,
+ mozilla::dom::BrowsingContext* aIgnoreBC) {
+ // XXX Keep this in sync with
+ // SessionHistoryEntry::SyncTreesForSubframeNavigation
+ //
+ // We need to sync up the browsing context and session history trees for
+ // subframe navigation. If the load was in a subframe, we forward up to
+ // the top browsing context, which will then recursively sync up all browsing
+ // contexts to their corresponding entries in the new session history tree. If
+ // we don't do this, then we can cache a content viewer on the wrong cloned
+ // entry, and subsequently restore it at the wrong time.
+ nsCOMPtr<nsISHEntry> newRootEntry = nsSHistory::GetRootSHEntry(aEntry);
+ if (newRootEntry) {
+ // newRootEntry is now the new root entry.
+ // Find the old root entry as well.
+
+ // Need a strong ref. on |oldRootEntry| so it isn't destroyed when
+ // SetChildHistoryEntry() does SwapHistoryEntries() (bug 304639).
+ nsCOMPtr<nsISHEntry> oldRootEntry = nsSHistory::GetRootSHEntry(this);
+
+ if (oldRootEntry) {
+ nsSHistory::SwapEntriesData data = {aIgnoreBC, newRootEntry, nullptr};
+ nsSHistory::SetChildHistoryEntry(oldRootEntry, aTopBC, 0, &data);
+ }
+ }
+}
+
+void nsSHEntry::EvictDocumentViewer() {
+ nsCOMPtr<nsIDocumentViewer> viewer = GetDocumentViewer();
+ if (viewer) {
+ mShared->NotifyListenersDocumentViewerEvicted();
+ // Drop the presentation state before destroying the viewer, so that
+ // document teardown is able to correctly persist the state.
+ SetDocumentViewer(nullptr);
+ SyncPresentationState();
+ viewer->Destroy();
+ }
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetDocumentViewer(nsIDocumentViewer* aViewer) {
+ return GetState()->SetDocumentViewer(aViewer);
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetDocumentViewer(nsIDocumentViewer** aResult) {
+ *aResult = GetState()->mDocumentViewer;
+ NS_IF_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetIsInBFCache(bool* aResult) {
+ *aResult = !!GetState()->mDocumentViewer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::Clone(nsISHEntry** aResult) {
+ nsCOMPtr<nsISHEntry> entry = new nsSHEntry(*this);
+ entry.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetSaveLayoutStateFlag(bool* aFlag) {
+ *aFlag = mShared->mSaveLayoutState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetSaveLayoutStateFlag(bool aFlag) {
+ mShared->mSaveLayoutState = aFlag;
+ if (mShared->mLayoutHistoryState) {
+ mShared->mLayoutHistoryState->SetScrollPositionOnly(!aFlag);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetWindowState(nsISupports* aState) {
+ GetState()->mWindowState = aState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetWindowState(nsISupports** aState) {
+ NS_IF_ADDREF(*aState = GetState()->mWindowState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetRefreshURIList(nsIMutableArray** aList) {
+ NS_IF_ADDREF(*aList = GetState()->mRefreshURIList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetRefreshURIList(nsIMutableArray* aList) {
+ GetState()->mRefreshURIList = aList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHEntry::AddChildShell(nsIDocShellTreeItem* aShell) {
+ MOZ_ASSERT(aShell, "Null child shell added to history entry");
+ GetState()->mChildShells.AppendObject(aShell);
+}
+
+NS_IMETHODIMP
+nsSHEntry::ChildShellAt(int32_t aIndex, nsIDocShellTreeItem** aShell) {
+ NS_IF_ADDREF(*aShell = GetState()->mChildShells.SafeObjectAt(aIndex));
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHEntry::ClearChildShells() { GetState()->mChildShells.Clear(); }
+
+NS_IMETHODIMP_(void)
+nsSHEntry::SyncPresentationState() { GetState()->SyncPresentationState(); }
+
+nsDocShellEditorData* nsSHEntry::ForgetEditorData() {
+ // XXX jlebar Check how this is used.
+ return GetState()->mEditorData.release();
+}
+
+void nsSHEntry::SetEditorData(nsDocShellEditorData* aData) {
+ NS_ASSERTION(!(aData && GetState()->mEditorData),
+ "We're going to overwrite an owning ref!");
+ if (GetState()->mEditorData != aData) {
+ GetState()->mEditorData = mozilla::WrapUnique(aData);
+ }
+}
+
+bool nsSHEntry::HasDetachedEditor() {
+ return GetState()->mEditorData != nullptr;
+}
+
+bool nsSHEntry::HasBFCacheEntry(
+ mozilla::dom::SHEntrySharedParentState* aEntry) {
+ return GetState() == aEntry;
+}
+
+NS_IMETHODIMP
+nsSHEntry::AbandonBFCacheEntry() {
+ mShared = GetState()->Duplicate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetBfcacheID(uint64_t* aBFCacheID) {
+ *aBFCacheID = mShared->GetId();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetWireframe(JSContext* aCx, JS::MutableHandle<JS::Value> aOut) {
+ aOut.set(JS::NullValue());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetWireframe(JSContext* aCx, JS::Handle<JS::Value> aArg) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/docshell/shistory/nsSHEntry.h b/docshell/shistory/nsSHEntry.h
new file mode 100644
index 0000000000..1a018575bd
--- /dev/null
+++ b/docshell/shistory/nsSHEntry.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSHEntry_h
+#define nsSHEntry_h
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsISHEntry.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "mozilla/Attributes.h"
+
+class nsSHEntryShared;
+class nsIInputStream;
+class nsIURI;
+class nsIReferrerInfo;
+
+class nsSHEntry : public nsISHEntry, public nsSupportsWeakReference {
+ public:
+ nsSHEntry();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHENTRY
+
+ virtual void EvictDocumentViewer();
+
+ static nsresult Startup();
+ static void Shutdown();
+
+ nsSHEntryShared* GetState() { return mShared; }
+
+ protected:
+ explicit nsSHEntry(const nsSHEntry& aOther);
+ virtual ~nsSHEntry();
+
+ // We share the state in here with other SHEntries which correspond to the
+ // same document.
+ RefPtr<nsSHEntryShared> mShared;
+
+ // See nsSHEntry.idl for comments on these members.
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIURI> mOriginalURI;
+ nsCOMPtr<nsIURI> mResultPrincipalURI;
+ nsCOMPtr<nsIURI> mUnstrippedURI;
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+ nsString mTitle;
+ nsString mName;
+ nsCOMPtr<nsIInputStream> mPostData;
+ uint32_t mLoadType;
+ uint32_t mID;
+ int32_t mScrollPositionX;
+ int32_t mScrollPositionY;
+ nsWeakPtr mParent;
+ nsCOMArray<nsISHEntry> mChildren;
+ nsCOMPtr<nsIStructuredCloneContainer> mStateData;
+ nsString mSrcdocData;
+ nsCOMPtr<nsIURI> mBaseURI;
+ bool mLoadReplace;
+ bool mURIWasModified;
+ bool mIsSrcdocEntry;
+ bool mScrollRestorationIsManual;
+ bool mLoadedInThisProcess;
+ bool mPersist;
+ bool mHasUserInteraction;
+ bool mHasUserActivation;
+};
+
+#endif /* nsSHEntry_h */
diff --git a/docshell/shistory/nsSHEntryShared.cpp b/docshell/shistory/nsSHEntryShared.cpp
new file mode 100644
index 0000000000..2c0e33ef62
--- /dev/null
+++ b/docshell/shistory/nsSHEntryShared.cpp
@@ -0,0 +1,365 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSHEntryShared.h"
+
+#include "nsArray.h"
+#include "nsContentUtils.h"
+#include "nsDocShellEditorData.h"
+#include "nsIDocumentViewer.h"
+#include "nsISHistory.h"
+#include "mozilla/dom/Document.h"
+#include "nsILayoutHistoryState.h"
+#include "nsIWebNavigation.h"
+#include "nsSHistory.h"
+#include "nsThreadUtils.h"
+#include "nsFrameLoader.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Preferences.h"
+
+namespace dom = mozilla::dom;
+
+namespace {
+uint64_t gSHEntrySharedID = 0;
+nsTHashMap<nsUint64HashKey, mozilla::dom::SHEntrySharedParentState*>*
+ sIdToSharedState = nullptr;
+} // namespace
+
+namespace mozilla {
+namespace dom {
+
+/* static */
+uint64_t SHEntrySharedState::GenerateId() {
+ return nsContentUtils::GenerateProcessSpecificId(++gSHEntrySharedID);
+}
+
+/* static */
+SHEntrySharedParentState* SHEntrySharedParentState::Lookup(uint64_t aId) {
+ MOZ_ASSERT(aId != 0);
+
+ return sIdToSharedState ? sIdToSharedState->Get(aId) : nullptr;
+}
+
+static void AddSHEntrySharedParentState(
+ SHEntrySharedParentState* aSharedState) {
+ MOZ_ASSERT(aSharedState->mId != 0);
+
+ if (!sIdToSharedState) {
+ sIdToSharedState =
+ new nsTHashMap<nsUint64HashKey, SHEntrySharedParentState*>();
+ }
+ sIdToSharedState->InsertOrUpdate(aSharedState->mId, aSharedState);
+}
+
+SHEntrySharedParentState::SHEntrySharedParentState() {
+ AddSHEntrySharedParentState(this);
+}
+
+SHEntrySharedParentState::SHEntrySharedParentState(
+ nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp, const nsACString& aContentType)
+ : SHEntrySharedState(aTriggeringPrincipal, aPrincipalToInherit,
+ aPartitionedPrincipalToInherit, aCsp, aContentType) {
+ AddSHEntrySharedParentState(this);
+}
+
+SHEntrySharedParentState::~SHEntrySharedParentState() {
+ MOZ_ASSERT(mId != 0);
+
+ RefPtr<nsFrameLoader> loader = mFrameLoader;
+ SetFrameLoader(nullptr);
+ if (loader) {
+ if (NS_FAILED(NS_DispatchToCurrentThread(NS_NewRunnableFunction(
+ "SHEntrySharedParentState::~SHEntrySharedParentState",
+ [loader]() -> void { loader->AsyncDestroy(); })))) {
+ // Trigger AsyncDestroy immediately during shutdown.
+ loader->AsyncDestroy();
+ }
+ }
+
+ sIdToSharedState->Remove(mId);
+ if (sIdToSharedState->IsEmpty()) {
+ delete sIdToSharedState;
+ sIdToSharedState = nullptr;
+ }
+}
+
+void SHEntrySharedParentState::ChangeId(uint64_t aId) {
+ MOZ_ASSERT(aId != 0);
+
+ sIdToSharedState->Remove(mId);
+ mId = aId;
+ sIdToSharedState->InsertOrUpdate(mId, this);
+}
+
+void SHEntrySharedParentState::CopyFrom(SHEntrySharedParentState* aEntry) {
+ mDocShellID = aEntry->mDocShellID;
+ mTriggeringPrincipal = aEntry->mTriggeringPrincipal;
+ mPrincipalToInherit = aEntry->mPrincipalToInherit;
+ mPartitionedPrincipalToInherit = aEntry->mPartitionedPrincipalToInherit;
+ mCsp = aEntry->mCsp;
+ mSaveLayoutState = aEntry->mSaveLayoutState;
+ mContentType.Assign(aEntry->mContentType);
+ mIsFrameNavigation = aEntry->mIsFrameNavigation;
+ mSticky = aEntry->mSticky;
+ mDynamicallyCreated = aEntry->mDynamicallyCreated;
+ mCacheKey = aEntry->mCacheKey;
+ mLastTouched = aEntry->mLastTouched;
+}
+
+void dom::SHEntrySharedParentState::NotifyListenersDocumentViewerEvicted() {
+ if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) {
+ RefPtr<nsSHistory> nsshistory = static_cast<nsSHistory*>(shistory.get());
+ nsshistory->NotifyListenersDocumentViewerEvicted(1);
+ }
+}
+
+void SHEntrySharedChildState::CopyFrom(SHEntrySharedChildState* aEntry) {
+ mChildShells.AppendObjects(aEntry->mChildShells);
+}
+
+void SHEntrySharedParentState::SetFrameLoader(nsFrameLoader* aFrameLoader) {
+ // If expiration tracker is removing this object, IsTracked() returns false.
+ if (GetExpirationState()->IsTracked() && mFrameLoader) {
+ if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) {
+ shistory->RemoveFromExpirationTracker(this);
+ }
+ }
+
+ mFrameLoader = aFrameLoader;
+
+ if (mFrameLoader) {
+ if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) {
+ shistory->AddToExpirationTracker(this);
+ }
+ }
+}
+
+nsFrameLoader* SHEntrySharedParentState::GetFrameLoader() {
+ return mFrameLoader;
+}
+
+} // namespace dom
+} // namespace mozilla
+
+void nsSHEntryShared::Shutdown() {}
+
+nsSHEntryShared::~nsSHEntryShared() {
+ // The destruction can be caused by either the entry is removed from session
+ // history and no one holds the reference, or the whole session history is on
+ // destruction. We want to ensure that we invoke
+ // shistory->RemoveFromExpirationTracker for the former case.
+ RemoveFromExpirationTracker();
+
+ // Calling RemoveDynEntriesForBFCacheEntry on destruction is unnecessary since
+ // there couldn't be any SHEntry holding this shared entry, and we noticed
+ // that calling RemoveDynEntriesForBFCacheEntry in the middle of
+ // nsSHistory::Release can cause a crash, so set mSHistory to null explicitly
+ // before RemoveFromBFCacheSync.
+ mSHistory = nullptr;
+ if (mDocumentViewer) {
+ RemoveFromBFCacheSync();
+ }
+}
+
+NS_IMPL_QUERY_INTERFACE(nsSHEntryShared, nsIBFCacheEntry, nsIMutationObserver)
+NS_IMPL_ADDREF_INHERITED(nsSHEntryShared, dom::SHEntrySharedParentState)
+NS_IMPL_RELEASE_INHERITED(nsSHEntryShared, dom::SHEntrySharedParentState)
+
+already_AddRefed<nsSHEntryShared> nsSHEntryShared::Duplicate() {
+ RefPtr<nsSHEntryShared> newEntry = new nsSHEntryShared();
+
+ newEntry->dom::SHEntrySharedParentState::CopyFrom(this);
+ newEntry->dom::SHEntrySharedChildState::CopyFrom(this);
+
+ return newEntry.forget();
+}
+
+void nsSHEntryShared::RemoveFromExpirationTracker() {
+ nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory);
+ if (shistory && GetExpirationState()->IsTracked()) {
+ shistory->RemoveFromExpirationTracker(this);
+ }
+}
+
+void nsSHEntryShared::SyncPresentationState() {
+ if (mDocumentViewer && mWindowState) {
+ // If we have a content viewer and a window state, we should be ok.
+ return;
+ }
+
+ DropPresentationState();
+}
+
+void nsSHEntryShared::DropPresentationState() {
+ RefPtr<nsSHEntryShared> kungFuDeathGrip = this;
+
+ if (mDocument) {
+ mDocument->SetBFCacheEntry(nullptr);
+ mDocument->RemoveMutationObserver(this);
+ mDocument = nullptr;
+ }
+ if (mDocumentViewer) {
+ mDocumentViewer->ClearHistoryEntry();
+ }
+
+ RemoveFromExpirationTracker();
+ mDocumentViewer = nullptr;
+ mSticky = true;
+ mWindowState = nullptr;
+ mViewerBounds.SetRect(0, 0, 0, 0);
+ mChildShells.Clear();
+ mRefreshURIList = nullptr;
+ mEditorData = nullptr;
+}
+
+nsresult nsSHEntryShared::SetDocumentViewer(nsIDocumentViewer* aViewer) {
+ MOZ_ASSERT(!aViewer || !mDocumentViewer,
+ "SHEntryShared already contains viewer");
+
+ if (mDocumentViewer || !aViewer) {
+ DropPresentationState();
+ }
+
+ // If we're setting mDocumentViewer to null, state should already be cleared
+ // in the DropPresentationState() call above; If we're setting it to a
+ // non-null content viewer, the entry shouldn't have been tracked either.
+ MOZ_ASSERT(!GetExpirationState()->IsTracked());
+ mDocumentViewer = aViewer;
+
+ if (mDocumentViewer) {
+ // mSHistory is only set for root entries, but in general bfcache only
+ // applies to root entries as well. BFCache for subframe navigation has been
+ // disabled since 2005 in bug 304860.
+ if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) {
+ shistory->AddToExpirationTracker(this);
+ }
+
+ // Store observed document in strong pointer in case it is removed from
+ // the contentviewer
+ mDocument = mDocumentViewer->GetDocument();
+ if (mDocument) {
+ mDocument->SetBFCacheEntry(this);
+ mDocument->AddMutationObserver(this);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsSHEntryShared::RemoveFromBFCacheSync() {
+ MOZ_ASSERT(mDocumentViewer && mDocument, "we're not in the bfcache!");
+
+ // The call to DropPresentationState could drop the last reference, so hold
+ // |this| until RemoveDynEntriesForBFCacheEntry finishes.
+ RefPtr<nsSHEntryShared> kungFuDeathGrip = this;
+
+ // DropPresentationState would clear mDocumentViewer.
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ DropPresentationState();
+
+ if (viewer) {
+ viewer->Destroy();
+ }
+
+ // Now that we've dropped the viewer, we have to clear associated dynamic
+ // subframe entries.
+ nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory);
+ if (shistory) {
+ shistory->RemoveDynEntriesForBFCacheEntry(this);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsSHEntryShared::RemoveFromBFCacheAsync() {
+ MOZ_ASSERT(mDocumentViewer && mDocument, "we're not in the bfcache!");
+
+ // Check it again to play safe in release builds.
+ if (!mDocument) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // DropPresentationState would clear mDocumentViewer & mDocument. Capture and
+ // release the references asynchronously so that the document doesn't get
+ // nuked mid-mutation.
+ nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer;
+ RefPtr<dom::Document> document = mDocument;
+ RefPtr<nsSHEntryShared> self = this;
+ nsresult rv = mDocument->Dispatch(NS_NewRunnableFunction(
+ "nsSHEntryShared::RemoveFromBFCacheAsync", [self, viewer, document]() {
+ if (viewer) {
+ viewer->Destroy();
+ }
+
+ nsCOMPtr<nsISHistory> shistory = do_QueryReferent(self->mSHistory);
+ if (shistory) {
+ shistory->RemoveDynEntriesForBFCacheEntry(self);
+ }
+ }));
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch RemoveFromBFCacheAsync runnable.");
+ } else {
+ // Drop presentation. Only do this if we succeeded in posting the event
+ // since otherwise the document could be torn down mid-mutation, causing
+ // crashes.
+ DropPresentationState();
+ }
+
+ return NS_OK;
+}
+
+// Don't evict a page from bfcache for attribute mutations on NAC subtrees like
+// scrollbars.
+static bool IgnoreMutationForBfCache(const nsINode& aNode) {
+ for (const nsINode* node = &aNode; node; node = node->GetParentNode()) {
+ if (!node->ChromeOnlyAccess()) {
+ break;
+ }
+ // Make sure we find a scrollbar in the ancestor chain, to be safe.
+ if (node->IsXULElement(nsGkAtoms::scrollbar)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void nsSHEntryShared::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {
+ if (!IgnoreMutationForBfCache(*aContent)) {
+ RemoveFromBFCacheAsync();
+ }
+}
+
+void nsSHEntryShared::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ if (!IgnoreMutationForBfCache(*aElement)) {
+ RemoveFromBFCacheAsync();
+ }
+}
+
+void nsSHEntryShared::ContentAppended(nsIContent* aFirstNewContent) {
+ if (!IgnoreMutationForBfCache(*aFirstNewContent)) {
+ RemoveFromBFCacheAsync();
+ }
+}
+
+void nsSHEntryShared::ContentInserted(nsIContent* aChild) {
+ if (!IgnoreMutationForBfCache(*aChild)) {
+ RemoveFromBFCacheAsync();
+ }
+}
+
+void nsSHEntryShared::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ if (!IgnoreMutationForBfCache(*aChild)) {
+ RemoveFromBFCacheAsync();
+ }
+}
diff --git a/docshell/shistory/nsSHEntryShared.h b/docshell/shistory/nsSHEntryShared.h
new file mode 100644
index 0000000000..5ff855ead4
--- /dev/null
+++ b/docshell/shistory/nsSHEntryShared.h
@@ -0,0 +1,219 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSHEntryShared_h__
+#define nsSHEntryShared_h__
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsExpirationTracker.h"
+#include "nsIBFCacheEntry.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsRect.h"
+#include "nsString.h"
+#include "nsStubMutationObserver.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+
+class nsSHEntry;
+class nsISHEntry;
+class nsISHistory;
+class nsIContentSecurityPolicy;
+class nsIDocShellTreeItem;
+class nsIDocumentViewer;
+class nsILayoutHistoryState;
+class nsIPrincipal;
+class nsDocShellEditorData;
+class nsFrameLoader;
+class nsIMutableArray;
+class nsSHistory;
+
+// A document may have multiple SHEntries, either due to hash navigations or
+// calls to history.pushState. SHEntries corresponding to the same document
+// share many members; in particular, they share state related to the
+// back/forward cache.
+//
+// The classes defined here are the vehicle for this sharing.
+//
+// Some of the state can only be stored in the process where we did the actual
+// load, because that's where the objects live (eg. the content viewer).
+
+namespace mozilla {
+namespace dom {
+class Document;
+
+/**
+ * SHEntrySharedState holds shared state both in the child process and in the
+ * parent process.
+ */
+struct SHEntrySharedState {
+ SHEntrySharedState() : mId(GenerateId()) {}
+ SHEntrySharedState(const SHEntrySharedState& aState) = default;
+ SHEntrySharedState(nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ const nsACString& aContentType)
+ : mId(GenerateId()),
+ mTriggeringPrincipal(aTriggeringPrincipal),
+ mPrincipalToInherit(aPrincipalToInherit),
+ mPartitionedPrincipalToInherit(aPartitionedPrincipalToInherit),
+ mCsp(aCsp),
+ mContentType(aContentType) {}
+
+ // These members aren't copied by SHEntrySharedParentState::CopyFrom() because
+ // they're specific to a particular content viewer.
+ uint64_t mId = 0;
+
+ // These members are copied by SHEntrySharedParentState::CopyFrom(). If you
+ // add a member here, be sure to update the CopyFrom() implementation.
+ nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
+ nsCOMPtr<nsIPrincipal> mPrincipalToInherit;
+ nsCOMPtr<nsIPrincipal> mPartitionedPrincipalToInherit;
+ nsCOMPtr<nsIContentSecurityPolicy> mCsp;
+ nsCString mContentType;
+ // Child side updates layout history state when page is being unloaded or
+ // moved to bfcache.
+ nsCOMPtr<nsILayoutHistoryState> mLayoutHistoryState;
+ uint32_t mCacheKey = 0;
+ bool mIsFrameNavigation = false;
+ bool mSaveLayoutState = true;
+
+ protected:
+ static uint64_t GenerateId();
+};
+
+/**
+ * SHEntrySharedParentState holds the shared state that can live in the parent
+ * process.
+ */
+class SHEntrySharedParentState : public SHEntrySharedState {
+ public:
+ friend class SessionHistoryInfo;
+
+ uint64_t GetId() const { return mId; }
+ void ChangeId(uint64_t aId);
+
+ void SetFrameLoader(nsFrameLoader* aFrameLoader);
+
+ nsFrameLoader* GetFrameLoader();
+
+ void NotifyListenersDocumentViewerEvicted();
+
+ nsExpirationState* GetExpirationState() { return &mExpirationState; }
+
+ SHEntrySharedParentState();
+ SHEntrySharedParentState(nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aPrincipalToInherit,
+ nsIPrincipal* aPartitionedPrincipalToInherit,
+ nsIContentSecurityPolicy* aCsp,
+ const nsACString& aContentType);
+
+ // This returns the existing SHEntrySharedParentState that was registered for
+ // aId, if one exists.
+ static SHEntrySharedParentState* Lookup(uint64_t aId);
+
+ protected:
+ virtual ~SHEntrySharedParentState();
+ NS_INLINE_DECL_VIRTUAL_REFCOUNTING_WITH_DESTROY(SHEntrySharedParentState,
+ Destroy())
+
+ virtual void Destroy() { delete this; }
+
+ void CopyFrom(SHEntrySharedParentState* aSource);
+
+ // These members are copied by SHEntrySharedParentState::CopyFrom(). If you
+ // add a member here, be sure to update the CopyFrom() implementation.
+ nsID mDocShellID{};
+
+ nsIntRect mViewerBounds{0, 0, 0, 0};
+
+ uint32_t mLastTouched = 0;
+
+ // These members aren't copied by SHEntrySharedParentState::CopyFrom() because
+ // they're specific to a particular content viewer.
+ nsWeakPtr mSHistory;
+
+ RefPtr<nsFrameLoader> mFrameLoader;
+
+ nsExpirationState mExpirationState;
+
+ bool mSticky = true;
+ bool mDynamicallyCreated = false;
+
+ // This flag is about necko cache, not bfcache.
+ bool mExpired = false;
+};
+
+/**
+ * SHEntrySharedChildState holds the shared state that needs to live in the
+ * process where the document was loaded.
+ */
+class SHEntrySharedChildState {
+ protected:
+ void CopyFrom(SHEntrySharedChildState* aSource);
+
+ public:
+ // These members are copied by SHEntrySharedChildState::CopyFrom(). If you
+ // add a member here, be sure to update the CopyFrom() implementation.
+ nsCOMArray<nsIDocShellTreeItem> mChildShells;
+
+ // These members aren't copied by SHEntrySharedChildState::CopyFrom() because
+ // they're specific to a particular content viewer.
+ nsCOMPtr<nsIDocumentViewer> mDocumentViewer;
+ RefPtr<mozilla::dom::Document> mDocument;
+ nsCOMPtr<nsISupports> mWindowState;
+ // FIXME Move to parent?
+ nsCOMPtr<nsIMutableArray> mRefreshURIList;
+ UniquePtr<nsDocShellEditorData> mEditorData;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * nsSHEntryShared holds the shared state if the session history is not stored
+ * in the parent process, or if the load itself happens in the parent process.
+ * Note, since nsSHEntryShared inherits both SHEntrySharedParentState and
+ * SHEntrySharedChildState and those have some same member variables,
+ * the ones from SHEntrySharedParentState should be used.
+ */
+class nsSHEntryShared final : public nsIBFCacheEntry,
+ public nsStubMutationObserver,
+ public mozilla::dom::SHEntrySharedParentState,
+ public mozilla::dom::SHEntrySharedChildState {
+ public:
+ static void EnsureHistoryTracker();
+ static void Shutdown();
+
+ using SHEntrySharedParentState::SHEntrySharedParentState;
+
+ already_AddRefed<nsSHEntryShared> Duplicate();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIBFCACHEENTRY
+
+ // The nsIMutationObserver bits we actually care about.
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ private:
+ ~nsSHEntryShared();
+
+ friend class nsSHEntry;
+
+ void RemoveFromExpirationTracker();
+ void SyncPresentationState();
+ void DropPresentationState();
+
+ nsresult SetDocumentViewer(nsIDocumentViewer* aViewer);
+};
+
+#endif
diff --git a/docshell/shistory/nsSHistory.cpp b/docshell/shistory/nsSHistory.cpp
new file mode 100644
index 0000000000..d69d78a9ec
--- /dev/null
+++ b/docshell/shistory/nsSHistory.cpp
@@ -0,0 +1,2410 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSHistory.h"
+
+#include <algorithm>
+
+#include "nsContentUtils.h"
+#include "nsCOMArray.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDocShell.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsHashKeys.h"
+#include "nsIDocShell.h"
+#include "nsIDocumentViewer.h"
+#include "nsDocShellLoadState.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsILayoutHistoryState.h"
+#include "nsIObserverService.h"
+#include "nsISHEntry.h"
+#include "nsISHistoryListener.h"
+#include "nsIURI.h"
+#include "nsIXULRuntime.h"
+#include "nsNetUtil.h"
+#include "nsTHashMap.h"
+#include "nsSHEntry.h"
+#include "SessionHistoryEntry.h"
+#include "nsTArray.h"
+#include "prsystem.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/RemoteWebProgressRequest.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProcessPriorityManager.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "nsIWebNavigation.h"
+#include "nsDocShellLoadTypes.h"
+#include "base/process.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+#define PREF_SHISTORY_SIZE "browser.sessionhistory.max_entries"
+#define PREF_SHISTORY_MAX_TOTAL_VIEWERS \
+ "browser.sessionhistory.max_total_viewers"
+#define CONTENT_VIEWER_TIMEOUT_SECONDS \
+ "browser.sessionhistory.contentViewerTimeout"
+// Observe fission.bfcacheInParent so that BFCache can be enabled/disabled when
+// the pref is changed.
+#define PREF_FISSION_BFCACHEINPARENT "fission.bfcacheInParent"
+
+// Default this to time out unused content viewers after 30 minutes
+#define CONTENT_VIEWER_TIMEOUT_SECONDS_DEFAULT (30 * 60)
+
+static constexpr const char* kObservedPrefs[] = {
+ PREF_SHISTORY_SIZE, PREF_SHISTORY_MAX_TOTAL_VIEWERS,
+ PREF_FISSION_BFCACHEINPARENT, nullptr};
+
+static int32_t gHistoryMaxSize = 50;
+
+// List of all SHistory objects, used for content viewer cache eviction.
+// When being destroyed, this helper removes everything from the list to avoid
+// assertions when we leak.
+struct ListHelper {
+#ifdef DEBUG
+ ~ListHelper() { mList.clear(); }
+#endif // DEBUG
+
+ LinkedList<nsSHistory> mList;
+};
+
+static ListHelper gSHistoryList;
+// Max viewers allowed total, across all SHistory objects - negative default
+// means we will calculate how many viewers to cache based on total memory
+int32_t nsSHistory::sHistoryMaxTotalViewers = -1;
+
+// A counter that is used to be able to know the order in which
+// entries were touched, so that we can evict older entries first.
+static uint32_t gTouchCounter = 0;
+
+extern mozilla::LazyLogModule gSHLog;
+
+LazyLogModule gSHistoryLog("nsSHistory");
+
+#define LOG(format) MOZ_LOG(gSHistoryLog, mozilla::LogLevel::Debug, format)
+
+extern mozilla::LazyLogModule gPageCacheLog;
+extern mozilla::LazyLogModule gSHIPBFCacheLog;
+
+// This macro makes it easier to print a log message which includes a URI's
+// spec. Example use:
+//
+// nsIURI *uri = [...];
+// LOG_SPEC(("The URI is %s.", _spec), uri);
+//
+#define LOG_SPEC(format, uri) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gSHistoryLog, LogLevel::Debug)) { \
+ nsAutoCString _specStr("(null)"_ns); \
+ if (uri) { \
+ _specStr = uri->GetSpecOrDefault(); \
+ } \
+ const char* _spec = _specStr.get(); \
+ LOG(format); \
+ } \
+ PR_END_MACRO
+
+// This macro makes it easy to log a message including an SHEntry's URI.
+// For example:
+//
+// nsCOMPtr<nsISHEntry> shentry = [...];
+// LOG_SHENTRY_SPEC(("shentry %p has uri %s.", shentry.get(), _spec), shentry);
+//
+#define LOG_SHENTRY_SPEC(format, shentry) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gSHistoryLog, LogLevel::Debug)) { \
+ nsCOMPtr<nsIURI> uri = shentry->GetURI(); \
+ LOG_SPEC(format, uri); \
+ } \
+ PR_END_MACRO
+
+// Calls a F on all registered session history listeners.
+template <typename F>
+static void NotifyListeners(nsAutoTObserverArray<nsWeakPtr, 2>& aListeners,
+ F&& f) {
+ for (const nsWeakPtr& weakPtr : aListeners.EndLimitedRange()) {
+ nsCOMPtr<nsISHistoryListener> listener = do_QueryReferent(weakPtr);
+ if (listener) {
+ f(listener);
+ }
+ }
+}
+
+class MOZ_STACK_CLASS SHistoryChangeNotifier {
+ public:
+ explicit SHistoryChangeNotifier(nsSHistory* aHistory) {
+ // If we're already in an update, the outermost change notifier will
+ // update browsing context in the destructor.
+ if (!aHistory->HasOngoingUpdate()) {
+ aHistory->SetHasOngoingUpdate(true);
+ mSHistory = aHistory;
+ }
+ }
+
+ ~SHistoryChangeNotifier() {
+ if (mSHistory) {
+ MOZ_ASSERT(mSHistory->HasOngoingUpdate());
+ mSHistory->SetHasOngoingUpdate(false);
+
+ RefPtr<BrowsingContext> rootBC = mSHistory->GetBrowsingContext();
+ if (mozilla::SessionHistoryInParent() && rootBC) {
+ rootBC->Canonical()->HistoryCommitIndexAndLength();
+ }
+ }
+ }
+
+ RefPtr<nsSHistory> mSHistory;
+};
+
+enum HistCmd { HIST_CMD_GOTOINDEX, HIST_CMD_RELOAD };
+
+class nsSHistoryObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ nsSHistoryObserver() {}
+
+ static void PrefChanged(const char* aPref, void* aSelf);
+ void PrefChanged(const char* aPref);
+
+ protected:
+ ~nsSHistoryObserver() {}
+};
+
+StaticRefPtr<nsSHistoryObserver> gObserver;
+
+NS_IMPL_ISUPPORTS(nsSHistoryObserver, nsIObserver)
+
+// static
+void nsSHistoryObserver::PrefChanged(const char* aPref, void* aSelf) {
+ static_cast<nsSHistoryObserver*>(aSelf)->PrefChanged(aPref);
+}
+
+void nsSHistoryObserver::PrefChanged(const char* aPref) {
+ nsSHistory::UpdatePrefs();
+ nsSHistory::GloballyEvictDocumentViewers();
+}
+
+NS_IMETHODIMP
+nsSHistoryObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "cacheservice:empty-cache") ||
+ !strcmp(aTopic, "memory-pressure")) {
+ nsSHistory::GloballyEvictAllDocumentViewers();
+ }
+
+ return NS_OK;
+}
+
+void nsSHistory::EvictDocumentViewerForEntry(nsISHEntry* aEntry) {
+ nsCOMPtr<nsIDocumentViewer> viewer = aEntry->GetDocumentViewer();
+ if (viewer) {
+ LOG_SHENTRY_SPEC(("Evicting content viewer 0x%p for "
+ "owning SHEntry 0x%p at %s.",
+ viewer.get(), aEntry, _spec),
+ aEntry);
+
+ // Drop the presentation state before destroying the viewer, so that
+ // document teardown is able to correctly persist the state.
+ NotifyListenersDocumentViewerEvicted(1);
+ aEntry->SetDocumentViewer(nullptr);
+ aEntry->SyncPresentationState();
+ viewer->Destroy();
+ } else if (nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry)) {
+ if (RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader()) {
+ nsCOMPtr<nsFrameLoaderOwner> owner =
+ do_QueryInterface(frameLoader->GetOwnerContent());
+ RefPtr<nsFrameLoader> currentFrameLoader;
+ if (owner) {
+ currentFrameLoader = owner->GetFrameLoader();
+ }
+
+ // Only destroy non-current frameloader when evicting from the bfcache.
+ if (currentFrameLoader != frameLoader) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ ("nsSHistory::EvictDocumentViewerForEntry "
+ "destroying an nsFrameLoader."));
+ NotifyListenersDocumentViewerEvicted(1);
+ she->SetFrameLoader(nullptr);
+ frameLoader->Destroy();
+ }
+ }
+ }
+
+ // When dropping bfcache, we have to remove associated dynamic entries as
+ // well.
+ int32_t index = GetIndexOfEntry(aEntry);
+ if (index != -1) {
+ RemoveDynEntries(index, aEntry);
+ }
+}
+
+nsSHistory::nsSHistory(BrowsingContext* aRootBC)
+ : mRootBC(aRootBC->Id()),
+ mHasOngoingUpdate(false),
+ mIndex(-1),
+ mRequestedIndex(-1),
+ mRootDocShellID(aRootBC->GetHistoryID()) {
+ static bool sCalledStartup = false;
+ if (!sCalledStartup) {
+ Startup();
+ sCalledStartup = true;
+ }
+
+ // Add this new SHistory object to the list
+ gSHistoryList.mList.insertBack(this);
+
+ // Init mHistoryTracker on setting mRootBC so we can bind its event
+ // target to the tabGroup.
+ mHistoryTracker = mozilla::MakeUnique<HistoryTracker>(
+ this,
+ mozilla::Preferences::GetUint(CONTENT_VIEWER_TIMEOUT_SECONDS,
+ CONTENT_VIEWER_TIMEOUT_SECONDS_DEFAULT),
+ GetCurrentSerialEventTarget());
+}
+
+nsSHistory::~nsSHistory() {
+ // Clear mEntries explicitly here so that the destructor of the entries
+ // can still access nsSHistory in a reasonable way.
+ mEntries.Clear();
+}
+
+NS_IMPL_ADDREF(nsSHistory)
+NS_IMPL_RELEASE(nsSHistory)
+
+NS_INTERFACE_MAP_BEGIN(nsSHistory)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISHistory)
+ NS_INTERFACE_MAP_ENTRY(nsISHistory)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+// static
+uint32_t nsSHistory::CalcMaxTotalViewers() {
+// This value allows tweaking how fast the allowed amount of content viewers
+// grows with increasing amounts of memory. Larger values mean slower growth.
+#ifdef ANDROID
+# define MAX_TOTAL_VIEWERS_BIAS 15.9
+#else
+# define MAX_TOTAL_VIEWERS_BIAS 14
+#endif
+
+ // Calculate an estimate of how many ContentViewers we should cache based
+ // on RAM. This assumes that the average ContentViewer is 4MB (conservative)
+ // and caps the max at 8 ContentViewers
+ //
+ // TODO: Should we split the cache memory betw. ContentViewer caching and
+ // nsCacheService?
+ //
+ // RAM | ContentViewers | on Android
+ // -------------------------------------
+ // 32 Mb 0 0
+ // 64 Mb 1 0
+ // 128 Mb 2 0
+ // 256 Mb 3 1
+ // 512 Mb 5 2
+ // 768 Mb 6 2
+ // 1024 Mb 8 3
+ // 2048 Mb 8 5
+ // 3072 Mb 8 7
+ // 4096 Mb 8 8
+ uint64_t bytes = PR_GetPhysicalMemorySize();
+
+ if (bytes == 0) {
+ return 0;
+ }
+
+ // Conversion from unsigned int64_t to double doesn't work on all platforms.
+ // We need to truncate the value at INT64_MAX to make sure we don't
+ // overflow.
+ if (bytes > INT64_MAX) {
+ bytes = INT64_MAX;
+ }
+
+ double kBytesD = (double)(bytes >> 10);
+
+ // This is essentially the same calculation as for nsCacheService,
+ // except that we divide the final memory calculation by 4, since
+ // we assume each ContentViewer takes on average 4MB
+ uint32_t viewers = 0;
+ double x = std::log(kBytesD) / std::log(2.0) - MAX_TOTAL_VIEWERS_BIAS;
+ if (x > 0) {
+ viewers = (uint32_t)(x * x - x + 2.001); // add .001 for rounding
+ viewers /= 4;
+ }
+
+ // Cap it off at 8 max
+ if (viewers > 8) {
+ viewers = 8;
+ }
+ return viewers;
+}
+
+// static
+void nsSHistory::UpdatePrefs() {
+ Preferences::GetInt(PREF_SHISTORY_SIZE, &gHistoryMaxSize);
+ if (mozilla::SessionHistoryInParent() && !mozilla::BFCacheInParent()) {
+ sHistoryMaxTotalViewers = 0;
+ return;
+ }
+
+ Preferences::GetInt(PREF_SHISTORY_MAX_TOTAL_VIEWERS,
+ &sHistoryMaxTotalViewers);
+ // If the pref is negative, that means we calculate how many viewers
+ // we think we should cache, based on total memory
+ if (sHistoryMaxTotalViewers < 0) {
+ sHistoryMaxTotalViewers = CalcMaxTotalViewers();
+ }
+}
+
+// static
+nsresult nsSHistory::Startup() {
+ UpdatePrefs();
+
+ // The goal of this is to unbreak users who have inadvertently set their
+ // session history size to less than the default value.
+ int32_t defaultHistoryMaxSize =
+ Preferences::GetInt(PREF_SHISTORY_SIZE, 50, PrefValueKind::Default);
+ if (gHistoryMaxSize < defaultHistoryMaxSize) {
+ gHistoryMaxSize = defaultHistoryMaxSize;
+ }
+
+ // Allow the user to override the max total number of cached viewers,
+ // but keep the per SHistory cached viewer limit constant
+ if (!gObserver) {
+ gObserver = new nsSHistoryObserver();
+ Preferences::RegisterCallbacks(nsSHistoryObserver::PrefChanged,
+ kObservedPrefs, gObserver.get());
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ // Observe empty-cache notifications so tahat clearing the disk/memory
+ // cache will also evict all content viewers.
+ obsSvc->AddObserver(gObserver, "cacheservice:empty-cache", false);
+
+ // Same for memory-pressure notifications
+ obsSvc->AddObserver(gObserver, "memory-pressure", false);
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+void nsSHistory::Shutdown() {
+ if (gObserver) {
+ Preferences::UnregisterCallbacks(nsSHistoryObserver::PrefChanged,
+ kObservedPrefs, gObserver.get());
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->RemoveObserver(gObserver, "cacheservice:empty-cache");
+ obsSvc->RemoveObserver(gObserver, "memory-pressure");
+ }
+ gObserver = nullptr;
+ }
+}
+
+// static
+already_AddRefed<nsISHEntry> nsSHistory::GetRootSHEntry(nsISHEntry* aEntry) {
+ nsCOMPtr<nsISHEntry> rootEntry = aEntry;
+ nsCOMPtr<nsISHEntry> result = nullptr;
+ while (rootEntry) {
+ result = rootEntry;
+ rootEntry = result->GetParent();
+ }
+
+ return result.forget();
+}
+
+// static
+nsresult nsSHistory::WalkHistoryEntries(nsISHEntry* aRootEntry,
+ BrowsingContext* aBC,
+ WalkHistoryEntriesFunc aCallback,
+ void* aData) {
+ NS_ENSURE_TRUE(aRootEntry, NS_ERROR_FAILURE);
+
+ int32_t childCount = aRootEntry->GetChildCount();
+ for (int32_t i = 0; i < childCount; i++) {
+ nsCOMPtr<nsISHEntry> childEntry;
+ aRootEntry->GetChildAt(i, getter_AddRefs(childEntry));
+ if (!childEntry) {
+ // childEntry can be null for valid reasons, for example if the
+ // docshell at index i never loaded anything useful.
+ // Remember to clone also nulls in the child array (bug 464064).
+ aCallback(nullptr, nullptr, i, aData);
+ continue;
+ }
+
+ BrowsingContext* childBC = nullptr;
+ if (aBC) {
+ for (BrowsingContext* child : aBC->Children()) {
+ // If the SH pref is on and we are in the parent process, update
+ // canonical BC directly
+ bool foundChild = false;
+ if (mozilla::SessionHistoryInParent() && XRE_IsParentProcess()) {
+ if (child->Canonical()->HasHistoryEntry(childEntry)) {
+ childBC = child;
+ foundChild = true;
+ }
+ }
+
+ nsDocShell* docshell = static_cast<nsDocShell*>(child->GetDocShell());
+ if (docshell && docshell->HasHistoryEntry(childEntry)) {
+ childBC = docshell->GetBrowsingContext();
+ foundChild = true;
+ }
+
+ // XXX Simplify this once the old and new session history
+ // implementations don't run at the same time.
+ if (foundChild) {
+ break;
+ }
+ }
+ }
+
+ nsresult rv = aCallback(childEntry, childBC, i, aData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+// callback data for WalkHistoryEntries
+struct MOZ_STACK_CLASS CloneAndReplaceData {
+ CloneAndReplaceData(uint32_t aCloneID, nsISHEntry* aReplaceEntry,
+ bool aCloneChildren, nsISHEntry* aDestTreeParent)
+ : cloneID(aCloneID),
+ cloneChildren(aCloneChildren),
+ replaceEntry(aReplaceEntry),
+ destTreeParent(aDestTreeParent) {}
+
+ uint32_t cloneID;
+ bool cloneChildren;
+ nsISHEntry* replaceEntry;
+ nsISHEntry* destTreeParent;
+ nsCOMPtr<nsISHEntry> resultEntry;
+};
+
+nsresult nsSHistory::CloneAndReplaceChild(nsISHEntry* aEntry,
+ BrowsingContext* aOwnerBC,
+ int32_t aChildIndex, void* aData) {
+ nsCOMPtr<nsISHEntry> dest;
+
+ CloneAndReplaceData* data = static_cast<CloneAndReplaceData*>(aData);
+ uint32_t cloneID = data->cloneID;
+ nsISHEntry* replaceEntry = data->replaceEntry;
+
+ if (!aEntry) {
+ if (data->destTreeParent) {
+ data->destTreeParent->AddChild(nullptr, aChildIndex);
+ }
+ return NS_OK;
+ }
+
+ uint32_t srcID = aEntry->GetID();
+
+ nsresult rv = NS_OK;
+ if (srcID == cloneID) {
+ // Replace the entry
+ dest = replaceEntry;
+ } else {
+ // Clone the SHEntry...
+ rv = aEntry->Clone(getter_AddRefs(dest));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ dest->SetIsSubFrame(true);
+
+ if (srcID != cloneID || data->cloneChildren) {
+ // Walk the children
+ CloneAndReplaceData childData(cloneID, replaceEntry, data->cloneChildren,
+ dest);
+ rv = nsSHistory::WalkHistoryEntries(aEntry, aOwnerBC, CloneAndReplaceChild,
+ &childData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (srcID != cloneID && aOwnerBC) {
+ nsSHistory::HandleEntriesToSwapInDocShell(aOwnerBC, aEntry, dest);
+ }
+
+ if (data->destTreeParent) {
+ data->destTreeParent->AddChild(dest, aChildIndex);
+ }
+ data->resultEntry = dest;
+ return rv;
+}
+
+// static
+nsresult nsSHistory::CloneAndReplace(
+ nsISHEntry* aSrcEntry, BrowsingContext* aOwnerBC, uint32_t aCloneID,
+ nsISHEntry* aReplaceEntry, bool aCloneChildren, nsISHEntry** aDestEntry) {
+ NS_ENSURE_ARG_POINTER(aDestEntry);
+ NS_ENSURE_TRUE(aReplaceEntry, NS_ERROR_FAILURE);
+ CloneAndReplaceData data(aCloneID, aReplaceEntry, aCloneChildren, nullptr);
+ nsresult rv = CloneAndReplaceChild(aSrcEntry, aOwnerBC, 0, &data);
+ data.resultEntry.swap(*aDestEntry);
+ return rv;
+}
+
+// static
+void nsSHistory::WalkContiguousEntries(
+ nsISHEntry* aEntry, const std::function<void(nsISHEntry*)>& aCallback) {
+ MOZ_ASSERT(aEntry);
+
+ nsCOMPtr<nsISHistory> shistory = aEntry->GetShistory();
+ if (!shistory) {
+ // If there is no session history in the entry, it means this is not a root
+ // entry. So, we can return from here.
+ return;
+ }
+
+ int32_t index = shistory->GetIndexOfEntry(aEntry);
+ int32_t count = shistory->GetCount();
+
+ nsCOMPtr<nsIURI> targetURI = aEntry->GetURI();
+
+ // First, call the callback on the input entry.
+ aCallback(aEntry);
+
+ // Walk backward to find the entries that have the same origin as the
+ // input entry.
+ for (int32_t i = index - 1; i >= 0; i--) {
+ RefPtr<nsISHEntry> entry;
+ shistory->GetEntryAtIndex(i, getter_AddRefs(entry));
+ if (entry) {
+ nsCOMPtr<nsIURI> uri = entry->GetURI();
+ if (NS_FAILED(nsContentUtils::GetSecurityManager()->CheckSameOriginURI(
+ targetURI, uri, false, false))) {
+ break;
+ }
+
+ aCallback(entry);
+ }
+ }
+
+ // Then, Walk forward.
+ for (int32_t i = index + 1; i < count; i++) {
+ RefPtr<nsISHEntry> entry;
+ shistory->GetEntryAtIndex(i, getter_AddRefs(entry));
+ if (entry) {
+ nsCOMPtr<nsIURI> uri = entry->GetURI();
+ if (NS_FAILED(nsContentUtils::GetSecurityManager()->CheckSameOriginURI(
+ targetURI, uri, false, false))) {
+ break;
+ }
+
+ aCallback(entry);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::AddChildSHEntryHelper(nsISHEntry* aCloneRef, nsISHEntry* aNewEntry,
+ BrowsingContext* aRootBC,
+ bool aCloneChildren) {
+ MOZ_ASSERT(aRootBC->IsTop());
+
+ /* You are currently in the rootDocShell.
+ * You will get here when a subframe has a new url
+ * to load and you have walked up the tree all the
+ * way to the top to clone the current SHEntry hierarchy
+ * and replace the subframe where a new url was loaded with
+ * a new entry.
+ */
+ nsCOMPtr<nsISHEntry> child;
+ nsCOMPtr<nsISHEntry> currentHE;
+ int32_t index = mIndex;
+ if (index < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ GetEntryAtIndex(index, getter_AddRefs(currentHE));
+ NS_ENSURE_TRUE(currentHE, NS_ERROR_FAILURE);
+
+ nsresult rv = NS_OK;
+ uint32_t cloneID = aCloneRef->GetID();
+ rv = nsSHistory::CloneAndReplace(currentHE, aRootBC, cloneID, aNewEntry,
+ aCloneChildren, getter_AddRefs(child));
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = AddEntry(child, true);
+ if (NS_SUCCEEDED(rv)) {
+ child->SetDocshellID(aRootBC->GetHistoryID());
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsSHistory::SetChildHistoryEntry(nsISHEntry* aEntry,
+ BrowsingContext* aBC,
+ int32_t aEntryIndex, void* aData) {
+ SwapEntriesData* data = static_cast<SwapEntriesData*>(aData);
+ if (!aBC || aBC == data->ignoreBC) {
+ return NS_OK;
+ }
+
+ nsISHEntry* destTreeRoot = data->destTreeRoot;
+
+ nsCOMPtr<nsISHEntry> destEntry;
+
+ if (data->destTreeParent) {
+ // aEntry is a clone of some child of destTreeParent, but since the
+ // trees aren't necessarily in sync, we'll have to locate it.
+ // Note that we could set aShell's entry to null if we don't find a
+ // corresponding entry under destTreeParent.
+
+ uint32_t targetID = aEntry->GetID();
+
+ // First look at the given index, since this is the common case.
+ nsCOMPtr<nsISHEntry> entry;
+ data->destTreeParent->GetChildAt(aEntryIndex, getter_AddRefs(entry));
+ if (entry && entry->GetID() == targetID) {
+ destEntry.swap(entry);
+ } else {
+ int32_t childCount;
+ data->destTreeParent->GetChildCount(&childCount);
+ for (int32_t i = 0; i < childCount; ++i) {
+ data->destTreeParent->GetChildAt(i, getter_AddRefs(entry));
+ if (!entry) {
+ continue;
+ }
+
+ if (entry->GetID() == targetID) {
+ destEntry.swap(entry);
+ break;
+ }
+ }
+ }
+ } else {
+ destEntry = destTreeRoot;
+ }
+
+ nsSHistory::HandleEntriesToSwapInDocShell(aBC, aEntry, destEntry);
+ // Now handle the children of aEntry.
+ SwapEntriesData childData = {data->ignoreBC, destTreeRoot, destEntry};
+ return nsSHistory::WalkHistoryEntries(aEntry, aBC, SetChildHistoryEntry,
+ &childData);
+}
+
+// static
+void nsSHistory::HandleEntriesToSwapInDocShell(
+ mozilla::dom::BrowsingContext* aBC, nsISHEntry* aOldEntry,
+ nsISHEntry* aNewEntry) {
+ bool shPref = mozilla::SessionHistoryInParent();
+ if (aBC->IsInProcess() || !shPref) {
+ nsDocShell* docshell = static_cast<nsDocShell*>(aBC->GetDocShell());
+ if (docshell) {
+ docshell->SwapHistoryEntries(aOldEntry, aNewEntry);
+ }
+ } else {
+ // FIXME Bug 1633988: Need to update entries?
+ }
+
+ // XXX Simplify this once the old and new session history implementations
+ // don't run at the same time.
+ if (shPref && XRE_IsParentProcess()) {
+ aBC->Canonical()->SwapHistoryEntries(aOldEntry, aNewEntry);
+ }
+}
+
+void nsSHistory::UpdateRootBrowsingContextState(BrowsingContext* aRootBC) {
+ if (aRootBC && aRootBC->EverAttached()) {
+ bool sameDocument = IsEmptyOrHasEntriesForSingleTopLevelPage();
+ if (sameDocument != aRootBC->GetIsSingleToplevelInHistory()) {
+ // If the browsing context is discarded then its session history is
+ // invalid and will go away.
+ Unused << aRootBC->SetIsSingleToplevelInHistory(sameDocument);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::AddToRootSessionHistory(bool aCloneChildren, nsISHEntry* aOSHE,
+ BrowsingContext* aRootBC,
+ nsISHEntry* aEntry, uint32_t aLoadType,
+ bool aShouldPersist,
+ Maybe<int32_t>* aPreviousEntryIndex,
+ Maybe<int32_t>* aLoadedEntryIndex) {
+ MOZ_ASSERT(aRootBC->IsTop());
+
+ nsresult rv = NS_OK;
+
+ // If we need to clone our children onto the new session
+ // history entry, do so now.
+ if (aCloneChildren && aOSHE) {
+ uint32_t cloneID = aOSHE->GetID();
+ nsCOMPtr<nsISHEntry> newEntry;
+ nsSHistory::CloneAndReplace(aOSHE, aRootBC, cloneID, aEntry, true,
+ getter_AddRefs(newEntry));
+ NS_ASSERTION(aEntry == newEntry,
+ "The new session history should be in the new entry");
+ }
+ // This is the root docshell
+ bool addToSHistory = !LOAD_TYPE_HAS_FLAGS(
+ aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY);
+ if (!addToSHistory) {
+ // Replace current entry in session history; If the requested index is
+ // valid, it indicates the loading was triggered by a history load, and
+ // we should replace the entry at requested index instead.
+ int32_t index = GetIndexForReplace();
+
+ // Replace the current entry with the new entry
+ if (index >= 0) {
+ rv = ReplaceEntry(index, aEntry);
+ } else {
+ // If we're trying to replace an inexistant shistory entry, append.
+ addToSHistory = true;
+ }
+ }
+ if (addToSHistory) {
+ // Add to session history
+ *aPreviousEntryIndex = Some(mIndex);
+ rv = AddEntry(aEntry, aShouldPersist);
+ *aLoadedEntryIndex = Some(mIndex);
+ MOZ_LOG(gPageCacheLog, LogLevel::Verbose,
+ ("Previous index: %d, Loaded index: %d",
+ aPreviousEntryIndex->value(), aLoadedEntryIndex->value()));
+ }
+ if (NS_SUCCEEDED(rv)) {
+ aEntry->SetDocshellID(aRootBC->GetHistoryID());
+ }
+ return rv;
+}
+
+/* Add an entry to the History list at mIndex and
+ * increment the index to point to the new entry
+ */
+NS_IMETHODIMP
+nsSHistory::AddEntry(nsISHEntry* aSHEntry, bool aPersist) {
+ NS_ENSURE_ARG(aSHEntry);
+
+ nsCOMPtr<nsISHistory> shistoryOfEntry = aSHEntry->GetShistory();
+ if (shistoryOfEntry && shistoryOfEntry != this) {
+ NS_WARNING(
+ "The entry has been associated to another nsISHistory instance. "
+ "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() "
+ "first if you're copying an entry from another nsISHistory.");
+ return NS_ERROR_FAILURE;
+ }
+
+ aSHEntry->SetShistory(this);
+
+ // If we have a root docshell, update the docshell id of the root shentry to
+ // match the id of that docshell
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (rootBC) {
+ aSHEntry->SetDocshellID(mRootDocShellID);
+ }
+
+ if (mIndex >= 0) {
+ MOZ_ASSERT(mIndex < Length(), "Index out of range!");
+ if (mIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mEntries[mIndex] && !mEntries[mIndex]->GetPersist()) {
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
+ aSHEntry->SetPersist(aPersist);
+ mEntries[mIndex] = aSHEntry;
+ UpdateRootBrowsingContextState();
+ return NS_OK;
+ }
+ }
+ SHistoryChangeNotifier change(this);
+
+ int32_t truncating = Length() - 1 - mIndex;
+ if (truncating > 0) {
+ NotifyListeners(mListeners,
+ [truncating](auto l) { l->OnHistoryTruncate(truncating); });
+ }
+
+ nsCOMPtr<nsIURI> uri = aSHEntry->GetURI();
+ NotifyListeners(mListeners,
+ [&uri, this](auto l) { l->OnHistoryNewEntry(uri, mIndex); });
+
+ // Remove all entries after the current one, add the new one, and set the
+ // new one as the current one.
+ MOZ_ASSERT(mIndex >= -1);
+ aSHEntry->SetPersist(aPersist);
+ mEntries.TruncateLength(mIndex + 1);
+ mEntries.AppendElement(aSHEntry);
+ mIndex++;
+ if (mIndex > 0) {
+ UpdateEntryLength(mEntries[mIndex - 1], mEntries[mIndex], false);
+ }
+
+ // Purge History list if it is too long
+ if (gHistoryMaxSize >= 0 && Length() > gHistoryMaxSize) {
+ PurgeHistory(Length() - gHistoryMaxSize);
+ }
+
+ UpdateRootBrowsingContextState();
+
+ return NS_OK;
+}
+
+void nsSHistory::NotifyOnHistoryReplaceEntry() {
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
+}
+
+/* Get size of the history list */
+NS_IMETHODIMP
+nsSHistory::GetCount(int32_t* aResult) {
+ MOZ_ASSERT(aResult, "null out param?");
+ *aResult = Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::GetIndex(int32_t* aResult) {
+ MOZ_ASSERT(aResult, "null out param?");
+ *aResult = mIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::SetIndex(int32_t aIndex) {
+ if (aIndex < 0 || aIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mIndex = aIndex;
+ return NS_OK;
+}
+
+/* Get the requestedIndex */
+NS_IMETHODIMP
+nsSHistory::GetRequestedIndex(int32_t* aResult) {
+ MOZ_ASSERT(aResult, "null out param?");
+ *aResult = mRequestedIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::InternalSetRequestedIndex(int32_t aRequestedIndex) {
+ MOZ_ASSERT(aRequestedIndex >= -1 && aRequestedIndex < Length());
+ mRequestedIndex = aRequestedIndex;
+}
+
+NS_IMETHODIMP
+nsSHistory::GetEntryAtIndex(int32_t aIndex, nsISHEntry** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (aIndex < 0 || aIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aResult = mEntries[aIndex];
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(int32_t)
+nsSHistory::GetIndexOfEntry(nsISHEntry* aSHEntry) {
+ for (int32_t i = 0; i < Length(); i++) {
+ if (aSHEntry == mEntries[i]) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+static void LogEntry(nsISHEntry* aEntry, int32_t aIndex, int32_t aTotal,
+ const nsCString& aPrefix, bool aIsCurrent) {
+ if (!aEntry) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s+- %i SH Entry null\n", aPrefix.get(), aIndex));
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri = aEntry->GetURI();
+ nsAutoString title, name;
+ aEntry->GetTitle(title);
+ aEntry->GetName(name);
+
+ SHEntrySharedParentState* shared;
+ if (mozilla::SessionHistoryInParent()) {
+ shared = static_cast<SessionHistoryEntry*>(aEntry)->SharedInfo();
+ } else {
+ shared = static_cast<nsSHEntry*>(aEntry)->GetState();
+ }
+
+ nsID docShellId;
+ aEntry->GetDocshellID(docShellId);
+
+ int32_t childCount = aEntry->GetChildCount();
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("%s%s+- %i SH Entry %p %" PRIu64 " %s\n", aIsCurrent ? ">" : " ",
+ aPrefix.get(), aIndex, aEntry, shared->GetId(),
+ nsIDToCString(docShellId).get()));
+
+ nsCString prefix(aPrefix);
+ if (aIndex < aTotal - 1) {
+ prefix.AppendLiteral("| ");
+ } else {
+ prefix.AppendLiteral(" ");
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s%s URL = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ uri->GetSpecOrDefault().get()));
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s%s Title = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ NS_LossyConvertUTF16toASCII(title).get()));
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s%s Name = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ NS_LossyConvertUTF16toASCII(name).get()));
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ (" %s%s Is in BFCache = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ aEntry->GetIsInBFCache() ? "true" : "false"));
+
+ nsCOMPtr<nsISHEntry> prevChild;
+ for (int32_t i = 0; i < childCount; ++i) {
+ nsCOMPtr<nsISHEntry> child;
+ aEntry->GetChildAt(i, getter_AddRefs(child));
+ LogEntry(child, i, childCount, prefix, false);
+ child.swap(prevChild);
+ }
+}
+
+void nsSHistory::LogHistory() {
+ if (!MOZ_LOG_TEST(gSHLog, LogLevel::Debug)) {
+ return;
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("nsSHistory %p\n", this));
+ int32_t length = Length();
+ for (int32_t i = 0; i < length; i++) {
+ LogEntry(mEntries[i], i, length, EmptyCString(), i == mIndex);
+ }
+}
+
+void nsSHistory::WindowIndices(int32_t aIndex, int32_t* aOutStartIndex,
+ int32_t* aOutEndIndex) {
+ *aOutStartIndex = std::max(0, aIndex - nsSHistory::VIEWER_WINDOW);
+ *aOutEndIndex = std::min(Length() - 1, aIndex + nsSHistory::VIEWER_WINDOW);
+}
+
+static void MarkAsInitialEntry(
+ SessionHistoryEntry* aEntry,
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable) {
+ if (!aEntry->BCHistoryLength().Modified()) {
+ ++(aEntry->BCHistoryLength());
+ }
+ aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry);
+ for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) {
+ if (entry) {
+ MarkAsInitialEntry(entry, aHashtable);
+ }
+ }
+}
+
+static void ClearEntries(SessionHistoryEntry* aEntry) {
+ aEntry->ClearBCHistoryLength();
+ for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) {
+ if (entry) {
+ ClearEntries(entry);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::PurgeHistory(int32_t aNumEntries) {
+ if (Length() <= 0 || aNumEntries <= 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SHistoryChangeNotifier change(this);
+
+ aNumEntries = std::min(aNumEntries, Length());
+
+ NotifyListeners(mListeners,
+ [aNumEntries](auto l) { l->OnHistoryPurge(aNumEntries); });
+
+ // Set all the entries hanging of the first entry that we keep
+ // (mEntries[aNumEntries]) as being created as the result of a load
+ // (so contributing one to their BCHistoryLength).
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*> docshellIDToEntry;
+ if (aNumEntries != Length()) {
+ nsCOMPtr<SessionHistoryEntry> she =
+ do_QueryInterface(mEntries[aNumEntries]);
+ if (she) {
+ MarkAsInitialEntry(she, docshellIDToEntry);
+ }
+ }
+
+ // Reset the BCHistoryLength of all the entries that we're removing to a new
+ // counter with value 0 while decreasing their contribution to a shared
+ // BCHistoryLength. The end result is that they don't contribute to the
+ // BCHistoryLength of any other entry anymore.
+ for (int32_t i = 0; i < aNumEntries; ++i) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(mEntries[i]);
+ if (she) {
+ ClearEntries(she);
+ }
+ }
+
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (rootBC) {
+ rootBC->PreOrderWalk([&docshellIDToEntry](BrowsingContext* aBC) {
+ SessionHistoryEntry* entry = docshellIDToEntry.Get(aBC->GetHistoryID());
+ Unused << aBC->SetHistoryEntryCount(
+ entry ? uint32_t(entry->BCHistoryLength()) : 0);
+ });
+ }
+
+ // Remove the first `aNumEntries` entries.
+ mEntries.RemoveElementsAt(0, aNumEntries);
+
+ // Adjust the indices, but don't let them go below -1.
+ mIndex -= aNumEntries;
+ mIndex = std::max(mIndex, -1);
+ mRequestedIndex -= aNumEntries;
+ mRequestedIndex = std::max(mRequestedIndex, -1);
+
+ if (rootBC && rootBC->GetDocShell()) {
+ rootBC->GetDocShell()->HistoryPurged(aNumEntries);
+ }
+
+ UpdateRootBrowsingContextState(rootBC);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::AddSHistoryListener(nsISHistoryListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ // Check if the listener supports Weak Reference. This is a must.
+ // This listener functionality is used by embedders and we want to
+ // have the right ownership with who ever listens to SHistory
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ if (!listener) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mListeners.AppendElementUnlessExists(listener);
+ return NS_OK;
+}
+
+void nsSHistory::NotifyListenersDocumentViewerEvicted(uint32_t aNumEvicted) {
+ NotifyListeners(mListeners, [aNumEvicted](auto l) {
+ l->OnDocumentViewerEvicted(aNumEvicted);
+ });
+}
+
+NS_IMETHODIMP
+nsSHistory::RemoveSHistoryListener(nsISHistoryListener* aListener) {
+ // Make sure the listener that wants to be removed is the
+ // one we have in store.
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ mListeners.RemoveElement(listener);
+ return NS_OK;
+}
+
+/* Replace an entry in the History list at a particular index.
+ * Do not update index or count.
+ */
+NS_IMETHODIMP
+nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry) {
+ NS_ENSURE_ARG(aReplaceEntry);
+
+ if (aIndex < 0 || aIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsISHistory> shistoryOfEntry = aReplaceEntry->GetShistory();
+ if (shistoryOfEntry && shistoryOfEntry != this) {
+ NS_WARNING(
+ "The entry has been associated to another nsISHistory instance. "
+ "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() "
+ "first if you're copying an entry from another nsISHistory.");
+ return NS_ERROR_FAILURE;
+ }
+
+ aReplaceEntry->SetShistory(this);
+
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
+
+ aReplaceEntry->SetPersist(true);
+ mEntries[aIndex] = aReplaceEntry;
+
+ UpdateRootBrowsingContextState();
+
+ return NS_OK;
+}
+
+// Calls OnHistoryReload on all registered session history listeners.
+// Listeners may return 'false' to cancel an action so make sure that we
+// set the return value to 'false' if one of the listeners wants to cancel.
+NS_IMETHODIMP
+nsSHistory::NotifyOnHistoryReload(bool* aCanReload) {
+ *aCanReload = true;
+
+ for (const nsWeakPtr& weakPtr : mListeners.EndLimitedRange()) {
+ nsCOMPtr<nsISHistoryListener> listener = do_QueryReferent(weakPtr);
+ if (listener) {
+ bool retval = true;
+
+ if (NS_SUCCEEDED(listener->OnHistoryReload(&retval)) && !retval) {
+ *aCanReload = false;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::EvictOutOfRangeDocumentViewers(int32_t aIndex) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ ("nsSHistory::EvictOutOfRangeDocumentViewers %i", aIndex));
+
+ // Check our per SHistory object limit in the currently navigated SHistory
+ EvictOutOfRangeWindowDocumentViewers(aIndex);
+ // Check our total limit across all SHistory objects
+ GloballyEvictDocumentViewers();
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::EvictDocumentViewersOrReplaceEntry(nsISHEntry* aNewSHEntry,
+ bool aReplace) {
+ if (!aReplace) {
+ int32_t curIndex;
+ GetIndex(&curIndex);
+ if (curIndex > -1) {
+ EvictOutOfRangeDocumentViewers(curIndex);
+ }
+ } else {
+ nsCOMPtr<nsISHEntry> rootSHEntry = nsSHistory::GetRootSHEntry(aNewSHEntry);
+
+ int32_t index = GetIndexOfEntry(rootSHEntry);
+ if (index > -1) {
+ ReplaceEntry(index, rootSHEntry);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::EvictAllDocumentViewers() {
+ // XXXbz we don't actually do a good job of evicting things as we should, so
+ // we might have viewers quite far from mIndex. So just evict everything.
+ for (int32_t i = 0; i < Length(); i++) {
+ EvictDocumentViewerForEntry(mEntries[i]);
+ }
+
+ return NS_OK;
+}
+
+static void FinishRestore(CanonicalBrowsingContext* aBrowsingContext,
+ nsDocShellLoadState* aLoadState,
+ SessionHistoryEntry* aEntry,
+ nsFrameLoader* aFrameLoader, bool aCanSave) {
+ MOZ_ASSERT(aEntry);
+ MOZ_ASSERT(aFrameLoader);
+
+ aEntry->SetFrameLoader(nullptr);
+
+ nsCOMPtr<nsISHistory> shistory = aEntry->GetShistory();
+ int32_t indexOfHistoryLoad =
+ shistory ? shistory->GetIndexOfEntry(aEntry) : -1;
+
+ nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner =
+ do_QueryInterface(aBrowsingContext->GetEmbedderElement());
+ if (frameLoaderOwner && aFrameLoader->GetMaybePendingBrowsingContext() &&
+ indexOfHistoryLoad >= 0) {
+ RefPtr<BrowsingContextWebProgress> webProgress =
+ aBrowsingContext->GetWebProgress();
+ if (webProgress) {
+ // Synthesize a STATE_START WebProgress state change event from here
+ // in order to ensure emitting it on the BrowsingContext we navigate
+ // *from* instead of the BrowsingContext we navigate *to*. This will fire
+ // before and the next one will be ignored by BrowsingContextWebProgress:
+ // https://searchfox.org/mozilla-central/rev/77f0b36028b2368e342c982ea47609040b399d89/docshell/base/BrowsingContextWebProgress.cpp#196-203
+ nsCOMPtr<nsIURI> nextURI = aEntry->GetURI();
+ nsCOMPtr<nsIURI> nextOriginalURI = aEntry->GetOriginalURI();
+ nsCOMPtr<nsIRequest> request = MakeAndAddRef<RemoteWebProgressRequest>(
+ nextURI, nextOriginalURI ? nextOriginalURI : nextURI,
+ ""_ns /* aMatchedList */);
+ webProgress->OnStateChange(webProgress, request,
+ nsIWebProgressListener::STATE_START |
+ nsIWebProgressListener::STATE_IS_DOCUMENT |
+ nsIWebProgressListener::STATE_IS_REQUEST |
+ nsIWebProgressListener::STATE_IS_WINDOW |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ NS_OK);
+ }
+
+ RefPtr<CanonicalBrowsingContext> loadingBC =
+ aFrameLoader->GetMaybePendingBrowsingContext()->Canonical();
+ RefPtr<nsFrameLoader> currentFrameLoader =
+ frameLoaderOwner->GetFrameLoader();
+ // The current page can be bfcached, store the
+ // nsFrameLoader in the current SessionHistoryEntry.
+ RefPtr<SessionHistoryEntry> currentSHEntry =
+ aBrowsingContext->GetActiveSessionHistoryEntry();
+ if (currentSHEntry) {
+ // Update layout history state now, before we change the IsInBFCache flag
+ // and the active session history entry.
+ aBrowsingContext->SynchronizeLayoutHistoryState();
+
+ if (aCanSave) {
+ currentSHEntry->SetFrameLoader(currentFrameLoader);
+ Unused << aBrowsingContext->SetIsInBFCache(true);
+ }
+ }
+
+ if (aBrowsingContext->IsActive()) {
+ loadingBC->PreOrderWalk([&](BrowsingContext* aContext) {
+ if (BrowserParent* bp = aContext->Canonical()->GetBrowserParent()) {
+ ProcessPriorityManager::BrowserPriorityChanged(bp, true);
+ }
+ });
+ }
+
+ if (aEntry) {
+ aEntry->SetWireframe(Nothing());
+ }
+
+ // ReplacedBy will swap the entry back.
+ aBrowsingContext->SetActiveSessionHistoryEntry(aEntry);
+ loadingBC->SetActiveSessionHistoryEntry(nullptr);
+ NavigationIsolationOptions options;
+ aBrowsingContext->ReplacedBy(loadingBC, options);
+
+ // Assuming we still have the session history, update the index.
+ if (loadingBC->GetSessionHistory()) {
+ shistory->InternalSetRequestedIndex(indexOfHistoryLoad);
+ shistory->UpdateIndex();
+ }
+ loadingBC->HistoryCommitIndexAndLength();
+
+ // ResetSHEntryHasUserInteractionCache(); ?
+ // browser.navigation.requireUserInteraction is still
+ // disabled everywhere.
+
+ frameLoaderOwner->RestoreFrameLoaderFromBFCache(aFrameLoader);
+ // EvictOutOfRangeDocumentViewers is called here explicitly to
+ // possibly evict the now in the bfcache document.
+ // HistoryCommitIndexAndLength might not have evicted that before the
+ // FrameLoader swap.
+ shistory->EvictOutOfRangeDocumentViewers(indexOfHistoryLoad);
+
+ // The old page can't be stored in the bfcache,
+ // destroy the nsFrameLoader.
+ if (!aCanSave && currentFrameLoader) {
+ currentFrameLoader->Destroy();
+ }
+
+ Unused << loadingBC->SetIsInBFCache(false);
+
+ // We need to call this after we've restored the page from BFCache (see
+ // SetIsInBFCache(false) above), so that the page is not frozen anymore and
+ // the right focus events are fired.
+ frameLoaderOwner->UpdateFocusAndMouseEnterStateAfterFrameLoaderChange();
+
+ return;
+ }
+
+ aFrameLoader->Destroy();
+
+ // Fall back to do a normal load.
+ aBrowsingContext->LoadURI(aLoadState, false);
+}
+
+/* static */
+void nsSHistory::LoadURIOrBFCache(LoadEntryResult& aLoadEntry) {
+ if (mozilla::BFCacheInParent() && aLoadEntry.mBrowsingContext->IsTop()) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ RefPtr<nsDocShellLoadState> loadState = aLoadEntry.mLoadState;
+ RefPtr<CanonicalBrowsingContext> canonicalBC =
+ aLoadEntry.mBrowsingContext->Canonical();
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(loadState->SHEntry());
+ nsCOMPtr<SessionHistoryEntry> currentShe =
+ canonicalBC->GetActiveSessionHistoryEntry();
+ MOZ_ASSERT(she);
+ RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader();
+ if (frameLoader &&
+ (!currentShe || (she->SharedInfo() != currentShe->SharedInfo() &&
+ !currentShe->GetFrameLoader()))) {
+ bool canSave = (!currentShe || currentShe->GetSaveLayoutStateFlag()) &&
+ canonicalBC->AllowedInBFCache(Nothing(), nullptr);
+
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ ("nsSHistory::LoadURIOrBFCache "
+ "saving presentation=%i",
+ canSave));
+
+ nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner =
+ do_QueryInterface(canonicalBC->GetEmbedderElement());
+ if (frameLoaderOwner) {
+ RefPtr<nsFrameLoader> currentFrameLoader =
+ frameLoaderOwner->GetFrameLoader();
+ if (currentFrameLoader &&
+ currentFrameLoader->GetMaybePendingBrowsingContext()) {
+ if (WindowGlobalParent* wgp =
+ currentFrameLoader->GetMaybePendingBrowsingContext()
+ ->Canonical()
+ ->GetCurrentWindowGlobal()) {
+ wgp->PermitUnload([canonicalBC, loadState, she, frameLoader,
+ currentFrameLoader, canSave](bool aAllow) {
+ if (aAllow && !canonicalBC->IsReplaced()) {
+ FinishRestore(canonicalBC, loadState, she, frameLoader,
+ canSave && canonicalBC->AllowedInBFCache(
+ Nothing(), nullptr));
+ } else if (currentFrameLoader->GetMaybePendingBrowsingContext()) {
+ nsISHistory* shistory =
+ currentFrameLoader->GetMaybePendingBrowsingContext()
+ ->Canonical()
+ ->GetSessionHistory();
+ if (shistory) {
+ shistory->InternalSetRequestedIndex(-1);
+ }
+ }
+ });
+ return;
+ }
+ }
+ }
+
+ FinishRestore(canonicalBC, loadState, she, frameLoader, canSave);
+ return;
+ }
+ if (frameLoader) {
+ she->SetFrameLoader(nullptr);
+ frameLoader->Destroy();
+ }
+ }
+
+ RefPtr<BrowsingContext> bc = aLoadEntry.mBrowsingContext;
+ RefPtr<nsDocShellLoadState> loadState = aLoadEntry.mLoadState;
+ bc->LoadURI(loadState, false);
+}
+
+/* static */
+void nsSHistory::LoadURIs(nsTArray<LoadEntryResult>& aLoadResults) {
+ for (LoadEntryResult& loadEntry : aLoadResults) {
+ LoadURIOrBFCache(loadEntry);
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::Reload(uint32_t aReloadFlags) {
+ nsTArray<LoadEntryResult> loadResults;
+ nsresult rv = Reload(aReloadFlags, loadResults);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (loadResults.IsEmpty()) {
+ return NS_OK;
+ }
+
+ LoadURIs(loadResults);
+ return NS_OK;
+}
+
+nsresult nsSHistory::Reload(uint32_t aReloadFlags,
+ nsTArray<LoadEntryResult>& aLoadResults) {
+ MOZ_ASSERT(aLoadResults.IsEmpty());
+
+ uint32_t loadType;
+ if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY &&
+ aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE) {
+ loadType = LOAD_RELOAD_BYPASS_PROXY_AND_CACHE;
+ } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY) {
+ loadType = LOAD_RELOAD_BYPASS_PROXY;
+ } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE) {
+ loadType = LOAD_RELOAD_BYPASS_CACHE;
+ } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE) {
+ loadType = LOAD_RELOAD_CHARSET_CHANGE;
+ } else {
+ loadType = LOAD_RELOAD_NORMAL;
+ }
+
+ // We are reloading. Send Reload notifications.
+ // nsDocShellLoadFlagType is not public, where as nsIWebNavigation
+ // is public. So send the reload notifications with the
+ // nsIWebNavigation flags.
+ bool canNavigate = true;
+ MOZ_ALWAYS_SUCCEEDS(NotifyOnHistoryReload(&canNavigate));
+ if (!canNavigate) {
+ return NS_OK;
+ }
+
+ nsresult rv = LoadEntry(
+ mIndex, loadType, HIST_CMD_RELOAD, aLoadResults, /* aSameEpoch */ false,
+ /* aLoadCurrentEntry */ true,
+ aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION);
+ if (NS_FAILED(rv)) {
+ aLoadResults.Clear();
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::ReloadCurrentEntry() {
+ nsTArray<LoadEntryResult> loadResults;
+ nsresult rv = ReloadCurrentEntry(loadResults);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LoadURIs(loadResults);
+ return NS_OK;
+}
+
+nsresult nsSHistory::ReloadCurrentEntry(
+ nsTArray<LoadEntryResult>& aLoadResults) {
+ // Notify listeners
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryGotoIndex(); });
+
+ return LoadEntry(mIndex, LOAD_HISTORY, HIST_CMD_RELOAD, aLoadResults,
+ /* aSameEpoch */ false, /* aLoadCurrentEntry */ true,
+ /* aUserActivation */ false);
+}
+
+void nsSHistory::EvictOutOfRangeWindowDocumentViewers(int32_t aIndex) {
+ // XXX rename method to EvictDocumentViewersExceptAroundIndex, or something.
+
+ // We need to release all content viewers that are no longer in the range
+ //
+ // aIndex - VIEWER_WINDOW to aIndex + VIEWER_WINDOW
+ //
+ // to ensure that this SHistory object isn't responsible for more than
+ // VIEWER_WINDOW content viewers. But our job is complicated by the
+ // fact that two entries which are related by either hash navigations or
+ // history.pushState will have the same content viewer.
+ //
+ // To illustrate the issue, suppose VIEWER_WINDOW = 3 and we have four
+ // linked entries in our history. Suppose we then add a new content
+ // viewer and call into this function. So the history looks like:
+ //
+ // A A A A B
+ // + *
+ //
+ // where the letters are content viewers and + and * denote the beginning and
+ // end of the range aIndex +/- VIEWER_WINDOW.
+ //
+ // Although one copy of the content viewer A exists outside the range, we
+ // don't want to evict A, because it has other copies in range!
+ //
+ // We therefore adjust our eviction strategy to read:
+ //
+ // Evict each content viewer outside the range aIndex -/+
+ // VIEWER_WINDOW, unless that content viewer also appears within the
+ // range.
+ //
+ // (Note that it's entirely legal to have two copies of one content viewer
+ // separated by a different content viewer -- call pushState twice, go back
+ // once, and refresh -- so we can't rely on identical viewers only appearing
+ // adjacent to one another.)
+
+ if (aIndex < 0) {
+ return;
+ }
+ NS_ENSURE_TRUE_VOID(aIndex < Length());
+
+ // Calculate the range that's safe from eviction.
+ int32_t startSafeIndex, endSafeIndex;
+ WindowIndices(aIndex, &startSafeIndex, &endSafeIndex);
+
+ LOG(
+ ("EvictOutOfRangeWindowDocumentViewers(index=%d), "
+ "Length()=%d. Safe range [%d, %d]",
+ aIndex, Length(), startSafeIndex, endSafeIndex));
+
+ // The content viewers in range aIndex -/+ VIEWER_WINDOW will not be
+ // evicted. Collect a set of them so we don't accidentally evict one of them
+ // if it appears outside this range.
+ nsCOMArray<nsIDocumentViewer> safeViewers;
+ nsTArray<RefPtr<nsFrameLoader>> safeFrameLoaders;
+ for (int32_t i = startSafeIndex; i <= endSafeIndex; i++) {
+ nsCOMPtr<nsIDocumentViewer> viewer = mEntries[i]->GetDocumentViewer();
+ if (viewer) {
+ safeViewers.AppendObject(viewer);
+ } else if (nsCOMPtr<SessionHistoryEntry> she =
+ do_QueryInterface(mEntries[i])) {
+ nsFrameLoader* frameLoader = she->GetFrameLoader();
+ if (frameLoader) {
+ safeFrameLoaders.AppendElement(frameLoader);
+ }
+ }
+ }
+
+ // Walk the SHistory list and evict any content viewers that aren't safe.
+ // (It's important that the condition checks Length(), rather than a cached
+ // copy of Length(), because the length might change between iterations.)
+ for (int32_t i = 0; i < Length(); i++) {
+ nsCOMPtr<nsISHEntry> entry = mEntries[i];
+ nsCOMPtr<nsIDocumentViewer> viewer = entry->GetDocumentViewer();
+ if (viewer) {
+ if (safeViewers.IndexOf(viewer) == -1) {
+ EvictDocumentViewerForEntry(entry);
+ }
+ } else if (nsCOMPtr<SessionHistoryEntry> she =
+ do_QueryInterface(mEntries[i])) {
+ nsFrameLoader* frameLoader = she->GetFrameLoader();
+ if (frameLoader) {
+ if (!safeFrameLoaders.Contains(frameLoader)) {
+ EvictDocumentViewerForEntry(entry);
+ }
+ }
+ }
+ }
+}
+
+namespace {
+
+class EntryAndDistance {
+ public:
+ EntryAndDistance(nsSHistory* aSHistory, nsISHEntry* aEntry, uint32_t aDist)
+ : mSHistory(aSHistory),
+ mEntry(aEntry),
+ mViewer(aEntry->GetDocumentViewer()),
+ mLastTouched(mEntry->GetLastTouched()),
+ mDistance(aDist) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry);
+ if (she) {
+ mFrameLoader = she->GetFrameLoader();
+ }
+ NS_ASSERTION(mViewer || mFrameLoader,
+ "Entry should have a content viewer or frame loader.");
+ }
+
+ bool operator<(const EntryAndDistance& aOther) const {
+ // Compare distances first, and fall back to last-accessed times.
+ if (aOther.mDistance != this->mDistance) {
+ return this->mDistance < aOther.mDistance;
+ }
+
+ return this->mLastTouched < aOther.mLastTouched;
+ }
+
+ bool operator==(const EntryAndDistance& aOther) const {
+ // This is a little silly; we need == so the default comaprator can be
+ // instantiated, but this function is never actually called when we sort
+ // the list of EntryAndDistance objects.
+ return aOther.mDistance == this->mDistance &&
+ aOther.mLastTouched == this->mLastTouched;
+ }
+
+ RefPtr<nsSHistory> mSHistory;
+ nsCOMPtr<nsISHEntry> mEntry;
+ nsCOMPtr<nsIDocumentViewer> mViewer;
+ RefPtr<nsFrameLoader> mFrameLoader;
+ uint32_t mLastTouched;
+ int32_t mDistance;
+};
+
+} // namespace
+
+// static
+void nsSHistory::GloballyEvictDocumentViewers() {
+ // First, collect from each SHistory object the entries which have a cached
+ // content viewer. Associate with each entry its distance from its SHistory's
+ // current index.
+
+ nsTArray<EntryAndDistance> entries;
+
+ for (auto shist : gSHistoryList.mList) {
+ // Maintain a list of the entries which have viewers and belong to
+ // this particular shist object. We'll add this list to the global list,
+ // |entries|, eventually.
+ nsTArray<EntryAndDistance> shEntries;
+
+ // Content viewers are likely to exist only within shist->mIndex -/+
+ // VIEWER_WINDOW, so only search within that range.
+ //
+ // A content viewer might exist outside that range due to either:
+ //
+ // * history.pushState or hash navigations, in which case a copy of the
+ // content viewer should exist within the range, or
+ //
+ // * bugs which cause us not to call nsSHistory::EvictDocumentViewers()
+ // often enough. Once we do call EvictDocumentViewers() for the
+ // SHistory object in question, we'll do a full search of its history
+ // and evict the out-of-range content viewers, so we don't bother here.
+ //
+ int32_t startIndex, endIndex;
+ shist->WindowIndices(shist->mIndex, &startIndex, &endIndex);
+ for (int32_t i = startIndex; i <= endIndex; i++) {
+ nsCOMPtr<nsISHEntry> entry = shist->mEntries[i];
+ nsCOMPtr<nsIDocumentViewer> viewer = entry->GetDocumentViewer();
+
+ bool found = false;
+ bool hasDocumentViewerOrFrameLoader = false;
+ if (viewer) {
+ hasDocumentViewerOrFrameLoader = true;
+ // Because one content viewer might belong to multiple SHEntries, we
+ // have to search through shEntries to see if we already know
+ // about this content viewer. If we find the viewer, update its
+ // distance from the SHistory's index and continue.
+ for (uint32_t j = 0; j < shEntries.Length(); j++) {
+ EntryAndDistance& container = shEntries[j];
+ if (container.mViewer == viewer) {
+ container.mDistance =
+ std::min(container.mDistance, DeprecatedAbs(i - shist->mIndex));
+ found = true;
+ break;
+ }
+ }
+ } else if (nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(entry)) {
+ if (RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader()) {
+ hasDocumentViewerOrFrameLoader = true;
+ // Similar search as above but using frameloader.
+ for (uint32_t j = 0; j < shEntries.Length(); j++) {
+ EntryAndDistance& container = shEntries[j];
+ if (container.mFrameLoader == frameLoader) {
+ container.mDistance = std::min(container.mDistance,
+ DeprecatedAbs(i - shist->mIndex));
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // If we didn't find a EntryAndDistance for this content viewer /
+ // frameloader, make a new one.
+ if (hasDocumentViewerOrFrameLoader && !found) {
+ EntryAndDistance container(shist, entry,
+ DeprecatedAbs(i - shist->mIndex));
+ shEntries.AppendElement(container);
+ }
+ }
+
+ // We've found all the entries belonging to shist which have viewers.
+ // Add those entries to our global list and move on.
+ entries.AppendElements(shEntries);
+ }
+
+ // We now have collected all cached content viewers. First check that we
+ // have enough that we actually need to evict some.
+ if ((int32_t)entries.Length() <= sHistoryMaxTotalViewers) {
+ return;
+ }
+
+ // If we need to evict, sort our list of entries and evict the largest
+ // ones. (We could of course get better algorithmic complexity here by using
+ // a heap or something more clever. But sHistoryMaxTotalViewers isn't large,
+ // so let's not worry about it.)
+ entries.Sort();
+
+ for (int32_t i = entries.Length() - 1; i >= sHistoryMaxTotalViewers; --i) {
+ (entries[i].mSHistory)->EvictDocumentViewerForEntry(entries[i].mEntry);
+ }
+}
+
+nsresult nsSHistory::FindEntryForBFCache(SHEntrySharedParentState* aEntry,
+ nsISHEntry** aResult,
+ int32_t* aResultIndex) {
+ *aResult = nullptr;
+ *aResultIndex = -1;
+
+ int32_t startIndex, endIndex;
+ WindowIndices(mIndex, &startIndex, &endIndex);
+
+ for (int32_t i = startIndex; i <= endIndex; ++i) {
+ nsCOMPtr<nsISHEntry> shEntry = mEntries[i];
+
+ // Does shEntry have the same BFCacheEntry as the argument to this method?
+ if (shEntry->HasBFCacheEntry(aEntry)) {
+ shEntry.forget(aResult);
+ *aResultIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::EvictExpiredDocumentViewerForEntry(
+ SHEntrySharedParentState* aEntry) {
+ int32_t index;
+ nsCOMPtr<nsISHEntry> shEntry;
+ FindEntryForBFCache(aEntry, getter_AddRefs(shEntry), &index);
+
+ if (index == mIndex) {
+ NS_WARNING("How did the current SHEntry expire?");
+ }
+
+ if (shEntry) {
+ EvictDocumentViewerForEntry(shEntry);
+ }
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::AddToExpirationTracker(SHEntrySharedParentState* aEntry) {
+ RefPtr<SHEntrySharedParentState> entry = aEntry;
+ if (!mHistoryTracker || !entry) {
+ return;
+ }
+
+ mHistoryTracker->AddObject(entry);
+ return;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::RemoveFromExpirationTracker(SHEntrySharedParentState* aEntry) {
+ RefPtr<SHEntrySharedParentState> entry = aEntry;
+ MOZ_ASSERT(mHistoryTracker && !mHistoryTracker->IsEmpty());
+ if (!mHistoryTracker || !entry) {
+ return;
+ }
+
+ mHistoryTracker->RemoveObject(entry);
+}
+
+// Evicts all content viewers in all history objects. This is very
+// inefficient, because it requires a linear search through all SHistory
+// objects for each viewer to be evicted. However, this method is called
+// infrequently -- only when the disk or memory cache is cleared.
+
+// static
+void nsSHistory::GloballyEvictAllDocumentViewers() {
+ int32_t maxViewers = sHistoryMaxTotalViewers;
+ sHistoryMaxTotalViewers = 0;
+ GloballyEvictDocumentViewers();
+ sHistoryMaxTotalViewers = maxViewers;
+}
+
+void GetDynamicChildren(nsISHEntry* aEntry, nsTArray<nsID>& aDocshellIDs) {
+ int32_t count = aEntry->GetChildCount();
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISHEntry> child;
+ aEntry->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ if (child->IsDynamicallyAdded()) {
+ child->GetDocshellID(*aDocshellIDs.AppendElement());
+ } else {
+ GetDynamicChildren(child, aDocshellIDs);
+ }
+ }
+ }
+}
+
+bool RemoveFromSessionHistoryEntry(nsISHEntry* aRoot,
+ nsTArray<nsID>& aDocshellIDs) {
+ bool didRemove = false;
+ int32_t childCount = aRoot->GetChildCount();
+ for (int32_t i = childCount - 1; i >= 0; --i) {
+ nsCOMPtr<nsISHEntry> child;
+ aRoot->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ nsID docshelldID;
+ child->GetDocshellID(docshelldID);
+ if (aDocshellIDs.Contains(docshelldID)) {
+ didRemove = true;
+ aRoot->RemoveChild(child);
+ } else if (RemoveFromSessionHistoryEntry(child, aDocshellIDs)) {
+ didRemove = true;
+ }
+ }
+ }
+ return didRemove;
+}
+
+bool RemoveChildEntries(nsISHistory* aHistory, int32_t aIndex,
+ nsTArray<nsID>& aEntryIDs) {
+ nsCOMPtr<nsISHEntry> root;
+ aHistory->GetEntryAtIndex(aIndex, getter_AddRefs(root));
+ return root ? RemoveFromSessionHistoryEntry(root, aEntryIDs) : false;
+}
+
+bool IsSameTree(nsISHEntry* aEntry1, nsISHEntry* aEntry2) {
+ if (!aEntry1 && !aEntry2) {
+ return true;
+ }
+ if ((!aEntry1 && aEntry2) || (aEntry1 && !aEntry2)) {
+ return false;
+ }
+ uint32_t id1 = aEntry1->GetID();
+ uint32_t id2 = aEntry2->GetID();
+ if (id1 != id2) {
+ return false;
+ }
+
+ int32_t count1 = aEntry1->GetChildCount();
+ int32_t count2 = aEntry2->GetChildCount();
+ // We allow null entries in the end of the child list.
+ int32_t count = std::max(count1, count2);
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISHEntry> child1, child2;
+ aEntry1->GetChildAt(i, getter_AddRefs(child1));
+ aEntry2->GetChildAt(i, getter_AddRefs(child2));
+ if (!IsSameTree(child1, child2)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool nsSHistory::RemoveDuplicate(int32_t aIndex, bool aKeepNext) {
+ NS_ASSERTION(aIndex >= 0, "aIndex must be >= 0!");
+ NS_ASSERTION(aIndex != 0 || aKeepNext,
+ "If we're removing index 0 we must be keeping the next");
+ NS_ASSERTION(aIndex != mIndex, "Shouldn't remove mIndex!");
+
+ int32_t compareIndex = aKeepNext ? aIndex + 1 : aIndex - 1;
+
+ nsresult rv;
+ nsCOMPtr<nsISHEntry> root1, root2;
+ rv = GetEntryAtIndex(aIndex, getter_AddRefs(root1));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ rv = GetEntryAtIndex(compareIndex, getter_AddRefs(root2));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ SHistoryChangeNotifier change(this);
+
+ if (IsSameTree(root1, root2)) {
+ if (aIndex < compareIndex) {
+ // If we're removing the entry with the lower index we need to move its
+ // BCHistoryLength to the entry we're keeping. If we're removing the entry
+ // with the higher index then it shouldn't have a modified
+ // BCHistoryLength.
+ UpdateEntryLength(root1, root2, true);
+ }
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(root1);
+ if (she) {
+ ClearEntries(she);
+ }
+ mEntries.RemoveElementAt(aIndex);
+
+ // FIXME Bug 1546350: Reimplement history listeners.
+ // if (mRootBC && mRootBC->GetDocShell()) {
+ // static_cast<nsDocShell*>(mRootBC->GetDocShell())
+ // ->HistoryEntryRemoved(aIndex);
+ //}
+
+ // Adjust our indices to reflect the removed entry.
+ if (mIndex > aIndex) {
+ mIndex = mIndex - 1;
+ }
+
+ // NB: If the entry we are removing is the entry currently
+ // being navigated to (mRequestedIndex) then we adjust the index
+ // only if we're not keeping the next entry (because if we are keeping
+ // the next entry (because the current is a duplicate of the next), then
+ // that entry slides into the spot that we're currently pointing to.
+ // We don't do this adjustment for mIndex because mIndex cannot equal
+ // aIndex.
+
+ // NB: We don't need to guard on mRequestedIndex being nonzero here,
+ // because either they're strictly greater than aIndex which is at least
+ // zero, or they are equal to aIndex in which case aKeepNext must be true
+ // if aIndex is zero.
+ if (mRequestedIndex > aIndex || (mRequestedIndex == aIndex && !aKeepNext)) {
+ mRequestedIndex = mRequestedIndex - 1;
+ }
+
+ return true;
+ }
+ return false;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex) {
+ bool didRemove;
+ RemoveEntries(aIDs, aStartIndex, &didRemove);
+ if (didRemove) {
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (rootBC && rootBC->GetDocShell()) {
+ rootBC->GetDocShell()->DispatchLocationChangeEvent();
+ }
+ }
+}
+
+void nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex,
+ bool* aDidRemove) {
+ SHistoryChangeNotifier change(this);
+
+ int32_t index = aStartIndex;
+ while (index >= 0 && RemoveChildEntries(this, --index, aIDs)) {
+ }
+ int32_t minIndex = index;
+ index = aStartIndex;
+ while (index >= 0 && RemoveChildEntries(this, index++, aIDs)) {
+ }
+
+ // We need to remove duplicate nsSHEntry trees.
+ *aDidRemove = false;
+ while (index > minIndex) {
+ if (index != mIndex && RemoveDuplicate(index, index < mIndex)) {
+ *aDidRemove = true;
+ }
+ --index;
+ }
+
+ UpdateRootBrowsingContextState();
+}
+
+void nsSHistory::RemoveFrameEntries(nsISHEntry* aEntry) {
+ int32_t count = aEntry->GetChildCount();
+ AutoTArray<nsID, 16> ids;
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISHEntry> child;
+ aEntry->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ child->GetDocshellID(*ids.AppendElement());
+ }
+ }
+ RemoveEntries(ids, mIndex);
+}
+
+void nsSHistory::RemoveDynEntries(int32_t aIndex, nsISHEntry* aEntry) {
+ // Remove dynamic entries which are at the index and belongs to the container.
+ nsCOMPtr<nsISHEntry> entry(aEntry);
+ if (!entry) {
+ GetEntryAtIndex(aIndex, getter_AddRefs(entry));
+ }
+
+ if (entry) {
+ AutoTArray<nsID, 16> toBeRemovedEntries;
+ GetDynamicChildren(entry, toBeRemovedEntries);
+ if (toBeRemovedEntries.Length()) {
+ RemoveEntries(toBeRemovedEntries, aIndex);
+ }
+ }
+}
+
+void nsSHistory::RemoveDynEntriesForBFCacheEntry(nsIBFCacheEntry* aBFEntry) {
+ int32_t index;
+ nsCOMPtr<nsISHEntry> shEntry;
+ FindEntryForBFCache(static_cast<nsSHEntryShared*>(aBFEntry),
+ getter_AddRefs(shEntry), &index);
+ if (shEntry) {
+ RemoveDynEntries(index, shEntry);
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::UpdateIndex() {
+ SHistoryChangeNotifier change(this);
+
+ // Update the actual index with the right value.
+ if (mIndex != mRequestedIndex && mRequestedIndex != -1) {
+ mIndex = mRequestedIndex;
+ }
+
+ mRequestedIndex = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::GotoIndex(int32_t aIndex, bool aUserActivation) {
+ nsTArray<LoadEntryResult> loadResults;
+ nsresult rv = GotoIndex(aIndex, loadResults, /*aSameEpoch*/ false,
+ aIndex == mIndex, aUserActivation);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LoadURIs(loadResults);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::EnsureCorrectEntryAtCurrIndex(nsISHEntry* aEntry) {
+ int index = mRequestedIndex == -1 ? mIndex : mRequestedIndex;
+ if (index > -1 && (mEntries[index] != aEntry)) {
+ ReplaceEntry(index, aEntry);
+ }
+}
+
+nsresult nsSHistory::GotoIndex(int32_t aIndex,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aSameEpoch, bool aLoadCurrentEntry,
+ bool aUserActivation) {
+ return LoadEntry(aIndex, LOAD_HISTORY, HIST_CMD_GOTOINDEX, aLoadResults,
+ aSameEpoch, aLoadCurrentEntry, aUserActivation);
+}
+
+NS_IMETHODIMP_(bool)
+nsSHistory::HasUserInteractionAtIndex(int32_t aIndex) {
+ nsCOMPtr<nsISHEntry> entry;
+ GetEntryAtIndex(aIndex, getter_AddRefs(entry));
+ if (!entry) {
+ return false;
+ }
+ return entry->GetHasUserInteraction();
+}
+
+nsresult nsSHistory::LoadNextPossibleEntry(
+ int32_t aNewIndex, long aLoadType, uint32_t aHistCmd,
+ nsTArray<LoadEntryResult>& aLoadResults, bool aLoadCurrentEntry,
+ bool aUserActivation) {
+ mRequestedIndex = -1;
+ if (aNewIndex < mIndex) {
+ return LoadEntry(aNewIndex - 1, aLoadType, aHistCmd, aLoadResults,
+ /*aSameEpoch*/ false, aLoadCurrentEntry, aUserActivation);
+ }
+ if (aNewIndex > mIndex) {
+ return LoadEntry(aNewIndex + 1, aLoadType, aHistCmd, aLoadResults,
+ /*aSameEpoch*/ false, aLoadCurrentEntry, aUserActivation);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsSHistory::LoadEntry(int32_t aIndex, long aLoadType,
+ uint32_t aHistCmd,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aSameEpoch, bool aLoadCurrentEntry,
+ bool aUserActivation) {
+ MOZ_LOG(gSHistoryLog, LogLevel::Debug,
+ ("LoadEntry(%d, 0x%lx, %u)", aIndex, aLoadType, aHistCmd));
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (!rootBC) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aIndex < 0 || aIndex >= Length()) {
+ MOZ_LOG(gSHistoryLog, LogLevel::Debug, ("Index out of range"));
+ // The index is out of range.
+ // Clear the requested index in case it had bogus value. This way the next
+ // load succeeds if the offset is reasonable.
+ mRequestedIndex = -1;
+
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t originalRequestedIndex = mRequestedIndex;
+ int32_t previousRequest = mRequestedIndex > -1 ? mRequestedIndex : mIndex;
+ int32_t requestedOffset = aIndex - previousRequest;
+
+ // This is a normal local history navigation.
+
+ nsCOMPtr<nsISHEntry> prevEntry;
+ nsCOMPtr<nsISHEntry> nextEntry;
+ GetEntryAtIndex(mIndex, getter_AddRefs(prevEntry));
+ GetEntryAtIndex(aIndex, getter_AddRefs(nextEntry));
+ if (!nextEntry || !prevEntry) {
+ mRequestedIndex = -1;
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mozilla::SessionHistoryInParent()) {
+ if (aHistCmd == HIST_CMD_GOTOINDEX) {
+ // https://html.spec.whatwg.org/#history-traversal:
+ // To traverse the history
+ // "If entry has a different Document object than the current entry, then
+ // run the following substeps: Remove any tasks queued by the history
+ // traversal task source..."
+ //
+ // aSameEpoch is true only if the navigations would have been
+ // generated in the same spin of the event loop (i.e. history.go(-2);
+ // history.go(-1)) and from the same content process.
+ if (aSameEpoch) {
+ bool same_doc = false;
+ prevEntry->SharesDocumentWith(nextEntry, &same_doc);
+ if (!same_doc) {
+ MOZ_LOG(
+ gSHistoryLog, LogLevel::Debug,
+ ("Aborting GotoIndex %d - same epoch and not same doc", aIndex));
+ // Ignore this load. Before SessionHistoryInParent, this would
+ // have been dropped in InternalLoad after we filter out SameDoc
+ // loads.
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ }
+ // Keep note of requested history index in mRequestedIndex; after all bailouts
+ mRequestedIndex = aIndex;
+
+ // Remember that this entry is getting loaded at this point in the sequence
+
+ nextEntry->SetLastTouched(++gTouchCounter);
+
+ // Get the uri for the entry we are about to visit
+ nsCOMPtr<nsIURI> nextURI = nextEntry->GetURI();
+
+ MOZ_ASSERT(nextURI, "nextURI can't be null");
+
+ // Send appropriate listener notifications.
+ if (aHistCmd == HIST_CMD_GOTOINDEX) {
+ // We are going somewhere else. This is not reload either
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryGotoIndex(); });
+ }
+
+ if (mRequestedIndex == mIndex) {
+ // Possibly a reload case
+ InitiateLoad(nextEntry, rootBC, aLoadType, aLoadResults, aLoadCurrentEntry,
+ aUserActivation, requestedOffset);
+ return NS_OK;
+ }
+
+ // Going back or forward.
+ bool differenceFound = LoadDifferingEntries(
+ prevEntry, nextEntry, rootBC, aLoadType, aLoadResults, aLoadCurrentEntry,
+ aUserActivation, requestedOffset);
+ if (!differenceFound) {
+ // LoadNextPossibleEntry will change the offset by one, and in order
+ // to keep track of the requestedOffset, need to reset mRequestedIndex to
+ // the value it had initially.
+ mRequestedIndex = originalRequestedIndex;
+ // We did not find any differences. Go further in the history.
+ return LoadNextPossibleEntry(aIndex, aLoadType, aHistCmd, aLoadResults,
+ aLoadCurrentEntry, aUserActivation);
+ }
+
+ return NS_OK;
+}
+
+bool nsSHistory::LoadDifferingEntries(nsISHEntry* aPrevEntry,
+ nsISHEntry* aNextEntry,
+ BrowsingContext* aParent, long aLoadType,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aLoadCurrentEntry,
+ bool aUserActivation, int32_t aOffset) {
+ MOZ_ASSERT(aPrevEntry && aNextEntry && aParent);
+
+ uint32_t prevID = aPrevEntry->GetID();
+ uint32_t nextID = aNextEntry->GetID();
+
+ // Check the IDs to verify if the pages are different.
+ if (prevID != nextID) {
+ // Set the Subframe flag if not navigating the root docshell.
+ aNextEntry->SetIsSubFrame(aParent->Id() != mRootBC);
+ InitiateLoad(aNextEntry, aParent, aLoadType, aLoadResults,
+ aLoadCurrentEntry, aUserActivation, aOffset);
+ return true;
+ }
+
+ // The entries are the same, so compare any child frames
+ int32_t pcnt = aPrevEntry->GetChildCount();
+ int32_t ncnt = aNextEntry->GetChildCount();
+
+ // Create an array for child browsing contexts.
+ nsTArray<RefPtr<BrowsingContext>> browsingContexts;
+ aParent->GetChildren(browsingContexts);
+
+ // Search for something to load next.
+ bool differenceFound = false;
+ for (int32_t i = 0; i < ncnt; ++i) {
+ // First get an entry which may cause a new page to be loaded.
+ nsCOMPtr<nsISHEntry> nChild;
+ aNextEntry->GetChildAt(i, getter_AddRefs(nChild));
+ if (!nChild) {
+ continue;
+ }
+ nsID docshellID;
+ nChild->GetDocshellID(docshellID);
+
+ // Then find the associated docshell.
+ RefPtr<BrowsingContext> bcChild;
+ for (const RefPtr<BrowsingContext>& bc : browsingContexts) {
+ if (bc->GetHistoryID() == docshellID) {
+ bcChild = bc;
+ break;
+ }
+ }
+ if (!bcChild) {
+ continue;
+ }
+
+ // Then look at the previous entries to see if there was
+ // an entry for the docshell.
+ nsCOMPtr<nsISHEntry> pChild;
+ for (int32_t k = 0; k < pcnt; ++k) {
+ nsCOMPtr<nsISHEntry> child;
+ aPrevEntry->GetChildAt(k, getter_AddRefs(child));
+ if (child) {
+ nsID dID;
+ child->GetDocshellID(dID);
+ if (dID == docshellID) {
+ pChild = child;
+ break;
+ }
+ }
+ }
+ if (!pChild) {
+ continue;
+ }
+
+ // Finally recursively call this method.
+ // This will either load a new page to shell or some subshell or
+ // do nothing.
+ if (LoadDifferingEntries(pChild, nChild, bcChild, aLoadType, aLoadResults,
+ aLoadCurrentEntry, aUserActivation, aOffset)) {
+ differenceFound = true;
+ }
+ }
+ return differenceFound;
+}
+
+void nsSHistory::InitiateLoad(nsISHEntry* aFrameEntry,
+ BrowsingContext* aFrameBC, long aLoadType,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aLoadCurrentEntry, bool aUserActivation,
+ int32_t aOffset) {
+ MOZ_ASSERT(aFrameBC && aFrameEntry);
+
+ LoadEntryResult* loadResult = aLoadResults.AppendElement();
+ loadResult->mBrowsingContext = aFrameBC;
+
+ nsCOMPtr<nsIURI> newURI = aFrameEntry->GetURI();
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(newURI);
+
+ loadState->SetHasValidUserGestureActivation(aUserActivation);
+
+ // At the time we initiate a history entry load we already know if https-first
+ // was able to upgrade the request from http to https. There is no point in
+ // re-retrying to upgrade.
+ loadState->SetIsExemptFromHTTPSFirstMode(true);
+
+ /* Set the loadType in the SHEntry too to what was passed on.
+ * This will be passed on to child subframes later in nsDocShell,
+ * so that proper loadType is maintained through out a frameset
+ */
+ aFrameEntry->SetLoadType(aLoadType);
+
+ loadState->SetLoadType(aLoadType);
+
+ loadState->SetSHEntry(aFrameEntry);
+
+ // If we're loading the current entry we want to treat it as not a
+ // same-document navigation (see nsDocShell::IsSameDocumentNavigation), so
+ // record that here in the LoadingSessionHistoryEntry.
+ bool loadingCurrentEntry;
+ if (mozilla::SessionHistoryInParent()) {
+ loadingCurrentEntry = aLoadCurrentEntry;
+ } else {
+ loadingCurrentEntry =
+ aFrameBC->GetDocShell() &&
+ nsDocShell::Cast(aFrameBC->GetDocShell())->IsOSHE(aFrameEntry);
+ }
+ loadState->SetLoadIsFromSessionHistory(aOffset, loadingCurrentEntry);
+
+ if (mozilla::SessionHistoryInParent()) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aFrameEntry);
+ aFrameBC->Canonical()->AddLoadingSessionHistoryEntry(
+ loadState->GetLoadingSessionHistoryInfo()->mLoadId, she);
+ }
+
+ nsCOMPtr<nsIURI> originalURI = aFrameEntry->GetOriginalURI();
+ loadState->SetOriginalURI(originalURI);
+
+ loadState->SetLoadReplace(aFrameEntry->GetLoadReplace());
+
+ loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE);
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal =
+ aFrameEntry->GetTriggeringPrincipal();
+ loadState->SetTriggeringPrincipal(triggeringPrincipal);
+ loadState->SetFirstParty(false);
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aFrameEntry->GetCsp();
+ loadState->SetCsp(csp);
+
+ loadResult->mLoadState = std::move(loadState);
+}
+
+NS_IMETHODIMP
+nsSHistory::CreateEntry(nsISHEntry** aEntry) {
+ nsCOMPtr<nsISHEntry> entry;
+ if (XRE_IsParentProcess() && mozilla::SessionHistoryInParent()) {
+ entry = new SessionHistoryEntry();
+ } else {
+ entry = new nsSHEntry();
+ }
+ entry.forget(aEntry);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(bool)
+nsSHistory::IsEmptyOrHasEntriesForSingleTopLevelPage() {
+ if (mEntries.IsEmpty()) {
+ return true;
+ }
+
+ nsISHEntry* entry = mEntries[0];
+ size_t length = mEntries.Length();
+ for (size_t i = 1; i < length; ++i) {
+ bool sharesDocument = false;
+ mEntries[i]->SharesDocumentWith(entry, &sharesDocument);
+ if (!sharesDocument) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void CollectEntries(
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable,
+ SessionHistoryEntry* aEntry) {
+ aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry);
+ for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) {
+ if (entry) {
+ CollectEntries(aHashtable, entry);
+ }
+ }
+}
+
+static void UpdateEntryLength(
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable,
+ SessionHistoryEntry* aNewEntry, bool aMove) {
+ SessionHistoryEntry* oldEntry = aHashtable.Get(aNewEntry->DocshellID());
+ if (oldEntry) {
+ MOZ_ASSERT(oldEntry->GetID() != aNewEntry->GetID() || !aMove ||
+ !aNewEntry->BCHistoryLength().Modified());
+ aNewEntry->SetBCHistoryLength(oldEntry->BCHistoryLength());
+ if (oldEntry->GetID() != aNewEntry->GetID()) {
+ MOZ_ASSERT(!aMove);
+ // If we have a new id then aNewEntry was created for a new load, so
+ // record that in BCHistoryLength.
+ ++aNewEntry->BCHistoryLength();
+ } else if (aMove) {
+ // We're moving the BCHistoryLength from the old entry to the new entry,
+ // so we need to let the old entry know that it's not contributing to its
+ // BCHistoryLength, and the new one that it does if the old one was
+ // before.
+ aNewEntry->BCHistoryLength().SetModified(
+ oldEntry->BCHistoryLength().Modified());
+ oldEntry->BCHistoryLength().SetModified(false);
+ }
+ }
+
+ for (const RefPtr<SessionHistoryEntry>& entry : aNewEntry->Children()) {
+ if (entry) {
+ UpdateEntryLength(aHashtable, entry, aMove);
+ }
+ }
+}
+
+void nsSHistory::UpdateEntryLength(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry,
+ bool aMove) {
+ nsCOMPtr<SessionHistoryEntry> oldSHE = do_QueryInterface(aOldEntry);
+ nsCOMPtr<SessionHistoryEntry> newSHE = do_QueryInterface(aNewEntry);
+
+ if (!oldSHE || !newSHE) {
+ return;
+ }
+
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*> docshellIDToEntry;
+ CollectEntries(docshellIDToEntry, oldSHE);
+
+ ::UpdateEntryLength(docshellIDToEntry, newSHE, aMove);
+}
diff --git a/docshell/shistory/nsSHistory.h b/docshell/shistory/nsSHistory.h
new file mode 100644
index 0000000000..97bbe7f0a9
--- /dev/null
+++ b/docshell/shistory/nsSHistory.h
@@ -0,0 +1,343 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSHistory_h
+#define nsSHistory_h
+
+#include "nsCOMPtr.h"
+#include "nsDocShellLoadState.h"
+#include "nsExpirationTracker.h"
+#include "nsISHistory.h"
+#include "nsSHEntryShared.h"
+#include "nsSimpleEnumerator.h"
+#include "nsTObserverArray.h"
+#include "nsWeakReference.h"
+
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/UniquePtr.h"
+
+class nsIDocShell;
+class nsDocShell;
+class nsSHistoryObserver;
+class nsISHEntry;
+
+namespace mozilla {
+namespace dom {
+class LoadSHEntryResult;
+}
+} // namespace mozilla
+
+class nsSHistory : public mozilla::LinkedListElement<nsSHistory>,
+ public nsISHistory,
+ public nsSupportsWeakReference {
+ public:
+ // The timer based history tracker is used to evict bfcache on expiration.
+ class HistoryTracker final
+ : public nsExpirationTracker<mozilla::dom::SHEntrySharedParentState, 3> {
+ public:
+ explicit HistoryTracker(nsSHistory* aSHistory, uint32_t aTimeout,
+ nsIEventTarget* aEventTarget)
+ : nsExpirationTracker(1000 * aTimeout / 2, "HistoryTracker",
+ aEventTarget) {
+ MOZ_ASSERT(aSHistory);
+ mSHistory = aSHistory;
+ }
+
+ protected:
+ virtual void NotifyExpired(
+ mozilla::dom::SHEntrySharedParentState* aObj) override {
+ RemoveObject(aObj);
+ mSHistory->EvictExpiredDocumentViewerForEntry(aObj);
+ }
+
+ private:
+ // HistoryTracker is owned by nsSHistory; it always outlives HistoryTracker
+ // so it's safe to use raw pointer here.
+ nsSHistory* mSHistory;
+ };
+
+ // Structure used in SetChildHistoryEntry
+ struct SwapEntriesData {
+ mozilla::dom::BrowsingContext*
+ ignoreBC; // constant; the browsing context to ignore
+ nsISHEntry* destTreeRoot; // constant; the root of the dest tree
+ nsISHEntry* destTreeParent; // constant; the node under destTreeRoot
+ // whose children will correspond to aEntry
+ };
+
+ explicit nsSHistory(mozilla::dom::BrowsingContext* aRootBC);
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHISTORY
+
+ // One time initialization method
+ static nsresult Startup();
+ static void Shutdown();
+ static void UpdatePrefs();
+
+ // Max number of total cached content viewers. If the pref
+ // browser.sessionhistory.max_total_viewers is negative, then
+ // this value is calculated based on the total amount of memory.
+ // Otherwise, it comes straight from the pref.
+ static uint32_t GetMaxTotalViewers() { return sHistoryMaxTotalViewers; }
+
+ // Get the root SHEntry from a given entry.
+ static already_AddRefed<nsISHEntry> GetRootSHEntry(nsISHEntry* aEntry);
+
+ // Callback prototype for WalkHistoryEntries.
+ // `aEntry` is the child history entry, `aBC` is its corresponding browsing
+ // context, `aChildIndex` is the child's index in its parent entry, and
+ // `aData` is the opaque pointer passed to WalkHistoryEntries. Both structs
+ // that are passed as `aData` to this function have a field
+ // `aEntriesToUpdate`, which is an array of entries we need to update in
+ // docshell, if the 'SH in parent' pref is on (which implies that this method
+ // is executed in the parent)
+ typedef nsresult (*WalkHistoryEntriesFunc)(nsISHEntry* aEntry,
+ mozilla::dom::BrowsingContext* aBC,
+ int32_t aChildIndex, void* aData);
+
+ // Clone a session history tree for subframe navigation.
+ // The tree rooted at |aSrcEntry| will be cloned into |aDestEntry|, except
+ // for the entry with id |aCloneID|, which will be replaced with
+ // |aReplaceEntry|. |aSrcShell| is a (possibly null) docshell which
+ // corresponds to |aSrcEntry| via its mLSHE or mOHE pointers, and will
+ // have that pointer updated to point to the cloned history entry.
+ // If aCloneChildren is true then the children of the entry with id
+ // |aCloneID| will be cloned into |aReplaceEntry|.
+ static nsresult CloneAndReplace(nsISHEntry* aSrcEntry,
+ mozilla::dom::BrowsingContext* aOwnerBC,
+ uint32_t aCloneID, nsISHEntry* aReplaceEntry,
+ bool aCloneChildren, nsISHEntry** aDestEntry);
+
+ // Child-walking callback for CloneAndReplace
+ static nsresult CloneAndReplaceChild(nsISHEntry* aEntry,
+ mozilla::dom::BrowsingContext* aOwnerBC,
+ int32_t aChildIndex, void* aData);
+
+ // Child-walking callback for SetHistoryEntry
+ static nsresult SetChildHistoryEntry(nsISHEntry* aEntry,
+ mozilla::dom::BrowsingContext* aBC,
+ int32_t aEntryIndex, void* aData);
+
+ // For each child of aRootEntry, find the corresponding shell which is
+ // a child of aBC, and call aCallback. The opaque pointer aData
+ // is passed to the callback.
+ static nsresult WalkHistoryEntries(nsISHEntry* aRootEntry,
+ mozilla::dom::BrowsingContext* aBC,
+ WalkHistoryEntriesFunc aCallback,
+ void* aData);
+
+ // This function finds all entries that are contiguous and same-origin with
+ // the aEntry. And call the aCallback on them, including the aEntry. This only
+ // works for the root entries. It will do nothing for non-root entries.
+ static void WalkContiguousEntries(
+ nsISHEntry* aEntry, const std::function<void(nsISHEntry*)>& aCallback);
+
+ nsTArray<nsCOMPtr<nsISHEntry>>& Entries() { return mEntries; }
+
+ void NotifyOnHistoryReplaceEntry();
+
+ void RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex,
+ bool* aDidRemove);
+
+ // The size of the window of SHEntries which can have alive viewers in the
+ // bfcache around the currently active SHEntry.
+ //
+ // We try to keep viewers for SHEntries between index - VIEWER_WINDOW and
+ // index + VIEWER_WINDOW alive.
+ static const int32_t VIEWER_WINDOW = 3;
+
+ struct LoadEntryResult {
+ RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext;
+ RefPtr<nsDocShellLoadState> mLoadState;
+ };
+
+ static void LoadURIs(nsTArray<LoadEntryResult>& aLoadResults);
+ static void LoadURIOrBFCache(LoadEntryResult& aLoadEntry);
+
+ // If this doesn't return an error then either aLoadResult is set to nothing,
+ // in which case the caller should ignore the load, or it returns a valid
+ // LoadEntryResult in aLoadResult which the caller should use to do the load.
+ nsresult Reload(uint32_t aReloadFlags,
+ nsTArray<LoadEntryResult>& aLoadResults);
+ nsresult ReloadCurrentEntry(nsTArray<LoadEntryResult>& aLoadResults);
+ nsresult GotoIndex(int32_t aIndex, nsTArray<LoadEntryResult>& aLoadResults,
+ bool aSameEpoch, bool aLoadCurrentEntry,
+ bool aUserActivation);
+
+ void WindowIndices(int32_t aIndex, int32_t* aOutStartIndex,
+ int32_t* aOutEndIndex);
+ void NotifyListenersDocumentViewerEvicted(uint32_t aNumEvicted);
+
+ int32_t Length() { return int32_t(mEntries.Length()); }
+ int32_t Index() { return mIndex; }
+ already_AddRefed<mozilla::dom::BrowsingContext> GetBrowsingContext() {
+ return mozilla::dom::BrowsingContext::Get(mRootBC);
+ }
+ bool HasOngoingUpdate() { return mHasOngoingUpdate; }
+ void SetHasOngoingUpdate(bool aVal) { mHasOngoingUpdate = aVal; }
+
+ void SetBrowsingContext(mozilla::dom::BrowsingContext* aRootBC) {
+ uint64_t newID = aRootBC ? aRootBC->Id() : 0;
+ if (mRootBC != newID) {
+ mRootBC = newID;
+ UpdateRootBrowsingContextState(aRootBC);
+ }
+ }
+
+ int32_t GetIndexForReplace() {
+ // Replace current entry in session history; If the requested index is
+ // valid, it indicates the loading was triggered by a history load, and
+ // we should replace the entry at requested index instead.
+ return mRequestedIndex == -1 ? mIndex : mRequestedIndex;
+ }
+
+ // Update the root browsing context state when adding, removing or
+ // replacing entries.
+ void UpdateRootBrowsingContextState() {
+ RefPtr<mozilla::dom::BrowsingContext> rootBC(GetBrowsingContext());
+ UpdateRootBrowsingContextState(rootBC);
+ }
+
+ void GetEpoch(uint64_t& aEpoch,
+ mozilla::Maybe<mozilla::dom::ContentParentId>& aId) const {
+ aEpoch = mEpoch;
+ aId = mEpochParentId;
+ }
+ void SetEpoch(uint64_t aEpoch,
+ mozilla::Maybe<mozilla::dom::ContentParentId> aId) {
+ mEpoch = aEpoch;
+ mEpochParentId = aId;
+ }
+
+ void LogHistory();
+
+ protected:
+ virtual ~nsSHistory();
+
+ uint64_t mRootBC;
+
+ private:
+ friend class nsSHistoryObserver;
+
+ void UpdateRootBrowsingContextState(
+ mozilla::dom::BrowsingContext* aBrowsingContext);
+
+ bool LoadDifferingEntries(nsISHEntry* aPrevEntry, nsISHEntry* aNextEntry,
+ mozilla::dom::BrowsingContext* aParent,
+ long aLoadType,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aLoadCurrentEntry, bool aUserActivation,
+ int32_t aOffset);
+ void InitiateLoad(nsISHEntry* aFrameEntry,
+ mozilla::dom::BrowsingContext* aFrameBC, long aLoadType,
+ nsTArray<LoadEntryResult>& aLoadResult,
+ bool aLoadCurrentEntry, bool aUserActivation,
+ int32_t aOffset);
+
+ nsresult LoadEntry(int32_t aIndex, long aLoadType, uint32_t aHistCmd,
+ nsTArray<LoadEntryResult>& aLoadResults, bool aSameEpoch,
+ bool aLoadCurrentEntry, bool aUserActivation);
+
+ // Find the history entry for a given bfcache entry. It only looks up between
+ // the range where alive viewers may exist (i.e nsSHistory::VIEWER_WINDOW).
+ nsresult FindEntryForBFCache(mozilla::dom::SHEntrySharedParentState* aEntry,
+ nsISHEntry** aResult, int32_t* aResultIndex);
+
+ // Evict content viewers in this window which don't lie in the "safe" range
+ // around aIndex.
+ virtual void EvictOutOfRangeWindowDocumentViewers(int32_t aIndex);
+ void EvictDocumentViewerForEntry(nsISHEntry* aEntry);
+ static void GloballyEvictDocumentViewers();
+ static void GloballyEvictAllDocumentViewers();
+
+ // Calculates a max number of total
+ // content viewers to cache, based on amount of total memory
+ static uint32_t CalcMaxTotalViewers();
+
+ nsresult LoadNextPossibleEntry(int32_t aNewIndex, long aLoadType,
+ uint32_t aHistCmd,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aLoadCurrentEntry, bool aUserActivation);
+
+ // aIndex is the index of the entry which may be removed.
+ // If aKeepNext is true, aIndex is compared to aIndex + 1,
+ // otherwise comparison is done to aIndex - 1.
+ bool RemoveDuplicate(int32_t aIndex, bool aKeepNext);
+
+ // We need to update entries in docshell and browsing context.
+ // If our docshell is located in parent or 'SH in parent' pref is off we can
+ // update it directly, Otherwise, we have two choices. If the browsing context
+ // that owns the docshell is in the same process as the process who called us
+ // over IPC, then we save entries that need to be updated in a list, and once
+ // we have returned from the IPC call, we update the docshell in the child
+ // process. Otherwise, if the browsing context is in a different process, we
+ // do a nested IPC call to that process to update the docshell in that
+ // process.
+ static void HandleEntriesToSwapInDocShell(mozilla::dom::BrowsingContext* aBC,
+ nsISHEntry* aOldEntry,
+ nsISHEntry* aNewEntry);
+
+ void UpdateEntryLength(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry,
+ bool aMove);
+
+ protected:
+ bool mHasOngoingUpdate;
+ nsTArray<nsCOMPtr<nsISHEntry>> mEntries; // entries are never null
+ private:
+ // Track all bfcache entries and evict on expiration.
+ mozilla::UniquePtr<HistoryTracker> mHistoryTracker;
+
+ int32_t mIndex; // -1 means "no index"
+ int32_t mRequestedIndex; // -1 means "no requested index"
+
+ // Session History listeners
+ nsAutoTObserverArray<nsWeakPtr, 2> mListeners;
+
+ nsID mRootDocShellID;
+
+ // Max viewers allowed total, across all SHistory objects
+ static int32_t sHistoryMaxTotalViewers;
+
+ // The epoch (and id) tell us what navigations occured within the same
+ // event-loop spin in the child. We need to know this in order to
+ // implement spec requirements for dropping pending navigations when we
+ // do a history navigation, if it's not same-document. Content processes
+ // update the epoch via a runnable on each ::Go (including AsyncGo).
+ uint64_t mEpoch = 0;
+ mozilla::Maybe<mozilla::dom::ContentParentId> mEpochParentId;
+};
+
+// CallerWillNotifyHistoryIndexAndLengthChanges is used to prevent
+// SHistoryChangeNotifier to send automatic index and length updates.
+// When that is done, it is up to the caller to explicitly send those updates.
+// This is needed in cases when the update is a reaction to some change in a
+// child process and child process passes a changeId to the parent side.
+class MOZ_STACK_CLASS CallerWillNotifyHistoryIndexAndLengthChanges {
+ public:
+ explicit CallerWillNotifyHistoryIndexAndLengthChanges(
+ nsISHistory* aSHistory) {
+ nsSHistory* shistory = static_cast<nsSHistory*>(aSHistory);
+ if (shistory && !shistory->HasOngoingUpdate()) {
+ shistory->SetHasOngoingUpdate(true);
+ mSHistory = shistory;
+ }
+ }
+
+ ~CallerWillNotifyHistoryIndexAndLengthChanges() {
+ if (mSHistory) {
+ mSHistory->SetHasOngoingUpdate(false);
+ }
+ }
+
+ RefPtr<nsSHistory> mSHistory;
+};
+
+inline nsISupports* ToSupports(nsSHistory* aObj) {
+ return static_cast<nsISHistory*>(aObj);
+}
+
+#endif /* nsSHistory */
diff --git a/docshell/test/browser/Bug1622420Child.sys.mjs b/docshell/test/browser/Bug1622420Child.sys.mjs
new file mode 100644
index 0000000000..c5520d5943
--- /dev/null
+++ b/docshell/test/browser/Bug1622420Child.sys.mjs
@@ -0,0 +1,9 @@
+export class Bug1622420Child extends JSWindowActorChild {
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "hasWindowContextForTopBC":
+ return !!this.browsingContext.top.currentWindowContext;
+ }
+ return null;
+ }
+}
diff --git a/docshell/test/browser/Bug422543Child.sys.mjs b/docshell/test/browser/Bug422543Child.sys.mjs
new file mode 100644
index 0000000000..524ac33ffd
--- /dev/null
+++ b/docshell/test/browser/Bug422543Child.sys.mjs
@@ -0,0 +1,98 @@
+class SHistoryListener {
+ constructor() {
+ this.retval = true;
+ this.last = "initial";
+ }
+
+ OnHistoryNewEntry(aNewURI) {
+ this.last = "newentry";
+ }
+
+ OnHistoryGotoIndex() {
+ this.last = "gotoindex";
+ }
+
+ OnHistoryPurge() {
+ this.last = "purge";
+ }
+
+ OnHistoryReload() {
+ this.last = "reload";
+ return this.retval;
+ }
+
+ OnHistoryReplaceEntry() {}
+}
+SHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+]);
+
+let listeners;
+
+export class Bug422543Child extends JSWindowActorChild {
+ constructor() {
+ super();
+ }
+
+ actorCreated() {
+ if (listeners) {
+ return;
+ }
+
+ this.shistory = this.docShell.nsIWebNavigation.sessionHistory;
+ listeners = [new SHistoryListener(), new SHistoryListener()];
+
+ for (let listener of listeners) {
+ this.shistory.legacySHistory.addSHistoryListener(listener);
+ }
+ }
+
+ cleanup() {
+ for (let listener of listeners) {
+ this.shistory.legacySHistory.removeSHistoryListener(listener);
+ }
+ this.shistory = null;
+ listeners = null;
+ return {};
+ }
+
+ getListenerStatus() {
+ return listeners.map(l => l.last);
+ }
+
+ resetListeners() {
+ for (let listener of listeners) {
+ listener.last = "initial";
+ }
+
+ return {};
+ }
+
+ notifyReload() {
+ let history = this.shistory.legacySHistory;
+ let rval = history.notifyOnHistoryReload();
+ return { rval };
+ }
+
+ setRetval({ num, val }) {
+ listeners[num].retval = val;
+ return {};
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "cleanup":
+ return this.cleanup();
+ case "getListenerStatus":
+ return this.getListenerStatus();
+ case "notifyReload":
+ return this.notifyReload();
+ case "resetListeners":
+ return this.resetListeners();
+ case "setRetval":
+ return this.setRetval(msg.data);
+ }
+ return null;
+ }
+}
diff --git a/docshell/test/browser/browser.toml b/docshell/test/browser/browser.toml
new file mode 100644
index 0000000000..bcda46fd2e
--- /dev/null
+++ b/docshell/test/browser/browser.toml
@@ -0,0 +1,326 @@
+[DEFAULT]
+support-files = [
+ "Bug422543Child.sys.mjs",
+ "dummy_page.html",
+ "favicon_bug655270.ico",
+ "file_bug234628-1-child.html",
+ "file_bug234628-1.html",
+ "file_bug234628-10-child.xhtml",
+ "file_bug234628-10.html",
+ "file_bug234628-11-child.xhtml",
+ "file_bug234628-11-child.xhtml^headers^",
+ "file_bug234628-11.html",
+ "file_bug234628-2-child.html",
+ "file_bug234628-2.html",
+ "file_bug234628-3-child.html",
+ "file_bug234628-3.html",
+ "file_bug234628-4-child.html",
+ "file_bug234628-4.html",
+ "file_bug234628-5-child.html",
+ "file_bug234628-5.html",
+ "file_bug234628-6-child.html",
+ "file_bug234628-6-child.html^headers^",
+ "file_bug234628-6.html",
+ "file_bug234628-8-child.html",
+ "file_bug234628-8.html",
+ "file_bug234628-9-child.html",
+ "file_bug234628-9.html",
+ "file_bug420605.html",
+ "file_bug503832.html",
+ "file_bug655270.html",
+ "file_bug670318.html",
+ "file_bug673087-1.html",
+ "file_bug673087-1.html^headers^",
+ "file_bug673087-1-child.html",
+ "file_bug673087-2.html",
+ "file_bug852909.pdf",
+ "file_bug852909.png",
+ "file_bug1046022.html",
+ "file_bug1206879.html",
+ "file_bug1328501.html",
+ "file_bug1328501_frame.html",
+ "file_bug1328501_framescript.js",
+ "file_bug1543077-3-child.html",
+ "file_bug1543077-3.html",
+ "file_multiple_pushState.html",
+ "file_onbeforeunload_0.html",
+ "file_onbeforeunload_1.html",
+ "file_onbeforeunload_2.html",
+ "file_onbeforeunload_3.html",
+ "print_postdata.sjs",
+ "test-form_sjis.html",
+ "head.js",
+ "file_data_load_inherit_csp.html",
+ "file_click_link_within_view_source.html",
+ "onload_message.html",
+ "onpageshow_message.html",
+ "file_cross_process_csp_inheritance.html",
+ "file_open_about_blank.html",
+ "file_slow_load.sjs",
+ "file_bug1648464-1.html",
+ "file_bug1648464-1-child.html",
+ "file_bug1688368-1.sjs",
+ "file_bug1691153.html",
+ "file_bug1716290-1.sjs",
+ "file_bug1716290-2.sjs",
+ "file_bug1716290-3.sjs",
+ "file_bug1716290-4.sjs",
+ "file_bug1736248-1.html",
+]
+
+["browser_alternate_fixup_middle_click_link.js"]
+https_first_disabled = true
+
+["browser_backforward_restore_scroll.js"]
+https_first_disabled = true
+support-files = [
+ "file_backforward_restore_scroll.html",
+ "file_backforward_restore_scroll.html^headers^",
+]
+
+["browser_backforward_userinteraction.js"]
+support-files = ["dummy_iframe_page.html"]
+skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1607713
+
+["browser_backforward_userinteraction_about.js"]
+
+["browser_backforward_userinteraction_systemprincipal.js"]
+
+["browser_badCertDomainFixup.js"]
+
+["browser_bfcache_copycommand.js"]
+skip-if = ["os == 'linux' && bits == 64"] # Bug 1730593
+
+["browser_browsingContext-01.js"]
+https_first_disabled = true
+
+["browser_browsingContext-02.js"]
+https_first_disabled = true
+
+["browser_browsingContext-embedder.js"]
+
+["browser_browsingContext-getAllBrowsingContextsInSubtree.js"]
+
+["browser_browsingContext-getWindowByName.js"]
+
+["browser_browsingContext-webProgress.js"]
+skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1721261
+https_first_disabled = true
+
+["browser_browsing_context_attached.js"]
+https_first_disabled = true
+
+["browser_browsing_context_discarded.js"]
+https_first_disabled = true
+
+["browser_bug92473.js"]
+
+["browser_bug134911.js"]
+
+["browser_bug234628-1.js"]
+
+["browser_bug234628-10.js"]
+
+["browser_bug234628-11.js"]
+
+["browser_bug234628-2.js"]
+
+["browser_bug234628-3.js"]
+
+["browser_bug234628-4.js"]
+
+["browser_bug234628-5.js"]
+
+["browser_bug234628-6.js"]
+
+["browser_bug234628-8.js"]
+
+["browser_bug234628-9.js"]
+
+["browser_bug349769.js"]
+
+["browser_bug388121-1.js"]
+
+["browser_bug388121-2.js"]
+
+["browser_bug420605.js"]
+skip-if = ["verify"]
+
+["browser_bug422543.js"]
+https_first_disabled = true
+
+["browser_bug441169.js"]
+
+["browser_bug503832.js"]
+skip-if = ["verify"]
+
+["browser_bug554155.js"]
+
+["browser_bug655270.js"]
+
+["browser_bug655273.js"]
+
+["browser_bug670318.js"]
+skip-if = ["os == 'linux' && (debug || asan || tsan)"] # Bug 1717403
+
+["browser_bug673087-1.js"]
+
+["browser_bug673087-2.js"]
+
+["browser_bug673467.js"]
+
+["browser_bug852909.js"]
+skip-if = ["verify && debug && os == 'win'"]
+
+["browser_bug1206879.js"]
+https_first_disabled = true
+
+["browser_bug1309900_crossProcessHistoryNavigation.js"]
+https_first_disabled = true
+
+["browser_bug1328501.js"]
+https_first_disabled = true
+
+["browser_bug1347823.js"]
+
+["browser_bug1415918_beforeunload_options.js"]
+https_first_disabled = true
+
+["browser_bug1543077-3.js"]
+
+["browser_bug1594938.js"]
+
+["browser_bug1622420.js"]
+support-files = [
+ "file_bug1622420.html",
+ "Bug1622420Child.sys.mjs",
+]
+
+["browser_bug1648464-1.js"]
+
+["browser_bug1673702.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'linux' && bits == 64 && os_version == '18.04' && debug", # Bug 1674513
+ "os == 'win'", # Bug 1674513
+]
+support-files = [
+ "file_bug1673702.json",
+ "file_bug1673702.json^headers^",
+]
+
+["browser_bug1674464.js"]
+https_first_disabled = true
+skip-if = [
+ "!fission",
+ "!crashreporter", # On a crash we only keep history when fission is enabled.
+]
+
+["browser_bug1688368-1.js"]
+
+["browser_bug1691153.js"]
+https_first_disabled = true
+
+["browser_bug1705872.js"]
+
+["browser_bug1716290-1.js"]
+
+["browser_bug1716290-2.js"]
+
+["browser_bug1716290-3.js"]
+
+["browser_bug1716290-4.js"]
+
+["browser_bug1719178.js"]
+
+["browser_bug1736248-1.js"]
+
+["browser_bug1757005.js"]
+
+["browser_bug1769189.js"]
+
+["browser_bug1798780.js"]
+
+["browser_click_link_within_view_source.js"]
+
+["browser_cross_process_csp_inheritance.js"]
+https_first_disabled = true
+
+["browser_csp_sandbox_no_script_js_uri.js"]
+support-files = [
+ "file_csp_sandbox_no_script_js_uri.html",
+ "file_csp_sandbox_no_script_js_uri.html^headers^",
+]
+
+["browser_csp_uir.js"]
+support-files = [
+ "file_csp_uir.html",
+ "file_csp_uir_dummy.html",
+]
+
+["browser_dataURI_unique_opaque_origin.js"]
+https_first_disabled = true
+
+["browser_data_load_inherit_csp.js"]
+
+["browser_fall_back_to_https.js"]
+https_first_disabled = true
+skip-if = ["os == 'mac'"]
+
+["browser_frameloader_swap_with_bfcache.js"]
+
+["browser_history_triggeringprincipal_viewsource.js"]
+https_first_disabled = true
+
+["browser_isInitialDocument.js"]
+https_first_disabled = true
+
+["browser_loadURI_postdata.js"]
+
+["browser_multiple_pushState.js"]
+https_first_disabled = true
+
+["browser_onbeforeunload_frame.js"]
+support-files = ["head_browser_onbeforeunload.js"]
+
+["browser_onbeforeunload_navigation.js"]
+skip-if = ["os == 'win' && !debug"] # bug 1300351
+
+["browser_onbeforeunload_parent.js"]
+support-files = ["head_browser_onbeforeunload.js"]
+
+["browser_onunload_stop.js"]
+https_first_disabled = true
+
+["browser_overlink.js"]
+support-files = ["overlink_test.html"]
+
+["browser_platform_emulation.js"]
+
+["browser_search_notification.js"]
+
+["browser_tab_replace_while_loading.js"]
+skip-if = [
+ "os == 'linux' && bits == 64 && os_version == '18.04'", # Bug 1604237
+ "os == 'win'", # Bug 1671794
+]
+
+["browser_tab_touch_events.js"]
+
+["browser_targetTopLevelLinkClicksToBlank.js"]
+
+["browser_title_in_session_history.js"]
+skip-if = ["!sessionHistoryInParent"]
+
+["browser_ua_emulation.js"]
+
+["browser_uriFixupAlternateRedirects.js"]
+https_first_disabled = true
+support-files = ["redirect_to_example.sjs"]
+
+["browser_uriFixupIntegration.js"]
+
+["browser_viewsource_chrome_to_content.js"]
+
+["browser_viewsource_multipart.js"]
+support-files = ["file_basic_multipart.sjs"]
diff --git a/docshell/test/browser/browser_alternate_fixup_middle_click_link.js b/docshell/test/browser/browser_alternate_fixup_middle_click_link.js
new file mode 100644
index 0000000000..dcf2d80af9
--- /dev/null
+++ b/docshell/test/browser/browser_alternate_fixup_middle_click_link.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that we don't do alternate fixup when users middle-click
+ * broken links in the content document.
+ */
+add_task(async function test_alt_fixup_middle_click() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ let link = content.document.createElement("a");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ link.href = "http://example/foo";
+ link.textContent = "Me, me, click me!";
+ content.document.body.append(link);
+ });
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ /* wantLoad = */ null,
+ /* waitForLoad = */ true,
+ /* waitForAnyTab = */ false,
+ /* maybeErrorPage = */ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "a[href]",
+ { button: 1 },
+ browser
+ );
+ let tab = await newTabPromise;
+ let { browsingContext } = tab.linkedBrowser;
+ // TBH, if the test fails, we probably force-crash because we try to reach
+ // *www.* example.com, which isn't proxied by the test infrastructure so
+ // will forcibly abort the test. But we need some asserts so they might as
+ // well be meaningful:
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example/foo",
+ "URL for tab should be correct."
+ );
+
+ ok(
+ browsingContext.currentWindowGlobal.documentURI.spec.startsWith(
+ "about:neterror"
+ ),
+ "Should be showing error page."
+ );
+ BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/docshell/test/browser/browser_backforward_restore_scroll.js b/docshell/test/browser/browser_backforward_restore_scroll.js
new file mode 100644
index 0000000000..ca687bf528
--- /dev/null
+++ b/docshell/test/browser/browser_backforward_restore_scroll.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888"
+);
+const URL1 = ROOT + "file_backforward_restore_scroll.html";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const URL2 = "http://example.net/";
+
+const SCROLL0 = 500;
+const SCROLL1 = 1000;
+
+function promiseBrowserLoaded(url) {
+ return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, url);
+}
+
+add_task(async function test() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, URL1);
+
+ // Scroll the 2 frames.
+ let children = gBrowser.selectedBrowser.browsingContext.children;
+ await SpecialPowers.spawn(children[0], [SCROLL0], scrollY =>
+ content.scrollTo(0, scrollY)
+ );
+ await SpecialPowers.spawn(children[1], [SCROLL1], scrollY =>
+ content.scrollTo(0, scrollY)
+ );
+
+ // Navigate forwards then backwards.
+ let loaded = promiseBrowserLoaded(URL2);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, URL2);
+ await loaded;
+
+ loaded = promiseBrowserLoaded(URL1);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.history.back();
+ });
+ await loaded;
+
+ // And check the results.
+ children = gBrowser.selectedBrowser.browsingContext.children;
+ await SpecialPowers.spawn(children[0], [SCROLL0], scrollY => {
+ Assert.equal(content.scrollY, scrollY, "frame 0 has correct scroll");
+ });
+ await SpecialPowers.spawn(children[1], [SCROLL1], scrollY => {
+ Assert.equal(content.scrollY, scrollY, "frame 1 has correct scroll");
+ });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/docshell/test/browser/browser_backforward_userinteraction.js b/docshell/test/browser/browser_backforward_userinteraction.js
new file mode 100644
index 0000000000..264299c902
--- /dev/null
+++ b/docshell/test/browser/browser_backforward_userinteraction.js
@@ -0,0 +1,374 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+const IFRAME_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_iframe_page.html";
+
+async function assertMenulist(entries, baseURL = TEST_PAGE) {
+ // Wait for the session data to be flushed before continuing the test
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ let backButton = document.getElementById("back-button");
+ let contextMenu = document.getElementById("backForwardMenu");
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(backButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+
+ ok(true, "history menu opened");
+
+ let nodes = contextMenu.childNodes;
+
+ is(
+ nodes.length,
+ entries.length,
+ "Has the expected number of contextMenu entries"
+ );
+
+ for (let i = 0; i < entries.length; i++) {
+ let node = nodes[i];
+ is(
+ node.getAttribute("uri").replace(/[?|#]/, "!"),
+ baseURL + "!entry=" + entries[i],
+ "contextMenu node has the correct uri"
+ );
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+}
+
+// There are different ways of loading a page, but they should exhibit roughly the same
+// back-forward behavior for the purpose of requiring user interaction. Thus, we
+// have a utility function that runs the same test with a parameterized method of loading
+// new URLs.
+async function runTopLevelTest(loadMethod, useHashes = false) {
+ let p = useHashes ? "#" : "?";
+
+ // Test with both pref on and off
+ for (let requireUserInteraction of [true, false]) {
+ Services.prefs.setBoolPref(
+ "browser.navigation.requireUserInteraction",
+ requireUserInteraction
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE + p + "entry=0"
+ );
+ let browser = tab.linkedBrowser;
+
+ assertBackForwardState(false, false);
+
+ await loadMethod(TEST_PAGE + p + "entry=1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist([1, 0]);
+
+ await loadMethod(TEST_PAGE + p + "entry=2");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(requireUserInteraction ? [2, 0] : [2, 1, 0]);
+
+ await loadMethod(TEST_PAGE + p + "entry=3");
+
+ info("Adding user interaction for entry=3");
+ // Add some user interaction to entry 3
+ await BrowserTestUtils.synthesizeMouse(
+ "body",
+ 0,
+ 0,
+ {},
+ browser.browsingContext,
+ true
+ );
+
+ assertBackForwardState(true, false);
+ await assertMenulist(requireUserInteraction ? [3, 0] : [3, 2, 1, 0]);
+
+ await loadMethod(TEST_PAGE + p + "entry=4");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(requireUserInteraction ? [4, 3, 0] : [4, 3, 2, 1, 0]);
+
+ info("Adding user interaction for entry=4");
+ // Add some user interaction to entry 4
+ await BrowserTestUtils.synthesizeMouse(
+ "body",
+ 0,
+ 0,
+ {},
+ browser.browsingContext,
+ true
+ );
+
+ await loadMethod(TEST_PAGE + p + "entry=5");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0]
+ );
+
+ await goBack(TEST_PAGE + p + "entry=4");
+ await goBack(TEST_PAGE + p + "entry=3");
+
+ if (!requireUserInteraction) {
+ await goBack(TEST_PAGE + p + "entry=2");
+ await goBack(TEST_PAGE + p + "entry=1");
+ }
+
+ assertBackForwardState(true, true);
+ await assertMenulist(
+ requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0]
+ );
+
+ await goBack(TEST_PAGE + p + "entry=0");
+
+ assertBackForwardState(false, true);
+
+ if (!requireUserInteraction) {
+ await goForward(TEST_PAGE + p + "entry=1");
+ await goForward(TEST_PAGE + p + "entry=2");
+ }
+
+ await goForward(TEST_PAGE + p + "entry=3");
+
+ assertBackForwardState(true, true);
+ await assertMenulist(
+ requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0]
+ );
+
+ await goForward(TEST_PAGE + p + "entry=4");
+
+ assertBackForwardState(true, true);
+ await assertMenulist(
+ requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0]
+ );
+
+ await goForward(TEST_PAGE + p + "entry=5");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0]
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+}
+
+async function runIframeTest(loadMethod) {
+ // Test with both pref on and off
+ for (let requireUserInteraction of [true, false]) {
+ Services.prefs.setBoolPref(
+ "browser.navigation.requireUserInteraction",
+ requireUserInteraction
+ );
+
+ // First test the boring case where we only have one iframe.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ IFRAME_PAGE + "?entry=0"
+ );
+ let browser = tab.linkedBrowser;
+
+ assertBackForwardState(false, false);
+
+ await loadMethod(TEST_PAGE + "?sub_entry=1", "frame1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist([0, 0], IFRAME_PAGE);
+
+ await loadMethod(TEST_PAGE + "?sub_entry=2", "frame1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [0, 0] : [0, 0, 0],
+ IFRAME_PAGE
+ );
+
+ let bc = await SpecialPowers.spawn(browser, [], function () {
+ return content.document.getElementById("frame1").browsingContext;
+ });
+
+ // Add some user interaction to sub entry 2
+ await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true);
+
+ await loadMethod(TEST_PAGE + "?sub_entry=3", "frame1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [0, 0, 0] : [0, 0, 0, 0],
+ IFRAME_PAGE
+ );
+
+ await loadMethod(TEST_PAGE + "?sub_entry=4", "frame1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [0, 0, 0] : [0, 0, 0, 0, 0],
+ IFRAME_PAGE
+ );
+
+ if (!requireUserInteraction) {
+ await goBack(TEST_PAGE + "?sub_entry=3", true);
+ }
+
+ await goBack(TEST_PAGE + "?sub_entry=2", true);
+
+ assertBackForwardState(true, true);
+ await assertMenulist(
+ requireUserInteraction ? [0, 0, 0] : [0, 0, 0, 0, 0],
+ IFRAME_PAGE
+ );
+
+ await loadMethod(IFRAME_PAGE + "?entry=1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [1, 0, 0] : [1, 0, 0, 0],
+ IFRAME_PAGE
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Two iframes, now we're talking.
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ IFRAME_PAGE + "?entry=0"
+ );
+ browser = tab.linkedBrowser;
+
+ await loadMethod(IFRAME_PAGE + "?entry=1");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(requireUserInteraction ? [1, 0] : [1, 0], IFRAME_PAGE);
+
+ // Add some user interaction to frame 1.
+ bc = await SpecialPowers.spawn(browser, [], function () {
+ return content.document.getElementById("frame1").browsingContext;
+ });
+ await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true);
+
+ // Add some user interaction to frame 2.
+ bc = await SpecialPowers.spawn(browser, [], function () {
+ return content.document.getElementById("frame2").browsingContext;
+ });
+ await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true);
+
+ // Navigate frame 2.
+ await loadMethod(TEST_PAGE + "?sub_entry=1", "frame2");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [1, 1, 0] : [1, 1, 0],
+ IFRAME_PAGE
+ );
+
+ // Add some user interaction to frame 1, again.
+ bc = await SpecialPowers.spawn(browser, [], function () {
+ return content.document.getElementById("frame1").browsingContext;
+ });
+ await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true);
+
+ // Navigate frame 2, again.
+ await loadMethod(TEST_PAGE + "?sub_entry=2", "frame2");
+
+ assertBackForwardState(true, false);
+ await assertMenulist(
+ requireUserInteraction ? [1, 1, 1, 0] : [1, 1, 1, 0],
+ IFRAME_PAGE
+ );
+
+ await goBack(TEST_PAGE + "?sub_entry=1", true);
+
+ assertBackForwardState(true, true);
+ await assertMenulist(
+ requireUserInteraction ? [1, 1, 1, 0] : [1, 1, 1, 0],
+ IFRAME_PAGE
+ );
+
+ await goBack(TEST_PAGE + "?sub_entry=0", true);
+
+ assertBackForwardState(true, true);
+ await assertMenulist(
+ requireUserInteraction ? [1, 1, 1, 0] : [1, 1, 1, 0],
+ IFRAME_PAGE
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+}
+
+// Test that when the pref is flipped, we are skipping history
+// entries without user interaction when following links with hash URIs.
+add_task(async function test_hashURI() {
+ async function followLinkHash(url) {
+ info(`Creating and following a link to ${url}`);
+ let browser = gBrowser.selectedBrowser;
+ let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ await SpecialPowers.spawn(browser, [url], function (url) {
+ let a = content.document.createElement("a");
+ a.href = url;
+ content.document.body.appendChild(a);
+ a.click();
+ });
+ await loaded;
+ info(`Loaded ${url}`);
+ }
+
+ await runTopLevelTest(followLinkHash, true);
+});
+
+// Test that when the pref is flipped, we are skipping history
+// entries without user interaction when using history.pushState.
+add_task(async function test_pushState() {
+ await runTopLevelTest(pushState);
+});
+
+// Test that when the pref is flipped, we are skipping history
+// entries without user interaction when following a link.
+add_task(async function test_followLink() {
+ await runTopLevelTest(followLink);
+});
+
+// Test that when the pref is flipped, we are skipping history
+// entries without user interaction when navigating inside an iframe
+// using history.pushState.
+add_task(async function test_iframe_pushState() {
+ await runIframeTest(pushState);
+});
+
+// Test that when the pref is flipped, we are skipping history
+// entries without user interaction when navigating inside an iframe
+// by following links.
+add_task(async function test_iframe_followLink() {
+ await runIframeTest(followLink);
+});
diff --git a/docshell/test/browser/browser_backforward_userinteraction_about.js b/docshell/test/browser/browser_backforward_userinteraction_about.js
new file mode 100644
index 0000000000..606fcc45c5
--- /dev/null
+++ b/docshell/test/browser/browser_backforward_userinteraction_about.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+
+// Regression test for navigating back after visiting an about: page
+// loaded in the parent process.
+add_task(async function test_about_back() {
+ // Test with both pref on and off
+ for (let requireUserInteraction of [true, false]) {
+ Services.prefs.setBoolPref(
+ "browser.navigation.requireUserInteraction",
+ requireUserInteraction
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE + "?entry=0"
+ );
+ let browser = tab.linkedBrowser;
+ assertBackForwardState(false, false);
+
+ await followLink(TEST_PAGE + "?entry=1");
+ assertBackForwardState(true, false);
+
+ await followLink(TEST_PAGE + "?entry=2");
+ assertBackForwardState(true, false);
+
+ // Add some user interaction to entry 2
+ await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, browser, true);
+
+ await loadURI("about:config");
+ assertBackForwardState(true, false);
+
+ await goBack(TEST_PAGE + "?entry=2");
+ assertBackForwardState(true, true);
+
+ if (!requireUserInteraction) {
+ await goBack(TEST_PAGE + "?entry=1");
+ assertBackForwardState(true, true);
+ }
+
+ await goBack(TEST_PAGE + "?entry=0");
+ assertBackForwardState(false, true);
+
+ if (!requireUserInteraction) {
+ await goForward(TEST_PAGE + "?entry=1");
+ assertBackForwardState(true, true);
+ }
+
+ await goForward(TEST_PAGE + "?entry=2");
+ assertBackForwardState(true, true);
+
+ await goForward("about:config");
+ assertBackForwardState(true, false);
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+});
diff --git a/docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js b/docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js
new file mode 100644
index 0000000000..289ed1d133
--- /dev/null
+++ b/docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+
+async function runTest(privilegedLoad) {
+ let prefVals;
+ // Test with both pref on and off, unless parent-controlled pref is enabled.
+ // This distinction can be removed once SHIP is enabled by default.
+ if (
+ Services.prefs.getBoolPref("browser.tabs.documentchannel.parent-controlled")
+ ) {
+ prefVals = [false];
+ } else {
+ prefVals = [true, false];
+ }
+
+ for (let requireUserInteraction of prefVals) {
+ Services.prefs.setBoolPref(
+ "browser.navigation.requireUserInteraction",
+ requireUserInteraction
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE + "?entry=0"
+ );
+
+ assertBackForwardState(false, false);
+
+ await followLink(TEST_PAGE + "?entry=1");
+
+ assertBackForwardState(true, false);
+
+ await followLink(TEST_PAGE + "?entry=2");
+
+ assertBackForwardState(true, false);
+
+ await followLink(TEST_PAGE + "?entry=3");
+
+ assertBackForwardState(true, false);
+
+ // Entry 4 will be added through a user action in browser chrome,
+ // giving user interaction to entry 3. Entry 4 should not gain automatic
+ // user interaction.
+ await privilegedLoad(TEST_PAGE + "?entry=4");
+
+ assertBackForwardState(true, false);
+
+ await followLink(TEST_PAGE + "?entry=5");
+
+ assertBackForwardState(true, false);
+
+ if (!requireUserInteraction) {
+ await goBack(TEST_PAGE + "?entry=4");
+ }
+ await goBack(TEST_PAGE + "?entry=3");
+
+ if (!requireUserInteraction) {
+ await goBack(TEST_PAGE + "?entry=2");
+ await goBack(TEST_PAGE + "?entry=1");
+ }
+
+ assertBackForwardState(true, true);
+
+ await goBack(TEST_PAGE + "?entry=0");
+
+ assertBackForwardState(false, true);
+
+ if (!requireUserInteraction) {
+ await goForward(TEST_PAGE + "?entry=1");
+ await goForward(TEST_PAGE + "?entry=2");
+ }
+
+ await goForward(TEST_PAGE + "?entry=3");
+
+ assertBackForwardState(true, true);
+
+ if (!requireUserInteraction) {
+ await goForward(TEST_PAGE + "?entry=4");
+ }
+
+ await goForward(TEST_PAGE + "?entry=5");
+
+ assertBackForwardState(true, false);
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+}
+
+// Test that we add a user interaction flag to the previous site when loading
+// a new site from user interaction with privileged UI, e.g. through the
+// URL bar.
+add_task(async function test_urlBar() {
+ await runTest(async function (url) {
+ info(`Loading ${url} via the URL bar.`);
+ let browser = gBrowser.selectedBrowser;
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ gURLBar.focus();
+ gURLBar.value = url;
+ gURLBar.goButton.click();
+ await loaded;
+ });
+});
diff --git a/docshell/test/browser/browser_badCertDomainFixup.js b/docshell/test/browser/browser_badCertDomainFixup.js
new file mode 100644
index 0000000000..4bf6ddcef3
--- /dev/null
+++ b/docshell/test/browser/browser_badCertDomainFixup.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test checks if we are correctly fixing https URLs by prefixing
+// with www. when we encounter a SSL_ERROR_BAD_CERT_DOMAIN error.
+// For example, https://example.com -> https://www.example.com.
+
+const PREF_BAD_CERT_DOMAIN_FIX_ENABLED =
+ "security.bad_cert_domain_error.url_fix_enabled";
+const PREF_ALLOW_HIJACKING_LOCALHOST =
+ "network.proxy.allow_hijacking_localhost";
+
+const BAD_CERT_DOMAIN_ERROR_URL = "https://badcertdomain.example.com:443";
+const FIXED_URL = "https://www.badcertdomain.example.com/";
+
+const BAD_CERT_DOMAIN_ERROR_URL2 =
+ "https://mismatch.badcertdomain.example.com:443";
+const IPV4_ADDRESS = "https://127.0.0.3:433";
+const BAD_CERT_DOMAIN_ERROR_PORT = "https://badcertdomain.example.com:82";
+
+async function verifyErrorPage(errorPageURL) {
+ let certErrorLoaded = BrowserTestUtils.waitForErrorPage(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser, errorPageURL);
+ await certErrorLoaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let ec;
+ await ContentTaskUtils.waitForCondition(() => {
+ ec = content.document.getElementById("errorCode");
+ return ec.textContent;
+ }, "Error code has been set inside the advanced button panel");
+ is(
+ ec.textContent,
+ "SSL_ERROR_BAD_CERT_DOMAIN",
+ "Correct error code is shown"
+ );
+ });
+}
+
+// Test that "www." is prefixed to a https url when we encounter a bad cert domain
+// error if the "www." form is included in the certificate's subjectAltNames.
+add_task(async function prefixBadCertDomain() {
+ // Turn off the pref and ensure that we show the error page as expected.
+ Services.prefs.setBoolPref(PREF_BAD_CERT_DOMAIN_FIX_ENABLED, false);
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ await verifyErrorPage(BAD_CERT_DOMAIN_ERROR_URL);
+ info("Cert error is shown as expected when the fixup pref is disabled");
+
+ // Turn on the pref and test that we fix the HTTPS URL.
+ Services.prefs.setBoolPref(PREF_BAD_CERT_DOMAIN_FIX_ENABLED, true);
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let loadSuccessful = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ FIXED_URL
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser, BAD_CERT_DOMAIN_ERROR_URL);
+ await loadSuccessful;
+
+ info("The URL was fixed as expected");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test that we don't prefix "www." to a https url when we encounter a bad cert domain
+// error under certain conditions.
+add_task(async function ignoreBadCertDomain() {
+ Services.prefs.setBoolPref(PREF_BAD_CERT_DOMAIN_FIX_ENABLED, true);
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ // Test for when "www." form is not present in the certificate.
+ await verifyErrorPage(BAD_CERT_DOMAIN_ERROR_URL2);
+ info("Certificate error was shown as expected");
+
+ // Test that urls with IP addresses are not fixed.
+ Services.prefs.setBoolPref(PREF_ALLOW_HIJACKING_LOCALHOST, true);
+ await verifyErrorPage(IPV4_ADDRESS);
+ Services.prefs.clearUserPref(PREF_ALLOW_HIJACKING_LOCALHOST);
+ info("Certificate error was shown as expected for an IP address");
+
+ // Test that urls with ports are not fixed.
+ await verifyErrorPage(BAD_CERT_DOMAIN_ERROR_PORT);
+ info("Certificate error was shown as expected for a host with port");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/docshell/test/browser/browser_bfcache_copycommand.js b/docshell/test/browser/browser_bfcache_copycommand.js
new file mode 100644
index 0000000000..cd0015ac03
--- /dev/null
+++ b/docshell/test/browser/browser_bfcache_copycommand.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function dummyPageURL(domain, query = "") {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ `https://${domain}`
+ ) +
+ "dummy_page.html" +
+ query
+ );
+}
+
+async function openContextMenu(browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "body",
+ 1,
+ 1,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+ await awaitPopupShown;
+}
+
+async function closeContextMenu() {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+}
+
+async function testWithDomain(domain) {
+ // Passing a query to make sure the next load is never a same-document
+ // navigation.
+ let dummy = dummyPageURL("example.org", "?start");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, dummy);
+ let browser = tab.linkedBrowser;
+
+ let sel = await SpecialPowers.spawn(browser, [], function () {
+ let sel = content.getSelection();
+ sel.removeAllRanges();
+ sel.selectAllChildren(content.document.body);
+ return sel.toString();
+ });
+
+ await openContextMenu(browser);
+
+ let copyItem = document.getElementById("context-copy");
+ ok(!copyItem.disabled, "Copy item should be enabled if text is selected.");
+
+ await closeContextMenu();
+
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ BrowserTestUtils.startLoadingURIString(browser, dummyPageURL(domain));
+ await loaded;
+ loaded = BrowserTestUtils.waitForLocationChange(gBrowser, dummy);
+ browser.goBack();
+ await loaded;
+
+ let sel2 = await SpecialPowers.spawn(browser, [], function () {
+ return content.getSelection().toString();
+ });
+ is(sel, sel2, "Selection should remain when coming out of BFCache.");
+
+ await openContextMenu(browser);
+
+ ok(!copyItem.disabled, "Copy item should be enabled if text is selected.");
+
+ await closeContextMenu();
+
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testValidSameOrigin() {
+ await testWithDomain("example.org");
+});
+
+add_task(async function testValidCrossOrigin() {
+ await testWithDomain("example.com");
+});
+
+add_task(async function testInvalid() {
+ await testWithDomain("this.is.invalid");
+});
diff --git a/docshell/test/browser/browser_browsingContext-01.js b/docshell/test/browser/browser_browsingContext-01.js
new file mode 100644
index 0000000000..95831d2567
--- /dev/null
+++ b/docshell/test/browser/browser_browsingContext-01.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "about:blank";
+
+async function getBrowsingContextId(browser, id) {
+ return SpecialPowers.spawn(browser, [id], async function (id) {
+ let contextId = content.window.docShell.browsingContext.id;
+
+ let frames = [content.window];
+ while (frames.length) {
+ let frame = frames.pop();
+ let target = frame.document.getElementById(id);
+ if (target) {
+ contextId = target.docShell.browsingContext.id;
+ break;
+ }
+
+ frames = frames.concat(Array.from(frame.frames));
+ }
+
+ return contextId;
+ });
+}
+
+async function addFrame(browser, id, parentId) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ parentId, id }],
+ async function ({ parentId, id }) {
+ let parent = null;
+ if (parentId) {
+ let frames = [content.window];
+ while (frames.length) {
+ let frame = frames.pop();
+ let target = frame.document.getElementById(parentId);
+ if (target) {
+ parent = target.contentWindow.document.body;
+ break;
+ }
+ frames = frames.concat(Array.from(frame.frames));
+ }
+ } else {
+ parent = content.document.body;
+ }
+
+ let frame = await new Promise(resolve => {
+ let frame = content.document.createElement("iframe");
+ frame.id = id || "";
+ frame.url = "about:blank";
+ frame.onload = () => resolve(frame);
+ parent.appendChild(frame);
+ });
+
+ return frame.contentWindow.docShell.browsingContext.id;
+ }
+ );
+}
+
+async function removeFrame(browser, id) {
+ return SpecialPowers.spawn(browser, [id], async function (id) {
+ let frames = [content.window];
+ while (frames.length) {
+ let frame = frames.pop();
+ let target = frame.document.getElementById(id);
+ if (target) {
+ target.remove();
+ break;
+ }
+
+ frames = frames.concat(Array.from(frame.frames));
+ }
+ });
+}
+
+function getBrowsingContextById(id) {
+ return BrowsingContext.get(id);
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ let topId = await getBrowsingContextId(browser, "");
+ let topContext = getBrowsingContextById(topId);
+ isnot(topContext, null);
+ is(topContext.parent, null);
+ is(
+ topId,
+ browser.browsingContext.id,
+ "<browser> has the correct browsingContext"
+ );
+ is(
+ browser.browserId,
+ topContext.browserId,
+ "browsing context should have a correct <browser> id"
+ );
+
+ let id0 = await addFrame(browser, "frame0");
+ let browsingContext0 = getBrowsingContextById(id0);
+ isnot(browsingContext0, null);
+ is(browsingContext0.parent, topContext);
+
+ await removeFrame(browser, "frame0");
+
+ is(topContext.children.indexOf(browsingContext0), -1);
+
+ // TODO(farre): Handle browsingContext removal [see Bug 1486719].
+ todo_isnot(browsingContext0.parent, topContext);
+ }
+ );
+});
+
+add_task(async function () {
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ ) + "dummy_page.html",
+ },
+ async function (browser) {
+ let path = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ );
+ await SpecialPowers.spawn(browser, [path], async function (path) {
+ var bc = new content.BroadcastChannel("browser_browsingContext");
+ function waitForMessage(command) {
+ let p = new Promise(resolve => {
+ bc.addEventListener("message", e => resolve(e), { once: true });
+ });
+ command();
+ return p;
+ }
+
+ // Open a new window and wait for the message.
+ let e1 = await waitForMessage(_ =>
+ content.window.open(path + "onpageshow_message.html", "", "noopener")
+ );
+
+ is(e1.data, "pageshow", "Got page show");
+
+ let e2 = await waitForMessage(_ => bc.postMessage("createiframe"));
+ is(e2.data.framesLength, 1, "Here we should have an iframe");
+
+ let e3 = await waitForMessage(_ => bc.postMessage("nextpage"));
+
+ is(e3.data.event, "load");
+ is(e3.data.framesLength, 0, "Here there shouldn't be an iframe");
+
+ // Return to the previous document. N.B. we expect to trigger
+ // BFCache here, hence we wait for pageshow.
+ let e4 = await waitForMessage(_ => bc.postMessage("back"));
+
+ is(e4.data, "pageshow");
+
+ let e5 = await waitForMessage(_ => bc.postMessage("queryframes"));
+ is(e5.data.framesLength, 1, "And again there should be an iframe");
+
+ is(e5.outerWindowId, e2.outerWindowId, "BF cache cached outer window");
+ is(e5.browsingContextId, e2.browsingContextId, "BF cache cached BC");
+
+ let e6 = await waitForMessage(_ => bc.postMessage("close"));
+ is(e6.data, "closed");
+
+ bc.close();
+ });
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_browsingContext-02.js b/docshell/test/browser/browser_browsingContext-02.js
new file mode 100644
index 0000000000..8439b869b0
--- /dev/null
+++ b/docshell/test/browser/browser_browsingContext-02.js
@@ -0,0 +1,235 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ const BASE1 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ );
+ const BASE2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test1.example.com"
+ );
+ const URL = BASE1 + "onload_message.html";
+ let sixth = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ URL + "#sixth",
+ true,
+ true
+ );
+ let seventh = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ URL + "#seventh",
+ true,
+ true
+ );
+ let browserIds = await SpecialPowers.spawn(
+ browser,
+ [{ base1: BASE1, base2: BASE2 }],
+ async function ({ base1, base2 }) {
+ let top = content;
+ top.name = "top";
+ top.location.href += "#top";
+
+ let contexts = {
+ top: top.location.href,
+ first: base1 + "dummy_page.html#first",
+ third: base2 + "dummy_page.html#third",
+ second: base1 + "dummy_page.html#second",
+ fourth: base2 + "dummy_page.html#fourth",
+ fifth: base1 + "dummy_page.html#fifth",
+ sixth: base1 + "onload_message.html#sixth",
+ seventh: base1 + "onload_message.html#seventh",
+ };
+
+ function addFrame(target, name) {
+ return content.SpecialPowers.spawn(
+ target,
+ [name, contexts[name]],
+ async (name, context) => {
+ let doc = this.content.document;
+
+ let frame = doc.createElement("iframe");
+ doc.body.appendChild(frame);
+ frame.name = name;
+ frame.src = context;
+ await new Promise(resolve => {
+ frame.addEventListener("load", resolve, { once: true });
+ });
+ return frame.browsingContext;
+ }
+ );
+ }
+
+ function addWindow(target, name, { options, resolve }) {
+ return content.SpecialPowers.spawn(
+ target,
+ [name, contexts[name], options, resolve],
+ (name, context, options, resolve) => {
+ let win = this.content.open(context, name, options);
+ let bc = win && win.docShell.browsingContext;
+
+ if (resolve) {
+ return new Promise(resolve =>
+ this.content.addEventListener("message", () => resolve(bc))
+ );
+ }
+ return Promise.resolve({ name });
+ }
+ );
+ }
+
+ // We're going to create a tree that looks like the
+ // following.
+ //
+ // top sixth seventh
+ // / \
+ // / \ /
+ // first second
+ // / \ /
+ // / \
+ // third fourth - - -
+ // /
+ // /
+ // fifth
+ //
+ // The idea is to have one top level non-auxiliary browsing
+ // context, five nested, one top level auxiliary with an
+ // opener, and one top level without an opener. Given that
+ // set of related and one unrelated browsing contexts we
+ // wish to confirm that targeting is able to find
+ // appropriate browsing contexts.
+
+ // WindowGlobalChild.findBrowsingContextWithName requires access
+ // checks, which can only be performed in the process of the accessor
+ // WindowGlobalChild.
+ function findWithName(bc, name) {
+ return content.SpecialPowers.spawn(bc, [name], name => {
+ return content.windowGlobalChild.findBrowsingContextWithName(
+ name
+ );
+ });
+ }
+
+ async function reachable(start, target) {
+ info(start.name, target.name);
+ is(
+ await findWithName(start, target.name),
+ target,
+ [start.name, "can reach", target.name].join(" ")
+ );
+ }
+
+ async function unreachable(start, target) {
+ is(
+ await findWithName(start, target.name),
+ null,
+ [start.name, "can't reach", target.name].join(" ")
+ );
+ }
+
+ let first = await addFrame(top, "first");
+ info("first");
+ let second = await addFrame(top, "second");
+ info("second");
+ let third = await addFrame(first, "third");
+ info("third");
+ let fourth = await addFrame(first, "fourth");
+ info("fourth");
+ let fifth = await addFrame(fourth, "fifth");
+ info("fifth");
+ let sixth = await addWindow(fourth, "sixth", { resolve: true });
+ info("sixth");
+ let seventh = await addWindow(fourth, "seventh", {
+ options: ["noopener"],
+ });
+ info("seventh");
+
+ let origin1 = [first, second, fifth, sixth];
+ let origin2 = [third, fourth];
+
+ let topBC = BrowsingContext.getFromWindow(top);
+ let frames = new Map([
+ [topBC, [topBC, first, second, third, fourth, fifth, sixth]],
+ [first, [topBC, ...origin1, third, fourth]],
+ [second, [topBC, ...origin1, third, fourth]],
+ [third, [topBC, ...origin2, fifth, sixth]],
+ [fourth, [topBC, ...origin2, fifth, sixth]],
+ [fifth, [topBC, ...origin1, third, fourth]],
+ [sixth, [...origin1, third, fourth]],
+ ]);
+
+ for (let [start, accessible] of frames) {
+ for (let frame of frames.keys()) {
+ if (accessible.includes(frame)) {
+ await reachable(start, frame);
+ } else {
+ await unreachable(start, frame);
+ }
+ }
+ await unreachable(start, seventh);
+ }
+
+ let topBrowserId = topBC.browserId;
+ ok(topBrowserId > 0, "Should have a browser ID.");
+ for (let [name, bc] of Object.entries({
+ first,
+ second,
+ third,
+ fourth,
+ fifth,
+ })) {
+ is(
+ bc.browserId,
+ topBrowserId,
+ `${name} frame should have the same browserId as top.`
+ );
+ }
+
+ ok(sixth.browserId > 0, "sixth should have a browserId.");
+ isnot(
+ sixth.browserId,
+ topBrowserId,
+ "sixth frame should have a different browserId to top."
+ );
+
+ return [topBrowserId, sixth.browserId];
+ }
+ );
+
+ [sixth, seventh] = await Promise.all([sixth, seventh]);
+
+ is(
+ browser.browserId,
+ browserIds[0],
+ "browser should have the right browserId."
+ );
+ is(
+ browser.browsingContext.browserId,
+ browserIds[0],
+ "browser's BrowsingContext should have the right browserId."
+ );
+ is(
+ sixth.linkedBrowser.browserId,
+ browserIds[1],
+ "sixth should have the right browserId."
+ );
+ is(
+ sixth.linkedBrowser.browsingContext.browserId,
+ browserIds[1],
+ "sixth's BrowsingContext should have the right browserId."
+ );
+
+ for (let tab of [sixth, seventh]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_browsingContext-embedder.js b/docshell/test/browser/browser_browsingContext-embedder.js
new file mode 100644
index 0000000000..9473a46eb4
--- /dev/null
+++ b/docshell/test/browser/browser_browsingContext-embedder.js
@@ -0,0 +1,156 @@
+"use strict";
+
+function observeOnce(topic) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (topic == aTopic) {
+ Services.obs.removeObserver(observer, topic);
+ setTimeout(() => resolve(aSubject), 0);
+ }
+ }, topic);
+ });
+}
+
+add_task(async function runTest() {
+ let fissionWindow = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ remote: true,
+ });
+
+ info(`chrome, parent`);
+ let chromeBC = fissionWindow.docShell.browsingContext;
+ ok(chromeBC.currentWindowGlobal, "Should have a current WindowGlobal");
+ is(chromeBC.embedderWindowGlobal, null, "chrome has no embedder global");
+ is(chromeBC.embedderElement, null, "chrome has no embedder element");
+ is(chromeBC.parent, null, "chrome has no parent");
+
+ // Open a new tab, and check that basic frames work out.
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: fissionWindow.gBrowser,
+ });
+
+ info(`root, parent`);
+ let rootBC = tab.linkedBrowser.browsingContext;
+ ok(rootBC.currentWindowGlobal, "[parent] root has a window global");
+ is(
+ rootBC.embedderWindowGlobal,
+ chromeBC.currentWindowGlobal,
+ "[parent] root has chrome as embedder global"
+ );
+ is(
+ rootBC.embedderElement,
+ tab.linkedBrowser,
+ "[parent] root has browser as embedder element"
+ );
+ is(rootBC.parent, null, "[parent] root has no parent");
+
+ // Test with an in-process frame
+ let frameId = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ info(`root, child`);
+ let rootBC = content.docShell.browsingContext;
+ is(rootBC.embedderElement, null, "[child] root has no embedder");
+ is(rootBC.parent, null, "[child] root has no parent");
+
+ info(`frame, child`);
+ let iframe = content.document.createElement("iframe");
+ content.document.body.appendChild(iframe);
+
+ let frameBC = iframe.contentWindow.docShell.browsingContext;
+ is(frameBC.embedderElement, iframe, "[child] frame embedded within iframe");
+ is(frameBC.parent, rootBC, "[child] frame has root as parent");
+
+ return frameBC.id;
+ });
+
+ info(`frame, parent`);
+ let frameBC = BrowsingContext.get(frameId);
+ ok(frameBC.currentWindowGlobal, "[parent] frame has a window global");
+ is(
+ frameBC.embedderWindowGlobal,
+ rootBC.currentWindowGlobal,
+ "[parent] frame has root as embedder global"
+ );
+ is(frameBC.embedderElement, null, "[parent] frame has no embedder element");
+ is(frameBC.parent, rootBC, "[parent] frame has root as parent");
+
+ // Test with an out-of-process iframe.
+
+ let oopID = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ info(`creating oop iframe`);
+ let oop = content.document.createElement("iframe");
+ oop.setAttribute("src", "https://example.com");
+ content.document.body.appendChild(oop);
+
+ await new Promise(resolve => {
+ oop.addEventListener("load", resolve, { once: true });
+ });
+
+ info(`oop frame, child`);
+ let oopBC = oop.frameLoader.browsingContext;
+ is(oopBC.embedderElement, oop, "[child] oop frame embedded within iframe");
+ is(
+ oopBC.parent,
+ content.docShell.browsingContext,
+ "[child] frame has root as parent"
+ );
+
+ return oopBC.id;
+ });
+
+ info(`oop frame, parent`);
+ let oopBC = BrowsingContext.get(oopID);
+ is(
+ oopBC.embedderWindowGlobal,
+ rootBC.currentWindowGlobal,
+ "[parent] oop frame has root as embedder global"
+ );
+ is(oopBC.embedderElement, null, "[parent] oop frame has no embedder element");
+ is(oopBC.parent, rootBC, "[parent] oop frame has root as parent");
+
+ info(`waiting for oop window global`);
+ ok(oopBC.currentWindowGlobal, "[parent] oop frame has a window global");
+
+ // Open a new window, and adopt |tab| into it.
+
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ remote: true,
+ });
+
+ info(`new chrome, parent`);
+ let newChromeBC = newWindow.docShell.browsingContext;
+ ok(newChromeBC.currentWindowGlobal, "Should have a current WindowGlobal");
+ is(
+ newChromeBC.embedderWindowGlobal,
+ null,
+ "new chrome has no embedder global"
+ );
+ is(newChromeBC.embedderElement, null, "new chrome has no embedder element");
+ is(newChromeBC.parent, null, "new chrome has no parent");
+
+ isnot(newChromeBC, chromeBC, "different chrome browsing context");
+
+ info(`adopting tab`);
+ let newTab = newWindow.gBrowser.adoptTab(tab);
+
+ is(
+ newTab.linkedBrowser.browsingContext,
+ rootBC,
+ "[parent] root browsing context survived"
+ );
+ is(
+ rootBC.embedderWindowGlobal,
+ newChromeBC.currentWindowGlobal,
+ "[parent] embedder window global updated"
+ );
+ is(
+ rootBC.embedderElement,
+ newTab.linkedBrowser,
+ "[parent] embedder element updated"
+ );
+ is(rootBC.parent, null, "[parent] root has no parent");
+
+ info(`closing window`);
+ await BrowserTestUtils.closeWindow(newWindow);
+ await BrowserTestUtils.closeWindow(fissionWindow);
+});
diff --git a/docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js b/docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js
new file mode 100644
index 0000000000..076383ad87
--- /dev/null
+++ b/docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function addFrame(url) {
+ let iframe = content.document.createElement("iframe");
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ });
+ return iframe.browsingContext;
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ // Add 15 example.com frames to the toplevel document.
+ let frames = await Promise.all(
+ Array.from({ length: 15 }).map(_ =>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ SpecialPowers.spawn(browser, ["http://example.com/"], addFrame)
+ )
+ );
+
+ // Add an example.org subframe to each example.com frame.
+ let subframes = await Promise.all(
+ Array.from({ length: 15 }).map((_, i) =>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ SpecialPowers.spawn(frames[i], ["http://example.org/"], addFrame)
+ )
+ );
+
+ Assert.deepEqual(
+ subframes[0].getAllBrowsingContextsInSubtree(),
+ [subframes[0]],
+ "Childless context only has self in subtree"
+ );
+ Assert.deepEqual(
+ frames[0].getAllBrowsingContextsInSubtree(),
+ [frames[0], subframes[0]],
+ "Single-child context has 2 contexts in subtree"
+ );
+ Assert.deepEqual(
+ browser.browsingContext.getAllBrowsingContextsInSubtree(),
+ [browser.browsingContext, ...frames, ...subframes],
+ "Toplevel context has all subtree contexts"
+ );
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_browsingContext-getWindowByName.js b/docshell/test/browser/browser_browsingContext-getWindowByName.js
new file mode 100644
index 0000000000..e57691b270
--- /dev/null
+++ b/docshell/test/browser/browser_browsingContext-getWindowByName.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function addWindow(name) {
+ var blank = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ blank.data = "about:blank";
+ let promise = BrowserTestUtils.waitForNewWindow({
+ anyWindow: true,
+ url: "about:blank",
+ });
+ Services.ww.openWindow(
+ null,
+ AppConstants.BROWSER_CHROME_URL,
+ name,
+ "chrome,dialog=no",
+ blank
+ );
+ return promise;
+}
+
+add_task(async function () {
+ let windows = [await addWindow("first"), await addWindow("second")];
+
+ for (let w of windows) {
+ isnot(w, null);
+ is(Services.ww.getWindowByName(w.name, null), w, `Found ${w.name}`);
+ }
+
+ await Promise.all(windows.map(BrowserTestUtils.closeWindow));
+});
diff --git a/docshell/test/browser/browser_browsingContext-webProgress.js b/docshell/test/browser/browser_browsingContext-webProgress.js
new file mode 100644
index 0000000000..225242be60
--- /dev/null
+++ b/docshell/test/browser/browser_browsingContext-webProgress.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ const browser = tab.linkedBrowser;
+ const aboutBlankBrowsingContext = browser.browsingContext;
+ const { webProgress } = aboutBlankBrowsingContext;
+ ok(
+ webProgress,
+ "Got a WebProgress interface on BrowsingContext in the parent process"
+ );
+ is(
+ webProgress.browsingContext,
+ browser.browsingContext,
+ "WebProgress.browsingContext refers to the right BrowsingContext"
+ );
+
+ const onLocationChanged = waitForNextLocationChange(webProgress);
+ const newLocation = "data:text/html;charset=utf-8,first-page";
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, newLocation);
+ await loaded;
+
+ const firstPageBrowsingContext = browser.browsingContext;
+ const isBfcacheInParentEnabled =
+ SpecialPowers.Services.appinfo.sessionHistoryInParent &&
+ SpecialPowers.Services.prefs.getBoolPref("fission.bfcacheInParent");
+ if (isBfcacheInParentEnabled) {
+ isnot(
+ aboutBlankBrowsingContext,
+ firstPageBrowsingContext,
+ "With bfcache in parent, navigations spawn a new BrowsingContext"
+ );
+ } else {
+ is(
+ aboutBlankBrowsingContext,
+ firstPageBrowsingContext,
+ "Without bfcache in parent, navigations reuse the same BrowsingContext"
+ );
+ }
+
+ info("Wait for onLocationChange to be fired");
+ {
+ const { browsingContext, location, request, flags } =
+ await onLocationChanged;
+ is(
+ browsingContext,
+ firstPageBrowsingContext,
+ "Location change fires on the new BrowsingContext"
+ );
+ ok(location instanceof Ci.nsIURI);
+ is(location.spec, newLocation);
+ ok(request instanceof Ci.nsIChannel);
+ is(request.URI.spec, newLocation);
+ is(flags, 0);
+ }
+
+ const onIframeLocationChanged = waitForNextLocationChange(webProgress);
+ const iframeLocation = "data:text/html;charset=utf-8,iframe";
+ const iframeBC = await SpecialPowers.spawn(
+ browser,
+ [iframeLocation],
+ async url => {
+ const iframe = content.document.createElement("iframe");
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ });
+
+ return iframe.browsingContext;
+ }
+ );
+ ok(
+ iframeBC.webProgress,
+ "The iframe BrowsingContext also exposes a WebProgress"
+ );
+ {
+ const { browsingContext, location, request, flags } =
+ await onIframeLocationChanged;
+ is(
+ browsingContext,
+ iframeBC,
+ "Iframe location change fires on the iframe BrowsingContext"
+ );
+ ok(location instanceof Ci.nsIURI);
+ is(location.spec, iframeLocation);
+ ok(request instanceof Ci.nsIChannel);
+ is(request.URI.spec, iframeLocation);
+ is(flags, 0);
+ }
+
+ const onSecondLocationChanged = waitForNextLocationChange(webProgress);
+ const onSecondPageDocumentStart = waitForNextDocumentStart(webProgress);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const secondLocation = "http://example.com/document-builder.sjs?html=com";
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, secondLocation);
+ await loaded;
+
+ const secondPageBrowsingContext = browser.browsingContext;
+ if (isBfcacheInParentEnabled) {
+ isnot(
+ firstPageBrowsingContext,
+ secondPageBrowsingContext,
+ "With bfcache in parent, navigations spawn a new BrowsingContext"
+ );
+ } else {
+ is(
+ firstPageBrowsingContext,
+ secondPageBrowsingContext,
+ "Without bfcache in parent, navigations reuse the same BrowsingContext"
+ );
+ }
+ {
+ const { browsingContext, location, request, flags } =
+ await onSecondLocationChanged;
+ is(
+ browsingContext,
+ secondPageBrowsingContext,
+ "Second location change fires on the new BrowsingContext"
+ );
+ ok(location instanceof Ci.nsIURI);
+ is(location.spec, secondLocation);
+ ok(request instanceof Ci.nsIChannel);
+ is(request.URI.spec, secondLocation);
+ is(flags, 0);
+ }
+ {
+ const { browsingContext, request } = await onSecondPageDocumentStart;
+ is(
+ browsingContext,
+ firstPageBrowsingContext,
+ "STATE_START, when navigating to another process, fires on the BrowsingContext we navigate *from*"
+ );
+ ok(request instanceof Ci.nsIChannel);
+ is(request.URI.spec, secondLocation);
+ }
+
+ const onBackLocationChanged = waitForNextLocationChange(webProgress, true);
+ const onBackDocumentStart = waitForNextDocumentStart(webProgress);
+ browser.goBack();
+
+ {
+ const { browsingContext, location, request, flags } =
+ await onBackLocationChanged;
+ is(
+ browsingContext,
+ firstPageBrowsingContext,
+ "location change, when navigating back, fires on the BrowsingContext we navigate *to*"
+ );
+ ok(location instanceof Ci.nsIURI);
+ is(location.spec, newLocation);
+ ok(request instanceof Ci.nsIChannel);
+ is(request.URI.spec, newLocation);
+ is(flags, 0);
+ }
+ {
+ const { browsingContext, request } = await onBackDocumentStart;
+ is(
+ browsingContext,
+ secondPageBrowsingContext,
+ "STATE_START, when navigating back, fires on the BrowsingContext we navigate *from*"
+ );
+ ok(request instanceof Ci.nsIChannel);
+ is(request.URI.spec, newLocation);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+function waitForNextLocationChange(webProgress, onlyTopLevel = false) {
+ return new Promise(resolve => {
+ const wpl = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ onLocationChange(progress, request, location, flags) {
+ if (onlyTopLevel && progress.browsingContext.parent) {
+ // Ignore non-toplevel.
+ return;
+ }
+ webProgress.removeProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL);
+ resolve({
+ browsingContext: progress.browsingContext,
+ location,
+ request,
+ flags,
+ });
+ },
+ };
+ // Add a strong reference to the progress listener.
+ resolve.wpl = wpl;
+ webProgress.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL);
+ });
+}
+
+function waitForNextDocumentStart(webProgress) {
+ return new Promise(resolve => {
+ const wpl = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ onStateChange(progress, request, flags, status) {
+ if (
+ flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT &&
+ flags & Ci.nsIWebProgressListener.STATE_START
+ ) {
+ webProgress.removeProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL);
+ resolve({ browsingContext: progress.browsingContext, request });
+ }
+ },
+ };
+ // Add a strong reference to the progress listener.
+ resolve.wpl = wpl;
+ webProgress.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL);
+ });
+}
diff --git a/docshell/test/browser/browser_browsing_context_attached.js b/docshell/test/browser/browser_browsing_context_attached.js
new file mode 100644
index 0000000000..60ef5e4aa6
--- /dev/null
+++ b/docshell/test/browser/browser_browsing_context_attached.js
@@ -0,0 +1,179 @@
+"use strict";
+
+const TEST_PATH =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ ) + "dummy_page.html";
+
+const TOPIC = "browsing-context-attached";
+
+async function observeAttached(callback) {
+ let attached = new Map();
+
+ function observer(subject, topic, data) {
+ is(topic, TOPIC, "observing correct topic");
+ ok(BrowsingContext.isInstance(subject), "subject to be a BrowsingContext");
+ is(typeof data, "string", "data to be a String");
+ info(`*** bc id=${subject.id}, why=${data}`);
+ attached.set(subject.id, { browsingContext: subject, why: data });
+ }
+
+ Services.obs.addObserver(observer, TOPIC);
+ try {
+ await callback();
+ return attached;
+ } finally {
+ Services.obs.removeObserver(observer, TOPIC);
+ }
+}
+
+add_task(async function toplevelForNewWindow() {
+ let win;
+
+ let attached = await observeAttached(async () => {
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ });
+
+ ok(
+ attached.has(win.browsingContext.id),
+ "got notification for window's chrome browsing context"
+ );
+ is(
+ attached.get(win.browsingContext.id).why,
+ "attach",
+ "expected reason for chrome browsing context"
+ );
+
+ ok(
+ attached.has(win.gBrowser.selectedBrowser.browsingContext.id),
+ "got notification for toplevel browsing context"
+ );
+ is(
+ attached.get(win.gBrowser.selectedBrowser.browsingContext.id).why,
+ "attach",
+ "expected reason for toplevel browsing context"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function toplevelForNewTab() {
+ let tab;
+
+ let attached = await observeAttached(async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ });
+
+ ok(
+ !attached.has(window.browsingContext.id),
+ "no notification for the current window's chrome browsing context"
+ );
+ ok(
+ attached.has(tab.linkedBrowser.browsingContext.id),
+ "got notification for toplevel browsing context"
+ );
+ is(
+ attached.get(tab.linkedBrowser.browsingContext.id).why,
+ "attach",
+ "expected reason for toplevel browsing context"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function subframe() {
+ let browsingContext;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let attached = await observeAttached(async () => {
+ browsingContext = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ let iframe = content.document.createElement("iframe");
+ content.document.body.appendChild(iframe);
+ iframe.contentWindow.location = "https://example.com/";
+ return iframe.browsingContext;
+ });
+ });
+
+ ok(
+ !attached.has(window.browsingContext.id),
+ "no notification for the current window's chrome browsing context"
+ );
+ ok(
+ !attached.has(tab.linkedBrowser.browsingContext.id),
+ "no notification for toplevel browsing context"
+ );
+ ok(
+ attached.has(browsingContext.id),
+ "got notification for frame's browsing context"
+ );
+ is(
+ attached.get(browsingContext.id).why,
+ "attach",
+ "expected reason for frame's browsing context"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function toplevelReplacedBy() {
+ let tab;
+
+ let attached = await observeAttached(async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots");
+ });
+
+ const firstContext = tab.linkedBrowser.browsingContext;
+ ok(
+ attached.has(firstContext.id),
+ "got notification for initial toplevel browsing context"
+ );
+ is(
+ attached.get(firstContext.id).why,
+ "attach",
+ "expected reason for initial toplevel browsing context"
+ );
+
+ attached = await observeAttached(async () => {
+ await loadURI(TEST_PATH);
+ });
+ const secondContext = tab.linkedBrowser.browsingContext;
+ ok(
+ attached.has(secondContext.id),
+ "got notification for replaced toplevel browsing context"
+ );
+ isnot(secondContext, firstContext, "browsing context to be replaced");
+ is(
+ attached.get(secondContext.id).why,
+ "replace",
+ "expected reason for replaced toplevel browsing context"
+ );
+ is(
+ secondContext.browserId,
+ firstContext.browserId,
+ "browserId has been kept"
+ );
+
+ attached = await observeAttached(async () => {
+ await loadURI("about:robots");
+ });
+ const thirdContext = tab.linkedBrowser.browsingContext;
+ ok(
+ attached.has(thirdContext.id),
+ "got notification for replaced toplevel browsing context"
+ );
+ isnot(thirdContext, secondContext, "browsing context to be replaced");
+ is(
+ attached.get(thirdContext.id).why,
+ "replace",
+ "expected reason for replaced toplevel browsing context"
+ );
+ is(
+ thirdContext.browserId,
+ secondContext.browserId,
+ "browserId has been kept"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/docshell/test/browser/browser_browsing_context_discarded.js b/docshell/test/browser/browser_browsing_context_discarded.js
new file mode 100644
index 0000000000..a300737d4f
--- /dev/null
+++ b/docshell/test/browser/browser_browsing_context_discarded.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const TOPIC = "browsing-context-discarded";
+
+async function observeDiscarded(browsingContexts, callback) {
+ let discarded = [];
+
+ let promise = BrowserUtils.promiseObserved(TOPIC, subject => {
+ ok(BrowsingContext.isInstance(subject), "subject to be a BrowsingContext");
+ discarded.push(subject);
+
+ return browsingContexts.every(item => discarded.includes(item));
+ });
+ await callback();
+ await promise;
+
+ return discarded;
+}
+
+add_task(async function toplevelForNewWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let browsingContext = win.gBrowser.selectedBrowser.browsingContext;
+
+ await observeDiscarded([win.browsingContext, browsingContext], async () => {
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
+
+add_task(async function toplevelForNewTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browsingContext = tab.linkedBrowser.browsingContext;
+
+ let discarded = await observeDiscarded([browsingContext], () => {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ ok(
+ !discarded.includes(window.browsingContext),
+ "no notification for the current window's chrome browsing context"
+ );
+});
+
+add_task(async function subframe() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browsingContext = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ let iframe = content.document.createElement("iframe");
+ content.document.body.appendChild(iframe);
+ iframe.contentWindow.location = "https://example.com/";
+ return iframe.browsingContext;
+ });
+
+ let discarded = await observeDiscarded([browsingContext], async () => {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ let iframe = content.document.querySelector("iframe");
+ iframe.remove();
+ });
+ });
+
+ ok(
+ !discarded.includes(tab.browsingContext),
+ "no notification for toplevel browsing context"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/docshell/test/browser/browser_bug1206879.js b/docshell/test/browser/browser_bug1206879.js
new file mode 100644
index 0000000000..38e17633b8
--- /dev/null
+++ b/docshell/test/browser/browser_bug1206879.js
@@ -0,0 +1,50 @@
+add_task(async function () {
+ let url =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ ) + "file_bug1206879.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
+
+ let numLocationChanges = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ let webprogress = content.docShell.QueryInterface(Ci.nsIWebProgress);
+ let locationChangeCount = 0;
+ let listener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ info("onLocationChange: " + aLocation.spec);
+ locationChangeCount++;
+ this.resolve();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ let locationPromise = new Promise((resolve, reject) => {
+ listener.resolve = resolve;
+ });
+ webprogress.addProgressListener(
+ listener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+
+ content.frames[0].history.pushState(null, null, "foo");
+
+ await locationPromise;
+ webprogress.removeProgressListener(listener);
+
+ return locationChangeCount;
+ }
+ );
+
+ gBrowser.removeTab(tab);
+ is(
+ numLocationChanges,
+ 1,
+ "pushState with a different URI should cause a LocationChange event."
+ );
+});
diff --git a/docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js b/docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js
new file mode 100644
index 0000000000..6b577a4d12
--- /dev/null
+++ b/docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function runTests() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about"
+ );
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ });
+
+ let browser = tab.linkedBrowser;
+
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, "about:config");
+ let href = await loaded;
+ is(href, "about:config", "Check about:config loaded");
+
+ // Using a dummy onunload listener to disable the bfcache as that can prevent
+ // the test browser load detection mechanism from working.
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "data:text/html,<body%20onunload=''><iframe></iframe></body>"
+ );
+ href = await loaded;
+ is(
+ href,
+ "data:text/html,<body%20onunload=''><iframe></iframe></body>",
+ "Check data URL loaded"
+ );
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ browser.goBack();
+ href = await loaded;
+ is(href, "about:config", "Check we've gone back to about:config");
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ browser.goForward();
+ href = await loaded;
+ is(
+ href,
+ "data:text/html,<body%20onunload=''><iframe></iframe></body>",
+ "Check we've gone forward to data URL"
+ );
+});
diff --git a/docshell/test/browser/browser_bug1328501.js b/docshell/test/browser/browser_bug1328501.js
new file mode 100644
index 0000000000..2be74b04d0
--- /dev/null
+++ b/docshell/test/browser/browser_bug1328501.js
@@ -0,0 +1,69 @@
+const HTML_URL =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug1328501.html";
+const FRAME_URL =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug1328501_frame.html";
+const FRAME_SCRIPT_URL =
+ "chrome://mochitests/content/browser/docshell/test/browser/file_bug1328501_framescript.js";
+add_task(async function testMultiFrameRestore() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.navigation.requireUserInteraction", false],
+ // Disable bfcache so that dummy_page.html doesn't enter there.
+ // The actual test page does already prevent bfcache and the test
+ // is just for http-on-opening-request handling in the child process.
+ ["browser.sessionhistory.max_total_viewers", 0],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: HTML_URL },
+ async function (browser) {
+ // Navigate 2 subframes and load about:blank.
+ let browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(
+ browser,
+ [FRAME_URL],
+ async function (FRAME_URL) {
+ function frameLoaded(frame) {
+ frame.contentWindow.location = FRAME_URL;
+ return new Promise(r => (frame.onload = r));
+ }
+ let frame1 = content.document.querySelector("#testFrame1");
+ let frame2 = content.document.querySelector("#testFrame2");
+ ok(frame1, "check found testFrame1");
+ ok(frame2, "check found testFrame2");
+ await frameLoaded(frame1);
+ await frameLoaded(frame2);
+ content.location = "dummy_page.html";
+ }
+ );
+ await browserLoaded;
+
+ // Load a frame script to query nsIDOMWindow on "http-on-opening-request",
+ // which will force about:blank content viewers being created.
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+
+ // The frame script also forwards frames-loaded.
+ let framesLoaded = BrowserTestUtils.waitForMessage(
+ browser.messageManager,
+ "test:frames-loaded"
+ );
+
+ browser.goBack();
+ await framesLoaded;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 1000));
+ await SpecialPowers.spawn(browser, [FRAME_URL], FRAME_URL => {
+ is(
+ content.document.querySelector("#testFrame1").contentWindow.location
+ .href,
+ FRAME_URL
+ );
+ is(
+ content.document.querySelector("#testFrame2").contentWindow.location
+ .href,
+ FRAME_URL
+ );
+ });
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug1347823.js b/docshell/test/browser/browser_bug1347823.js
new file mode 100644
index 0000000000..e30706b745
--- /dev/null
+++ b/docshell/test/browser/browser_bug1347823.js
@@ -0,0 +1,91 @@
+/**
+ * Test that session history's expiration tracker would remove bfcache on
+ * expiration.
+ */
+
+// With bfcache not expired.
+add_task(async function testValidCache() {
+ // Make an unrealistic large timeout.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sessionhistory.contentViewerTimeout", 86400],
+ // If Fission is disabled, the pref is no-op.
+ ["fission.bfcacheInParent", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "data:text/html;charset=utf-8,pageA1" },
+ async function (browser) {
+ // Make a simple modification for bfcache testing.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.textContent = "modified";
+ });
+
+ // Load a random page.
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "data:text/html;charset=utf-8,pageA2"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Go back and verify text content.
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ await awaitPageShow;
+ await SpecialPowers.spawn(browser, [], () => {
+ is(content.document.body.textContent, "modified");
+ });
+ }
+ );
+});
+
+// With bfcache expired.
+add_task(async function testExpiredCache() {
+ // Make bfcache timeout in 1 sec.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sessionhistory.contentViewerTimeout", 1],
+ // If Fission is disabled, the pref is no-op.
+ ["fission.bfcacheInParent", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "data:text/html;charset=utf-8,pageB1" },
+ async function (browser) {
+ // Make a simple modification for bfcache testing.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.textContent = "modified";
+ });
+
+ // Load a random page.
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "data:text/html;charset=utf-8,pageB2"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Wait for 3 times of expiration timeout, hopefully it's evicted...
+ await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ content.setTimeout(resolve, 5000);
+ });
+ });
+
+ // Go back and verify text content.
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ await awaitPageShow;
+ await SpecialPowers.spawn(browser, [], () => {
+ is(content.document.body.textContent, "pageB1");
+ });
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug134911.js b/docshell/test/browser/browser_bug134911.js
new file mode 100644
index 0000000000..9b52eeab47
--- /dev/null
+++ b/docshell/test/browser/browser_bug134911.js
@@ -0,0 +1,57 @@
+const TEXT = {
+ /* The test text decoded correctly as Shift_JIS */
+ rightText:
+ "\u30E6\u30CB\u30B3\u30FC\u30C9\u306F\u3001\u3059\u3079\u3066\u306E\u6587\u5B57\u306B\u56FA\u6709\u306E\u756A\u53F7\u3092\u4ED8\u4E0E\u3057\u307E\u3059",
+
+ enteredText1: "The quick brown fox jumps over the lazy dog",
+ enteredText2:
+ "\u03BE\u03B5\u03C3\u03BA\u03B5\u03C0\u03AC\u03B6\u03C9\u0020\u03C4\u1F74\u03BD\u0020\u03C8\u03C5\u03C7\u03BF\u03C6\u03B8\u03CC\u03C1\u03B1\u0020\u03B2\u03B4\u03B5\u03BB\u03C5\u03B3\u03BC\u03AF\u03B1",
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ rootDir + "test-form_sjis.html"
+ );
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(afterOpen);
+}
+
+function afterOpen() {
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(
+ afterChangeCharset
+ );
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [TEXT], function (TEXT) {
+ content.document.getElementById("testtextarea").value = TEXT.enteredText1;
+ content.document.getElementById("testinput").value = TEXT.enteredText2;
+ }).then(() => {
+ /* Force the page encoding to Shift_JIS */
+ BrowserForceEncodingDetection();
+ });
+}
+
+function afterChangeCharset() {
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [TEXT], function (TEXT) {
+ is(
+ content.document.getElementById("testpar").innerHTML,
+ TEXT.rightText,
+ "encoding successfully changed"
+ );
+ is(
+ content.document.getElementById("testtextarea").value,
+ TEXT.enteredText1,
+ "text preserved in <textarea>"
+ );
+ is(
+ content.document.getElementById("testinput").value,
+ TEXT.enteredText2,
+ "text preserved in <input>"
+ );
+ }).then(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/docshell/test/browser/browser_bug1415918_beforeunload_options.js b/docshell/test/browser/browser_bug1415918_beforeunload_options.js
new file mode 100644
index 0000000000..627abd90ba
--- /dev/null
+++ b/docshell/test/browser/browser_bug1415918_beforeunload_options.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+SimpleTest.requestFlakyTimeout("Needs to test a timeout");
+
+function delay(msec) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise(resolve => setTimeout(resolve, msec));
+}
+
+function allowNextNavigation(browser) {
+ return PromptTestUtils.handleNextPrompt(
+ browser,
+ { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
+ { buttonNumClick: 0 }
+ );
+}
+
+function cancelNextNavigation(browser) {
+ return PromptTestUtils.handleNextPrompt(
+ browser,
+ { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
+ { buttonNumClick: 1 }
+ );
+}
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ const permitUnloadTimeout = Services.prefs.getIntPref(
+ "dom.beforeunload_timeout_ms"
+ );
+
+ let url = TEST_PATH + "dummy_page.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser.browsingContext, [], () => {
+ content.addEventListener("beforeunload", event => {
+ event.preventDefault();
+ });
+ });
+
+ /*
+ * Check condition where beforeunload handlers request a prompt.
+ */
+
+ // Prompt is shown, user clicks OK.
+
+ let promptShownPromise = allowNextNavigation(browser);
+ ok(browser.permitUnload().permitUnload, "permit unload should be true");
+ await promptShownPromise;
+
+ // Prompt is shown, user clicks CANCEL.
+ promptShownPromise = cancelNextNavigation(browser);
+ ok(!browser.permitUnload().permitUnload, "permit unload should be false");
+ await promptShownPromise;
+
+ // Prompt is not shown, don't permit unload.
+ let promptShown = false;
+ let shownCallback = () => {
+ promptShown = true;
+ };
+
+ browser.addEventListener("DOMWillOpenModalDialog", shownCallback);
+ ok(
+ !browser.permitUnload("dontUnload").permitUnload,
+ "permit unload should be false"
+ );
+ ok(!promptShown, "prompt should not have been displayed");
+
+ // Prompt is not shown, permit unload.
+ promptShown = false;
+ ok(
+ browser.permitUnload("unload").permitUnload,
+ "permit unload should be true"
+ );
+ ok(!promptShown, "prompt should not have been displayed");
+ browser.removeEventListener("DOMWillOpenModalDialog", shownCallback);
+
+ promptShownPromise = PromptTestUtils.waitForPrompt(browser, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+
+ let promptDismissed = false;
+ let closedCallback = () => {
+ promptDismissed = true;
+ };
+
+ browser.addEventListener("DOMModalDialogClosed", closedCallback);
+
+ let promise = browser.asyncPermitUnload();
+
+ let promiseResolved = false;
+ promise.then(() => {
+ promiseResolved = true;
+ });
+
+ let dialog = await promptShownPromise;
+ ok(!promiseResolved, "Should not have resolved promise yet");
+
+ await delay(permitUnloadTimeout * 1.5);
+
+ ok(!promptDismissed, "Should not have dismissed prompt yet");
+ ok(!promiseResolved, "Should not have resolved promise yet");
+
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 });
+
+ let { permitUnload } = await promise;
+ ok(promptDismissed, "Should have dismissed prompt");
+ ok(!permitUnload, "Should not have permitted unload");
+
+ browser.removeEventListener("DOMModalDialogClosed", closedCallback);
+
+ promptShownPromise = allowNextNavigation(browser);
+
+ /*
+ * Check condition where no one requests a prompt. In all cases,
+ * permitUnload should be true, and all handlers fired.
+ */
+ url += "?1";
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ await promptShownPromise;
+
+ promptShown = false;
+ browser.addEventListener("DOMWillOpenModalDialog", shownCallback);
+
+ ok(browser.permitUnload().permitUnload, "permit unload should be true");
+ ok(!promptShown, "prompt should not have been displayed");
+
+ promptShown = false;
+ ok(
+ browser.permitUnload("dontUnload").permitUnload,
+ "permit unload should be true"
+ );
+ ok(!promptShown, "prompt should not have been displayed");
+
+ promptShown = false;
+ ok(
+ browser.permitUnload("unload").permitUnload,
+ "permit unload should be true"
+ );
+ ok(!promptShown, "prompt should not have been displayed");
+
+ browser.removeEventListener("DOMWillOpenModalDialog", shownCallback);
+
+ await BrowserTestUtils.removeTab(tab, { skipPermitUnload: true });
+});
diff --git a/docshell/test/browser/browser_bug1543077-3.js b/docshell/test/browser/browser_bug1543077-3.js
new file mode 100644
index 0000000000..7cef4aef10
--- /dev/null
+++ b/docshell/test/browser/browser_bug1543077-3.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1543077-3.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u3042"),
+ 136,
+ "Parent doc should be ISO-2022-JP initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u3042"),
+ 92,
+ "Child doc should be ISO-2022-JP initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u3042"),
+ 136,
+ "Parent doc should decode as ISO-2022-JP subsequently"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u3042"),
+ 92,
+ "Child doc should decode as ISO-2022-JP subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "ISO-2022-JP",
+ "Parent doc should report ISO-2022-JP subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "ISO-2022-JP",
+ "Child doc should report ISO-2022-JP subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1594938.js b/docshell/test/browser/browser_bug1594938.js
new file mode 100644
index 0000000000..569afe6901
--- /dev/null
+++ b/docshell/test/browser/browser_bug1594938.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test for Bug 1594938
+ *
+ * If a session history listener blocks reloads we shouldn't crash.
+ */
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "https://example.com/" },
+ async function (browser) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let history = this.content.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ ).sessionHistory;
+
+ let testDone = {};
+ testDone.promise = new Promise(resolve => {
+ testDone.resolve = resolve;
+ });
+
+ let listenerCalled = false;
+ let listener = {
+ OnHistoryNewEntry: aNewURI => {},
+ OnHistoryReload: () => {
+ listenerCalled = true;
+ this.content.setTimeout(() => {
+ testDone.resolve();
+ });
+ return false;
+ },
+ OnHistoryGotoIndex: () => {},
+ OnHistoryPurge: () => {},
+ OnHistoryReplaceEntry: () => {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ history.legacySHistory.addSHistoryListener(listener);
+
+ history.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ await testDone.promise;
+
+ Assert.ok(listenerCalled, "reloads were blocked");
+
+ history.legacySHistory.removeSHistoryListener(listener);
+ });
+
+ return;
+ }
+
+ let history = browser.browsingContext.sessionHistory;
+
+ let testDone = {};
+ testDone.promise = new Promise(resolve => {
+ testDone.resolve = resolve;
+ });
+
+ let listenerCalled = false;
+ let listener = {
+ OnHistoryNewEntry: aNewURI => {},
+ OnHistoryReload: () => {
+ listenerCalled = true;
+ setTimeout(() => {
+ testDone.resolve();
+ });
+ return false;
+ },
+ OnHistoryGotoIndex: () => {},
+ OnHistoryPurge: () => {},
+ OnHistoryReplaceEntry: () => {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ history.addSHistoryListener(listener);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let history = this.content.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ ).sessionHistory;
+ history.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ });
+ await testDone.promise;
+
+ Assert.ok(listenerCalled, "reloads were blocked");
+
+ history.removeSHistoryListener(listener);
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug1622420.js b/docshell/test/browser/browser_bug1622420.js
new file mode 100644
index 0000000000..81026611e4
--- /dev/null
+++ b/docshell/test/browser/browser_bug1622420.js
@@ -0,0 +1,31 @@
+const ACTOR = "Bug1622420";
+
+add_task(async function test() {
+ let base = getRootDirectory(gTestPath).slice(0, -1);
+ ChromeUtils.registerWindowActor(ACTOR, {
+ allFrames: true,
+ child: {
+ esModuleURI: `${base}/Bug1622420Child.sys.mjs`,
+ },
+ });
+
+ registerCleanupFunction(async () => {
+ gBrowser.removeTab(tab);
+
+ ChromeUtils.unregisterWindowActor(ACTOR);
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/docshell/test/browser/file_bug1622420.html"
+ );
+ let childBC = tab.linkedBrowser.browsingContext.children[0];
+ let success = await childBC.currentWindowGlobal
+ .getActor(ACTOR)
+ .sendQuery("hasWindowContextForTopBC");
+ ok(
+ success,
+ "Should have a WindowContext for the top BrowsingContext in the process of a child BrowsingContext"
+ );
+});
diff --git a/docshell/test/browser/browser_bug1648464-1.js b/docshell/test/browser/browser_bug1648464-1.js
new file mode 100644
index 0000000000..c2a8093a3d
--- /dev/null
+++ b/docshell/test/browser/browser_bug1648464-1.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1648464-1.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u00A4"),
+ 146,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u00A4"),
+ 95,
+ "Child doc should be windows-1252 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u3042"),
+ 146,
+ "Parent doc should decode as EUC-JP subsequently"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u3042"),
+ 95,
+ "Child doc should decode as EUC-JP subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "EUC-JP",
+ "Parent doc should report EUC-JP subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "EUC-JP",
+ "Child doc should report EUC-JP subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1673702.js b/docshell/test/browser/browser_bug1673702.js
new file mode 100644
index 0000000000..c32c38fe45
--- /dev/null
+++ b/docshell/test/browser/browser_bug1673702.js
@@ -0,0 +1,27 @@
+const DUMMY =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/docshell/test/browser/dummy_page.html";
+const JSON =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/docshell/test/browser/file_bug1673702.json";
+
+add_task(async function test_backAndReload() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: DUMMY },
+ async function (browser) {
+ info("Start JSON load.");
+ BrowserTestUtils.startLoadingURIString(browser, JSON);
+ await BrowserTestUtils.waitForLocationChange(gBrowser, JSON);
+
+ info("JSON load has started, go back.");
+ browser.goBack();
+ await BrowserTestUtils.browserStopped(browser);
+
+ info("Reload.");
+ BrowserReload();
+ await BrowserTestUtils.waitForLocationChange(gBrowser);
+
+ is(browser.documentURI.spec, DUMMY);
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug1674464.js b/docshell/test/browser/browser_bug1674464.js
new file mode 100644
index 0000000000..ff9e990a40
--- /dev/null
+++ b/docshell/test/browser/browser_bug1674464.js
@@ -0,0 +1,38 @@
+const DUMMY_1 =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/docshell/test/browser/dummy_page.html";
+const DUMMY_2 =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/docshell/test/browser/dummy_page.html";
+
+add_task(async function test_backAndReload() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: DUMMY_1 },
+ async function (browser) {
+ await BrowserTestUtils.crashFrame(browser);
+
+ info("Start second load.");
+ BrowserTestUtils.startLoadingURIString(browser, DUMMY_2);
+ await BrowserTestUtils.waitForLocationChange(gBrowser, DUMMY_2);
+
+ browser.goBack();
+ await BrowserTestUtils.waitForLocationChange(gBrowser);
+
+ is(
+ browser.browsingContext.childSessionHistory.index,
+ 0,
+ "We should have gone back to the first page"
+ );
+ is(
+ browser.browsingContext.childSessionHistory.count,
+ 2,
+ "If a tab crashes after a load has finished we shouldn't have an entry for about:tabcrashed"
+ );
+ is(
+ browser.documentURI.spec,
+ DUMMY_1,
+ "If a tab crashes after a load has finished we shouldn't have an entry for about:tabcrashed"
+ );
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug1688368-1.js b/docshell/test/browser/browser_bug1688368-1.js
new file mode 100644
index 0000000000..04fc3dd9a8
--- /dev/null
+++ b/docshell/test/browser/browser_bug1688368-1.js
@@ -0,0 +1,24 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1688368-1.sjs",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.body.textContent.indexOf("â"),
+ 0,
+ "Doc should be windows-1252 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.body.textContent.indexOf("☃"),
+ 0,
+ "Doc should be UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1691153.js b/docshell/test/browser/browser_bug1691153.js
new file mode 100644
index 0000000000..ed4969dae8
--- /dev/null
+++ b/docshell/test/browser/browser_bug1691153.js
@@ -0,0 +1,73 @@
+"use strict";
+
+add_task(async () => {
+ const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ );
+
+ const HTML_URI = TEST_PATH + "file_bug1691153.html";
+
+ // Opening the page that contains the iframe
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browser = tab.linkedBrowser;
+ let browserLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ HTML_URI,
+ true
+ );
+ info("new tab loaded");
+
+ BrowserTestUtils.startLoadingURIString(browser, HTML_URI);
+ await browserLoaded;
+ info("The test page has loaded!");
+
+ let first_message_promise = SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let blobPromise = new Promise((resolve, reject) => {
+ content.addEventListener("message", event => {
+ if (event.data.bloburl) {
+ info("Sanity check: recvd blob URL as " + event.data.bloburl);
+ resolve(event.data.bloburl);
+ }
+ });
+ });
+ content.postMessage("getblob", "*");
+ return blobPromise;
+ }
+ );
+ info("The test page has loaded!");
+ let blob_url = await first_message_promise;
+
+ Assert.ok(blob_url.startsWith("blob:"), "Sanity check: recvd blob");
+ info(`Received blob URL message from content: ${blob_url}`);
+ // try to open the blob in a new tab, manually created by the user
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ blob_url,
+ true,
+ false,
+ true
+ );
+
+ let principal = gBrowser.selectedTab.linkedBrowser._contentPrincipal;
+ Assert.ok(
+ !principal.isSystemPrincipal,
+ "Newly opened blob shouldn't be Systemprincipal"
+ );
+ Assert.ok(
+ !principal.isExpandedPrincipal,
+ "Newly opened blob shouldn't be ExpandedPrincipal"
+ );
+ Assert.ok(
+ principal.isContentPrincipal,
+ "Newly opened blob tab should be ContentPrincipal"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/docshell/test/browser/browser_bug1705872.js b/docshell/test/browser/browser_bug1705872.js
new file mode 100644
index 0000000000..87d10b4526
--- /dev/null
+++ b/docshell/test/browser/browser_bug1705872.js
@@ -0,0 +1,74 @@
+"use strict";
+
+async function doLoadAndGoBack(browser, ext) {
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, "https://example.com/");
+ await ext.awaitMessage("redir-handled");
+ await loaded;
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.history.back();
+ });
+ return pageShownPromise;
+}
+
+add_task(async function test_back() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "https://example.com/"],
+ web_accessible_resources: ["test.html"],
+ },
+ files: {
+ "test.html":
+ "<!DOCTYPE html><html><head><title>Test add-on</title></head><body></body></html>",
+ },
+ background: () => {
+ let { browser } = this;
+ browser.webRequest.onHeadersReceived.addListener(
+ details => {
+ if (details.statusCode != 200) {
+ return undefined;
+ }
+ browser.test.sendMessage("redir-handled");
+ return { redirectUrl: browser.runtime.getURL("test.html") };
+ },
+ {
+ urls: ["https://example.com/"],
+ types: ["main_frame"],
+ },
+ ["blocking"]
+ );
+ },
+ });
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab("about:home", async function (browser) {
+ await doLoadAndGoBack(browser, extension);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.documentURI,
+ "about:home",
+ "Gone back to the right page"
+ );
+ });
+
+ await doLoadAndGoBack(browser, extension);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.documentURI,
+ "about:home",
+ "Gone back to the right page"
+ );
+ });
+ });
+
+ await extension.unload();
+});
diff --git a/docshell/test/browser/browser_bug1716290-1.js b/docshell/test/browser/browser_bug1716290-1.js
new file mode 100644
index 0000000000..48add6f0eb
--- /dev/null
+++ b/docshell/test/browser/browser_bug1716290-1.js
@@ -0,0 +1,24 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1716290-1.sjs",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.characterSet,
+ "Shift_JIS",
+ "Doc should report Shift_JIS initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Doc should report windows-1252 subsequently (detector should override header)"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1716290-2.js b/docshell/test/browser/browser_bug1716290-2.js
new file mode 100644
index 0000000000..a33c61f076
--- /dev/null
+++ b/docshell/test/browser/browser_bug1716290-2.js
@@ -0,0 +1,24 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1716290-2.sjs",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.characterSet,
+ "Shift_JIS",
+ "Doc should report Shift_JIS initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Doc should report windows-1252 subsequently (detector should override meta resolving to the replacement encoding)"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1716290-3.js b/docshell/test/browser/browser_bug1716290-3.js
new file mode 100644
index 0000000000..f7e6ca5bbd
--- /dev/null
+++ b/docshell/test/browser/browser_bug1716290-3.js
@@ -0,0 +1,24 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1716290-3.sjs",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.characterSet,
+ "Shift_JIS",
+ "Doc should report Shift_JIS initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.characterSet,
+ "replacement",
+ "Doc should report replacement subsequently (non-ASCII-compatible HTTP header should override detector)"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1716290-4.js b/docshell/test/browser/browser_bug1716290-4.js
new file mode 100644
index 0000000000..ab0d46d7ff
--- /dev/null
+++ b/docshell/test/browser/browser_bug1716290-4.js
@@ -0,0 +1,24 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1716290-4.sjs",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.characterSet,
+ "Shift_JIS",
+ "Doc should report Shift_JIS initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.characterSet,
+ "UTF-16BE",
+ "Doc should report UTF-16BE subsequently (BOM should override detector)"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1719178.js b/docshell/test/browser/browser_bug1719178.js
new file mode 100644
index 0000000000..6c4fc2d15f
--- /dev/null
+++ b/docshell/test/browser/browser_bug1719178.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+SimpleTest.requestFlakyTimeout(
+ "The test needs to let objects die asynchronously."
+);
+
+add_task(async function test_accessing_shistory() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+ let sh = tab.linkedBrowser.browsingContext.sessionHistory;
+ ok(sh, "Should have SessionHistory object");
+ gBrowser.removeTab(tab);
+ tab = null;
+ for (let i = 0; i < 5; ++i) {
+ SpecialPowers.Services.obs.notifyObservers(
+ null,
+ "memory-pressure",
+ "heap-minimize"
+ );
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+ await new Promise(function (r) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(r, 50);
+ });
+ }
+
+ try {
+ sh.reloadCurrentEntry();
+ } catch (ex) {}
+ ok(true, "This test shouldn't crash.");
+});
diff --git a/docshell/test/browser/browser_bug1736248-1.js b/docshell/test/browser/browser_bug1736248-1.js
new file mode 100644
index 0000000000..d9cbe4fd85
--- /dev/null
+++ b/docshell/test/browser/browser_bug1736248-1.js
@@ -0,0 +1,34 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug1736248-1.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u00C3"),
+ 1064,
+ "Doc should be windows-1252 initially"
+ );
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Doc should report windows-1252 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u00E4"),
+ 1064,
+ "Doc should be UTF-8 subsequently"
+ );
+ is(
+ content.document.characterSet,
+ "UTF-8",
+ "Doc should report UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug1757005.js b/docshell/test/browser/browser_bug1757005.js
new file mode 100644
index 0000000000..6b257a9115
--- /dev/null
+++ b/docshell/test/browser/browser_bug1757005.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // (1) Load one page with bfcache disabled and another one with bfcache enabled.
+ // (2) Check that BrowsingContext.getCurrentTopByBrowserId(browserId) returns
+ // the expected browsing context both in the parent process and in the child process.
+ // (3) Go back and then forward
+ // (4) Run the same checks as in step 2 again.
+
+ let url1 = "data:text/html,<body onunload='/* disable bfcache */'>";
+ let url2 = "data:text/html,page2";
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: url1,
+ },
+ async function (browser) {
+ info("Initial load");
+
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, url2);
+ await loaded;
+ info("Second page loaded");
+
+ let browserId = browser.browserId;
+ ok(!!browser.browsingContext, "Should have a BrowsingContext. (1)");
+ is(
+ BrowsingContext.getCurrentTopByBrowserId(browserId),
+ browser.browsingContext,
+ "Should get the correct browsingContext(1)"
+ );
+
+ await ContentTask.spawn(browser, browserId, async function (browserId) {
+ Assert.ok(
+ BrowsingContext.getCurrentTopByBrowserId(browserId) ==
+ docShell.browsingContext
+ );
+ Assert.ok(docShell.browsingContext.browserId == browserId);
+ });
+
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ await awaitPageShow;
+ info("Back");
+
+ awaitPageShow = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ browser.goForward();
+ await awaitPageShow;
+ info("Forward");
+
+ ok(!!browser.browsingContext, "Should have a BrowsingContext. (2)");
+ is(
+ BrowsingContext.getCurrentTopByBrowserId(browserId),
+ browser.browsingContext,
+ "Should get the correct BrowsingContext. (2)"
+ );
+
+ await ContentTask.spawn(browser, browserId, async function (browserId) {
+ Assert.ok(
+ BrowsingContext.getCurrentTopByBrowserId(browserId) ==
+ docShell.browsingContext
+ );
+ Assert.ok(docShell.browsingContext.browserId == browserId);
+ });
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug1769189.js b/docshell/test/browser/browser_bug1769189.js
new file mode 100644
index 0000000000..08cb4f9002
--- /dev/null
+++ b/docshell/test/browser/browser_bug1769189.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_beforeUnload_and_replaceState() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html,<script>window.addEventListener('beforeunload', () => { window.history.replaceState(true, ''); });</script>",
+ },
+ async function (browser) {
+ let initialState = await SpecialPowers.spawn(browser, [], () => {
+ return content.history.state;
+ });
+
+ is(initialState, null, "history.state should be initially null.");
+
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ BrowserReload();
+ await awaitPageShow;
+
+ let updatedState = await SpecialPowers.spawn(browser, [], () => {
+ return content.history.state;
+ });
+ is(updatedState, true, "history.state should have been updated.");
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug1798780.js b/docshell/test/browser/browser_bug1798780.js
new file mode 100644
index 0000000000..a9ad16ee35
--- /dev/null
+++ b/docshell/test/browser/browser_bug1798780.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// The test loads an initial page and then another page which does enough
+// fragment navigations so that when going back to the initial page and then
+// forward to the last page, the initial page is evicted from the bfcache.
+add_task(async function testBFCacheEviction() {
+ // Make an unrealistic large timeout.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionhistory.contentViewerTimeout", 86400]],
+ });
+
+ const uri = "data:text/html,initial page";
+ const uri2 =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: uri },
+ async function (browser) {
+ BrowserTestUtils.startLoadingURIString(browser, uri2);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.location.hash = "1";
+ content.location.hash = "2";
+ content.location.hash = "3";
+ content.history.go(-4);
+ });
+
+ await awaitPageShow;
+
+ let awaitPageShow2 = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.history.go(4);
+ });
+ await awaitPageShow2;
+ ok(true, "Didn't time out.");
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug234628-1.js b/docshell/test/browser/browser_bug234628-1.js
new file mode 100644
index 0000000000..566da65bca
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-1.js
@@ -0,0 +1,47 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-1.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 129,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 85,
+ "Child doc should be windows-1252 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 129,
+ "Parent doc should be windows-1252 subsequently"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 85,
+ "Child doc should be windows-1252 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "windows-1252",
+ "Child doc should report windows-1252 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-10.js b/docshell/test/browser/browser_bug234628-10.js
new file mode 100644
index 0000000000..8fb51cf27c
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-10.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-10.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 151,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 71,
+ "Child doc should be utf-8 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 151,
+ "Parent doc should be windows-1252 initially"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 71,
+ "Child doc should decode as utf-8 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-8",
+ "Child doc should report UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-11.js b/docshell/test/browser/browser_bug234628-11.js
new file mode 100644
index 0000000000..d11645ff76
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-11.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-11.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 193,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 107,
+ "Child doc should be utf-8 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 193,
+ "Parent doc should be windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 107,
+ "Child doc should decode as utf-8 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-8",
+ "Child doc should report UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-2.js b/docshell/test/browser/browser_bug234628-2.js
new file mode 100644
index 0000000000..da93dc2ac2
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-2.js
@@ -0,0 +1,49 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-2.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 129,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf(
+ "\u00E2\u201A\u00AC"
+ ),
+ 78,
+ "Child doc should be windows-1252 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 129,
+ "Parent doc should be windows-1252 subsequently"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 78,
+ "Child doc should be UTF-8 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-8",
+ "Child doc should report UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-3.js b/docshell/test/browser/browser_bug234628-3.js
new file mode 100644
index 0000000000..8a143b51a6
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-3.js
@@ -0,0 +1,47 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-3.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 118,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 73,
+ "Child doc should be utf-8 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 118,
+ "Parent doc should be windows-1252 subsequently"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 73,
+ "Child doc should be utf-8 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-8",
+ "Child doc should report UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-4.js b/docshell/test/browser/browser_bug234628-4.js
new file mode 100644
index 0000000000..19ec0f8dbf
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-4.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-4.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 132,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 79,
+ "Child doc should be utf-8 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 132,
+ "Parent doc should decode as windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 79,
+ "Child doc should decode as utf-8 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-8",
+ "Child doc should report UTF-8 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-5.js b/docshell/test/browser/browser_bug234628-5.js
new file mode 100644
index 0000000000..77753ed78d
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-5.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-5.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 146,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 87,
+ "Child doc should be utf-16 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 146,
+ "Parent doc should be windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 87,
+ "Child doc should decode as utf-16 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-16LE",
+ "Child doc should report UTF-16LE subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-6.js b/docshell/test/browser/browser_bug234628-6.js
new file mode 100644
index 0000000000..88ff6c1a82
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-6.js
@@ -0,0 +1,47 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug234628-6.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 190,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 109,
+ "Child doc should be utf-16 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 190,
+ "Parent doc should be windows-1252 subsequently"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 109,
+ "Child doc should be utf-16 subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "windows-1252",
+ "Parent doc should report windows-1252 subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "UTF-16BE",
+ "Child doc should report UTF-16 subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-8.js b/docshell/test/browser/browser_bug234628-8.js
new file mode 100644
index 0000000000..024a3d4d64
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-8.js
@@ -0,0 +1,18 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetCheck(rootDir + "file_bug234628-8.html", afterOpen);
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u0402"),
+ 156,
+ "Parent doc should be windows-1251"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u0402"),
+ 99,
+ "Child doc should be windows-1251"
+ );
+}
diff --git a/docshell/test/browser/browser_bug234628-9.js b/docshell/test/browser/browser_bug234628-9.js
new file mode 100644
index 0000000000..ceb7dc4e63
--- /dev/null
+++ b/docshell/test/browser/browser_bug234628-9.js
@@ -0,0 +1,18 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetCheck(rootDir + "file_bug234628-9.html", afterOpen);
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u20AC"),
+ 145,
+ "Parent doc should be UTF-16"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u20AC"),
+ 96,
+ "Child doc should be windows-1252"
+ );
+}
diff --git a/docshell/test/browser/browser_bug349769.js b/docshell/test/browser/browser_bug349769.js
new file mode 100644
index 0000000000..33fca715cf
--- /dev/null
+++ b/docshell/test/browser/browser_bug349769.js
@@ -0,0 +1,79 @@
+add_task(async function test() {
+ const uris = [undefined, "about:blank"];
+
+ function checkContentProcess(newBrowser, uri) {
+ return ContentTask.spawn(newBrowser, [uri], async function (uri) {
+ var prin = content.document.nodePrincipal;
+ Assert.notEqual(
+ prin,
+ null,
+ "Loaded principal must not be null when adding " + uri
+ );
+ Assert.notEqual(
+ prin,
+ undefined,
+ "Loaded principal must not be undefined when loading " + uri
+ );
+
+ Assert.equal(
+ prin.isSystemPrincipal,
+ false,
+ "Loaded principal must not be system when loading " + uri
+ );
+ });
+ }
+
+ for (var uri of uris) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser },
+ async function (newBrowser) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(newBrowser);
+ BrowserTestUtils.startLoadingURIString(newBrowser, uri);
+
+ var prin = newBrowser.contentPrincipal;
+ isnot(
+ prin,
+ null,
+ "Forced principal must not be null when loading " + uri
+ );
+ isnot(
+ prin,
+ undefined,
+ "Forced principal must not be undefined when loading " + uri
+ );
+ is(
+ prin.isSystemPrincipal,
+ false,
+ "Forced principal must not be system when loading " + uri
+ );
+
+ // Belt-and-suspenders e10s check: make sure that the same checks hold
+ // true in the content process.
+ await checkContentProcess(newBrowser, uri);
+
+ await loadedPromise;
+
+ prin = newBrowser.contentPrincipal;
+ isnot(
+ prin,
+ null,
+ "Loaded principal must not be null when adding " + uri
+ );
+ isnot(
+ prin,
+ undefined,
+ "Loaded principal must not be undefined when loading " + uri
+ );
+ is(
+ prin.isSystemPrincipal,
+ false,
+ "Loaded principal must not be system when loading " + uri
+ );
+
+ // Belt-and-suspenders e10s check: make sure that the same checks hold
+ // true in the content process.
+ await checkContentProcess(newBrowser, uri);
+ }
+ );
+ }
+});
diff --git a/docshell/test/browser/browser_bug388121-1.js b/docshell/test/browser/browser_bug388121-1.js
new file mode 100644
index 0000000000..07c476f11b
--- /dev/null
+++ b/docshell/test/browser/browser_bug388121-1.js
@@ -0,0 +1,22 @@
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (newBrowser) {
+ await SpecialPowers.spawn(newBrowser, [], async function () {
+ var prin = content.document.nodePrincipal;
+ Assert.notEqual(prin, null, "Loaded principal must not be null");
+ Assert.notEqual(
+ prin,
+ undefined,
+ "Loaded principal must not be undefined"
+ );
+
+ Assert.equal(
+ prin.isSystemPrincipal,
+ false,
+ "Loaded principal must not be system"
+ );
+ });
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug388121-2.js b/docshell/test/browser/browser_bug388121-2.js
new file mode 100644
index 0000000000..695ff1079c
--- /dev/null
+++ b/docshell/test/browser/browser_bug388121-2.js
@@ -0,0 +1,74 @@
+function test() {
+ waitForExplicitFinish();
+
+ var w;
+ var iteration = 1;
+ const uris = ["", "about:blank"];
+ var uri;
+ var origWgp;
+
+ function testLoad() {
+ let wgp = w.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+ if (wgp == origWgp) {
+ // Go back to polling
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(testLoad, 10);
+ return;
+ }
+ var prin = wgp.documentPrincipal;
+ isnot(prin, null, "Loaded principal must not be null when adding " + uri);
+ isnot(
+ prin,
+ undefined,
+ "Loaded principal must not be undefined when loading " + uri
+ );
+ is(
+ prin.isSystemPrincipal,
+ false,
+ "Loaded principal must not be system when loading " + uri
+ );
+ w.close();
+
+ if (iteration == uris.length) {
+ finish();
+ } else {
+ ++iteration;
+ doTest();
+ }
+ }
+
+ function doTest() {
+ uri = uris[iteration - 1];
+ window.open(uri, "_blank", "width=10,height=10,noopener");
+ w = Services.wm.getMostRecentWindow("navigator:browser");
+ origWgp = w.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+ var prin = origWgp.documentPrincipal;
+ if (!uri) {
+ uri = undefined;
+ }
+ isnot(prin, null, "Forced principal must not be null when loading " + uri);
+ isnot(
+ prin,
+ undefined,
+ "Forced principal must not be undefined when loading " + uri
+ );
+ is(
+ prin.isSystemPrincipal,
+ false,
+ "Forced principal must not be system when loading " + uri
+ );
+ if (uri == undefined) {
+ // No actual load here, so just move along.
+ w.close();
+ ++iteration;
+ doTest();
+ } else {
+ // Need to poll, because load listeners on the content window won't
+ // survive the load.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(testLoad, 10);
+ }
+ }
+
+ doTest();
+}
diff --git a/docshell/test/browser/browser_bug420605.js b/docshell/test/browser/browser_bug420605.js
new file mode 100644
index 0000000000..152d4faedb
--- /dev/null
+++ b/docshell/test/browser/browser_bug420605.js
@@ -0,0 +1,131 @@
+/* Test for Bug 420605
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=420605
+ */
+
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+add_task(async function test() {
+ var pageurl =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug420605.html";
+ var fragmenturl =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug420605.html#firefox";
+
+ /* Queries nsINavHistoryService and returns a single history entry
+ * for a given URI */
+ function getNavHistoryEntry(aURI) {
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ options.maxResults = 1;
+
+ var query = PlacesUtils.history.getNewQuery();
+ query.uri = aURI;
+ var result = PlacesUtils.history.executeQuery(query, options);
+ result.root.containerOpen = true;
+
+ if (!result.root.childCount) {
+ return null;
+ }
+ return result.root.getChild(0);
+ }
+
+ // We'll save the favicon URL of the orignal page here and check that the
+ // page with a hash has the same favicon.
+ var originalFavicon;
+
+ // Control flow in this test is a bit complicated.
+ //
+ // When the page loads, onPageLoad (the DOMContentLoaded handler) and
+ // favicon-changed are both called, in some order. Once
+ // they've both run, we click a fragment link in the content page
+ // (clickLinkIfReady), which should trigger another favicon-changed event,
+ // this time for the fragment's URL.
+
+ var _clickLinkTimes = 0;
+ function clickLinkIfReady() {
+ _clickLinkTimes++;
+ if (_clickLinkTimes == 2) {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#firefox-link",
+ {},
+ gBrowser.selectedBrowser
+ );
+ }
+ }
+
+ function onPageLoad() {
+ clickLinkIfReady();
+ }
+
+ // Make sure neither of the test pages haven't been loaded before.
+ var info = getNavHistoryEntry(makeURI(pageurl));
+ ok(!info, "The test page must not have been visited already.");
+ info = getNavHistoryEntry(makeURI(fragmenturl));
+ ok(!info, "The fragment test page must not have been visited already.");
+
+ let promiseIcon1 = PlacesTestUtils.waitForNotification(
+ "favicon-changed",
+ events =>
+ events.some(e => {
+ if (e.url == pageurl) {
+ ok(
+ e.faviconUrl,
+ "Favicon value is not null for page without fragment."
+ );
+ originalFavicon = e.faviconUrl;
+
+ // Now that the favicon has loaded, click on fragment link.
+ // This should trigger the |case fragmenturl| below.
+ clickLinkIfReady();
+ return true;
+ }
+ return false;
+ })
+ );
+ let promiseIcon2 = PlacesTestUtils.waitForNotification(
+ "favicon-changed",
+ events =>
+ events.some(e => {
+ if (e.url == fragmenturl) {
+ // If the fragment URL's favicon isn't set, this branch won't
+ // be called and the test will time out.
+ is(
+ e.faviconUrl,
+ originalFavicon,
+ "New favicon should be same as original favicon."
+ );
+ ok(
+ e.faviconUrl,
+ "Favicon value is not null for page without fragment."
+ );
+ originalFavicon = e.faviconUrl;
+
+ // Now that the favicon has loaded, click on fragment link.
+ // This should trigger the |case fragmenturl| below.
+ clickLinkIfReady();
+ return true;
+ }
+ return false;
+ })
+ );
+
+ // Now open the test page in a new tab.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "DOMContentLoaded",
+ true
+ ).then(onPageLoad);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, pageurl);
+
+ await promiseIcon1;
+ await promiseIcon2;
+
+ // Let's explicitly check that we can get the favicon
+ // from nsINavHistoryService now.
+ info = getNavHistoryEntry(makeURI(fragmenturl));
+ ok(info, "There must be a history entry for the fragment.");
+ ok(info.icon, "The history entry must have an associated favicon.");
+ gBrowser.removeCurrentTab();
+});
diff --git a/docshell/test/browser/browser_bug422543.js b/docshell/test/browser/browser_bug422543.js
new file mode 100644
index 0000000000..d2b6370d03
--- /dev/null
+++ b/docshell/test/browser/browser_bug422543.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ACTOR = "Bug422543";
+
+let getActor = browser => {
+ return browser.browsingContext.currentWindowGlobal.getActor(ACTOR);
+};
+
+add_task(async function runTests() {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await setupAsync();
+ let browser = gBrowser.selectedBrowser;
+ // Now that we're set up, initialize our frame script.
+ await checkListenersAsync("initial", "listeners initialized");
+
+ // Check if all history listeners are always notified.
+ info("# part 1");
+ await whenPageShown(browser, () =>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.startLoadingURIString(browser, "http://www.example.com/")
+ );
+ await checkListenersAsync("newentry", "shistory has a new entry");
+ ok(browser.canGoBack, "we can go back");
+
+ await whenPageShown(browser, () => browser.goBack());
+ await checkListenersAsync("gotoindex", "back to the first shentry");
+ ok(browser.canGoForward, "we can go forward");
+
+ await whenPageShown(browser, () => browser.goForward());
+ await checkListenersAsync("gotoindex", "forward to the second shentry");
+
+ await whenPageShown(browser, () => browser.reload());
+ await checkListenersAsync("reload", "current shentry reloaded");
+
+ await whenPageShown(browser, () => browser.gotoIndex(0));
+ await checkListenersAsync("gotoindex", "back to the first index");
+
+ // Check nsISHistory.notifyOnHistoryReload
+ info("# part 2");
+ ok(await notifyReloadAsync(), "reloading has not been canceled");
+ await checkListenersAsync("reload", "saw the reload notification");
+
+ // Let the first listener cancel the reload action.
+ info("# part 3");
+ await resetListenersAsync();
+ await setListenerRetvalAsync(0, false);
+ ok(!(await notifyReloadAsync()), "reloading has been canceled");
+ await checkListenersAsync("reload", "saw the reload notification");
+
+ // Let both listeners cancel the reload action.
+ info("# part 4");
+ await resetListenersAsync();
+ await setListenerRetvalAsync(1, false);
+ ok(!(await notifyReloadAsync()), "reloading has been canceled");
+ await checkListenersAsync("reload", "saw the reload notification");
+
+ // Let the second listener cancel the reload action.
+ info("# part 5");
+ await resetListenersAsync();
+ await setListenerRetvalAsync(0, true);
+ ok(!(await notifyReloadAsync()), "reloading has been canceled");
+ await checkListenersAsync("reload", "saw the reload notification");
+
+ function sendQuery(message, arg = {}) {
+ return getActor(gBrowser.selectedBrowser).sendQuery(message, arg);
+ }
+
+ function checkListenersAsync(aLast, aMessage) {
+ return sendQuery("getListenerStatus").then(listenerStatuses => {
+ is(listenerStatuses[0], aLast, aMessage);
+ is(listenerStatuses[1], aLast, aMessage);
+ });
+ }
+
+ function resetListenersAsync() {
+ return sendQuery("resetListeners");
+ }
+
+ function notifyReloadAsync() {
+ return sendQuery("notifyReload").then(({ rval }) => {
+ return rval;
+ });
+ }
+
+ function setListenerRetvalAsync(num, val) {
+ return sendQuery("setRetval", { num, val });
+ }
+
+ async function setupAsync() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888"
+ );
+
+ let base = getRootDirectory(gTestPath).slice(0, -1);
+ ChromeUtils.registerWindowActor(ACTOR, {
+ child: {
+ esModuleURI: `${base}/Bug422543Child.sys.mjs`,
+ },
+ });
+
+ registerCleanupFunction(async () => {
+ await sendQuery("cleanup");
+ gBrowser.removeTab(tab);
+
+ ChromeUtils.unregisterWindowActor(ACTOR);
+ });
+
+ await sendQuery("init");
+ }
+ return;
+ }
+
+ await setup();
+ let browser = gBrowser.selectedBrowser;
+ // Now that we're set up, initialize our frame script.
+ checkListeners("initial", "listeners initialized");
+
+ // Check if all history listeners are always notified.
+ info("# part 1");
+ await whenPageShown(browser, () =>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.startLoadingURIString(browser, "http://www.example.com/")
+ );
+ checkListeners("newentry", "shistory has a new entry");
+ ok(browser.canGoBack, "we can go back");
+
+ await whenPageShown(browser, () => browser.goBack());
+ checkListeners("gotoindex", "back to the first shentry");
+ ok(browser.canGoForward, "we can go forward");
+
+ await whenPageShown(browser, () => browser.goForward());
+ checkListeners("gotoindex", "forward to the second shentry");
+
+ await whenPageShown(browser, () => browser.reload());
+ checkListeners("reload", "current shentry reloaded");
+
+ await whenPageShown(browser, () => browser.gotoIndex(0));
+ checkListeners("gotoindex", "back to the first index");
+
+ // Check nsISHistory.notifyOnHistoryReload
+ info("# part 2");
+ ok(notifyReload(browser), "reloading has not been canceled");
+ checkListeners("reload", "saw the reload notification");
+
+ // Let the first listener cancel the reload action.
+ info("# part 3");
+ resetListeners();
+ setListenerRetval(0, false);
+ ok(!notifyReload(browser), "reloading has been canceled");
+ checkListeners("reload", "saw the reload notification");
+
+ // Let both listeners cancel the reload action.
+ info("# part 4");
+ resetListeners();
+ setListenerRetval(1, false);
+ ok(!notifyReload(browser), "reloading has been canceled");
+ checkListeners("reload", "saw the reload notification");
+
+ // Let the second listener cancel the reload action.
+ info("# part 5");
+ resetListeners();
+ setListenerRetval(0, true);
+ ok(!notifyReload(browser), "reloading has been canceled");
+ checkListeners("reload", "saw the reload notification");
+});
+
+class SHistoryListener {
+ constructor() {
+ this.retval = true;
+ this.last = "initial";
+ }
+
+ OnHistoryNewEntry(aNewURI) {
+ this.last = "newentry";
+ }
+
+ OnHistoryGotoIndex() {
+ this.last = "gotoindex";
+ }
+
+ OnHistoryPurge() {
+ this.last = "purge";
+ }
+
+ OnHistoryReload() {
+ this.last = "reload";
+ return this.retval;
+ }
+
+ OnHistoryReplaceEntry() {}
+}
+SHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+]);
+
+let listeners = [new SHistoryListener(), new SHistoryListener()];
+
+function checkListeners(aLast, aMessage) {
+ is(listeners[0].last, aLast, aMessage);
+ is(listeners[1].last, aLast, aMessage);
+}
+
+function resetListeners() {
+ for (let listener of listeners) {
+ listener.last = "initial";
+ }
+}
+
+function notifyReload(browser) {
+ return browser.browsingContext.sessionHistory.notifyOnHistoryReload();
+}
+
+function setListenerRetval(num, val) {
+ listeners[num].retval = val;
+}
+
+async function setup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888"
+ );
+
+ let browser = tab.linkedBrowser;
+ registerCleanupFunction(async function () {
+ for (let listener of listeners) {
+ browser.browsingContext.sessionHistory.removeSHistoryListener(listener);
+ }
+ gBrowser.removeTab(tab);
+ });
+ for (let listener of listeners) {
+ browser.browsingContext.sessionHistory.addSHistoryListener(listener);
+ }
+}
+
+function whenPageShown(aBrowser, aNavigation) {
+ let promise = new Promise(resolve => {
+ let unregister = BrowserTestUtils.addContentEventListener(
+ aBrowser,
+ "pageshow",
+ () => {
+ unregister();
+ resolve();
+ },
+ { capture: true }
+ );
+ });
+
+ aNavigation();
+ return promise;
+}
diff --git a/docshell/test/browser/browser_bug441169.js b/docshell/test/browser/browser_bug441169.js
new file mode 100644
index 0000000000..09e83b040c
--- /dev/null
+++ b/docshell/test/browser/browser_bug441169.js
@@ -0,0 +1,44 @@
+/* Make sure that netError won't allow HTML injection through badcert parameters. See bug 441169. */
+var newBrowser;
+
+function task() {
+ let resolve;
+ let promise = new Promise(r => {
+ resolve = r;
+ });
+
+ addEventListener("DOMContentLoaded", checkPage, false);
+
+ function checkPage(event) {
+ if (event.target != content.document) {
+ return;
+ }
+ removeEventListener("DOMContentLoaded", checkPage, false);
+
+ is(
+ content.document.getElementById("test_span"),
+ null,
+ "Error message should not be parsed as HTML, and hence shouldn't include the 'test_span' element."
+ );
+ resolve();
+ }
+
+ var chromeURL =
+ "about:neterror?e=nssBadCert&u=https%3A//test.kuix.de/&c=UTF-8&d=This%20sentence%20should%20not%20be%20parsed%20to%20include%20a%20%3Cspan%20id=%22test_span%22%3Enamed%3C/span%3E%20span%20tag.%0A%0AThe%20certificate%20is%20only%20valid%20for%20%3Ca%20id=%22cert_domain_link%22%20title=%22kuix.de%22%3Ekuix.de%3C/a%3E%0A%0A(Error%20code%3A%20ssl_error_bad_cert_domain)";
+ content.location = chromeURL;
+
+ return promise;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ var newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ newBrowser = gBrowser.getBrowserForTab(newTab);
+
+ ContentTask.spawn(newBrowser, null, task).then(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/docshell/test/browser/browser_bug503832.js b/docshell/test/browser/browser_bug503832.js
new file mode 100644
index 0000000000..8699eb01fc
--- /dev/null
+++ b/docshell/test/browser/browser_bug503832.js
@@ -0,0 +1,76 @@
+/* Test for Bug 503832
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=503832
+ */
+
+add_task(async function () {
+ var pagetitle = "Page Title for Bug 503832";
+ var pageurl =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug503832.html";
+ var fragmenturl =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug503832.html#firefox";
+
+ var historyService = Cc[
+ "@mozilla.org/browser/nav-history-service;1"
+ ].getService(Ci.nsINavHistoryService);
+
+ let fragmentPromise = new Promise(resolve => {
+ const listener = events => {
+ const { url, title } = events[0];
+
+ switch (url) {
+ case pageurl:
+ is(title, pagetitle, "Correct page title for " + url);
+ return;
+ case fragmenturl:
+ is(title, pagetitle, "Correct page title for " + url);
+ // If titles for fragment URLs aren't set, this code
+ // branch won't be called and the test will timeout,
+ // resulting in a failure
+ PlacesObservers.removeListener(["page-title-changed"], listener);
+ resolve();
+ }
+ };
+
+ PlacesObservers.addListener(["page-title-changed"], listener);
+ });
+
+ /* Queries nsINavHistoryService and returns a single history entry
+ * for a given URI */
+ function getNavHistoryEntry(aURI) {
+ var options = historyService.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ options.maxResults = 1;
+
+ var query = historyService.getNewQuery();
+ query.uri = aURI;
+
+ var result = historyService.executeQuery(query, options);
+ result.root.containerOpen = true;
+
+ if (!result.root.childCount) {
+ return null;
+ }
+ var node = result.root.getChild(0);
+ result.root.containerOpen = false;
+ return node;
+ }
+
+ // Make sure neither of the test pages haven't been loaded before.
+ var info = getNavHistoryEntry(makeURI(pageurl));
+ ok(!info, "The test page must not have been visited already.");
+ info = getNavHistoryEntry(makeURI(fragmenturl));
+ ok(!info, "The fragment test page must not have been visited already.");
+
+ // Now open the test page in a new tab
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, pageurl);
+
+ // Now that the page is loaded, click on fragment link
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#firefox-link",
+ {},
+ gBrowser.selectedBrowser
+ );
+ await fragmentPromise;
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/docshell/test/browser/browser_bug554155.js b/docshell/test/browser/browser_bug554155.js
new file mode 100644
index 0000000000..223f6241de
--- /dev/null
+++ b/docshell/test/browser/browser_bug554155.js
@@ -0,0 +1,33 @@
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { gBrowser, url: "http://example.com" },
+ async function (browser) {
+ let numLocationChanges = 0;
+
+ let listener = {
+ onLocationChange(browser, webProgress, request, uri, flags) {
+ info("location change: " + (uri && uri.spec));
+ numLocationChanges++;
+ },
+ };
+
+ gBrowser.addTabsProgressListener(listener);
+
+ await SpecialPowers.spawn(browser, [], function () {
+ // pushState to a new URL (http://example.com/foo"). This should trigger
+ // exactly one LocationChange event.
+ content.history.pushState(null, null, "foo");
+ });
+
+ await Promise.resolve();
+
+ gBrowser.removeTabsProgressListener(listener);
+ is(
+ numLocationChanges,
+ 1,
+ "pushState should cause exactly one LocationChange event."
+ );
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug655270.js b/docshell/test/browser/browser_bug655270.js
new file mode 100644
index 0000000000..3120abb269
--- /dev/null
+++ b/docshell/test/browser/browser_bug655270.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test for Bug 655273
+ *
+ * Call pushState and then make sure that the favicon service associates our
+ * old favicon with the new URI.
+ */
+
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+add_task(async function test() {
+ const testDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ const origURL = testDir + "file_bug655270.html";
+ const newURL = origURL + "?new_page";
+
+ const faviconURL = testDir + "favicon_bug655270.ico";
+
+ let icon1;
+ let promiseIcon1 = PlacesTestUtils.waitForNotification(
+ "favicon-changed",
+ events =>
+ events.some(e => {
+ if (e.url == origURL) {
+ icon1 = e.faviconUrl;
+ return true;
+ }
+ return false;
+ })
+ );
+ let icon2;
+ let promiseIcon2 = PlacesTestUtils.waitForNotification(
+ "favicon-changed",
+ events =>
+ events.some(e => {
+ if (e.url == newURL) {
+ icon2 = e.faviconUrl;
+ return true;
+ }
+ return false;
+ })
+ );
+
+ // The page at origURL has a <link rel='icon'>, so we should get a call into
+ // our observer below when it loads. Once we verify that we have the right
+ // favicon URI, we call pushState, which should trigger another favicon change
+ // event, this time for the URI after pushState.
+ let tab = BrowserTestUtils.addTab(gBrowser, origURL);
+ await promiseIcon1;
+ is(icon1, faviconURL, "FaviconURL for original URI");
+ // Ignore the promise returned here and wait for the next
+ // onPageChanged notification.
+ SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.history.pushState("", "", "?new_page");
+ });
+ await promiseIcon2;
+ is(icon2, faviconURL, "FaviconURL for new URI");
+ gBrowser.removeTab(tab);
+});
diff --git a/docshell/test/browser/browser_bug655273.js b/docshell/test/browser/browser_bug655273.js
new file mode 100644
index 0000000000..e564eb2f6c
--- /dev/null
+++ b/docshell/test/browser/browser_bug655273.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for Bug 655273. Make sure that after changing the URI via
+ * history.pushState, the resulting SHEntry has the same title as our old
+ * SHEntry.
+ */
+
+add_task(async function test() {
+ waitForExplicitFinish();
+
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { gBrowser, url: "http://example.com" },
+ async function (browser) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let cw = content;
+ let oldTitle = cw.document.title;
+ ok(oldTitle, "Content window should initially have a title.");
+ cw.history.pushState("", "", "new_page");
+
+ let shistory = cw.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ ).sessionHistory;
+
+ is(
+ shistory.legacySHistory.getEntryAtIndex(shistory.index).title,
+ oldTitle,
+ "SHEntry title after pushstate."
+ );
+ });
+
+ return;
+ }
+
+ let bc = browser.browsingContext;
+ let oldTitle = browser.browsingContext.currentWindowGlobal.documentTitle;
+ ok(oldTitle, "Content window should initially have a title.");
+ SpecialPowers.spawn(browser, [], async function () {
+ content.history.pushState("", "", "new_page");
+ });
+
+ let shistory = bc.sessionHistory;
+ await SHListener.waitForHistory(shistory, SHListener.NewEntry);
+
+ is(
+ shistory.getEntryAtIndex(shistory.index).title,
+ oldTitle,
+ "SHEntry title after pushstate."
+ );
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug670318.js b/docshell/test/browser/browser_bug670318.js
new file mode 100644
index 0000000000..6fad3d39e6
--- /dev/null
+++ b/docshell/test/browser/browser_bug670318.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test for Bug 670318
+ *
+ * When LoadEntry() is called on a browser that has multiple duplicate history
+ * entries, history.index can end up out of range (>= history.count).
+ */
+
+const URL =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug670318.html";
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await ContentTask.spawn(browser, URL, async function (URL) {
+ let history = docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ ).sessionHistory;
+ let count = 0;
+
+ let testDone = {};
+ testDone.promise = new Promise(resolve => {
+ testDone.resolve = resolve;
+ });
+
+ // Since listener implements nsISupportsWeakReference, we are
+ // responsible for keeping it alive so that the GC doesn't clear
+ // it before the test completes. We do this by anchoring the listener
+ // to the message manager, and clearing it just before the test
+ // completes.
+ this._testListener = {
+ owner: this,
+ OnHistoryNewEntry(aNewURI) {
+ info("OnHistoryNewEntry " + aNewURI.spec + ", " + count);
+ if (aNewURI.spec == URL && 5 == ++count) {
+ addEventListener(
+ "load",
+ function onLoad() {
+ Assert.ok(
+ history.index < history.count,
+ "history.index is valid"
+ );
+ testDone.resolve();
+ },
+ { capture: true, once: true }
+ );
+
+ history.legacySHistory.removeSHistoryListener(
+ this.owner._testListener
+ );
+ delete this.owner._testListener;
+ this.owner = null;
+ content.setTimeout(() => {
+ content.location.reload();
+ }, 0);
+ }
+ },
+
+ OnHistoryReload: () => true,
+ OnHistoryGotoIndex: () => {},
+ OnHistoryPurge: () => {},
+ OnHistoryReplaceEntry: () => {
+ // The initial load of about:blank causes a transient entry to be
+ // created, so our first navigation to a real page is a replace
+ // instead of a new entry.
+ ++count;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ history.legacySHistory.addSHistoryListener(this._testListener);
+ content.location = URL;
+
+ await testDone.promise;
+ });
+
+ return;
+ }
+
+ let history = browser.browsingContext.sessionHistory;
+ let count = 0;
+
+ let testDone = {};
+ testDone.promise = new Promise(resolve => {
+ testDone.resolve = resolve;
+ });
+
+ let listener = {
+ async OnHistoryNewEntry(aNewURI) {
+ if (aNewURI.spec == URL && 5 == ++count) {
+ history.removeSHistoryListener(listener);
+ await ContentTask.spawn(browser, null, () => {
+ return new Promise(resolve => {
+ addEventListener(
+ "load",
+ evt => {
+ let history = docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ ).sessionHistory;
+ Assert.ok(
+ history.index < history.count,
+ "history.index is valid"
+ );
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+
+ content.location.reload();
+ });
+ });
+ testDone.resolve();
+ }
+ },
+
+ OnHistoryReload: () => true,
+ OnHistoryGotoIndex: () => {},
+ OnHistoryPurge: () => {},
+ OnHistoryReplaceEntry: () => {
+ // The initial load of about:blank causes a transient entry to be
+ // created, so our first navigation to a real page is a replace
+ // instead of a new entry.
+ ++count;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ history.addSHistoryListener(listener);
+ BrowserTestUtils.startLoadingURIString(browser, URL);
+
+ await testDone.promise;
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_bug673087-1.js b/docshell/test/browser/browser_bug673087-1.js
new file mode 100644
index 0000000000..427b246d76
--- /dev/null
+++ b/docshell/test/browser/browser_bug673087-1.js
@@ -0,0 +1,46 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug673087-1.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u00A4"),
+ 151,
+ "Parent doc should be windows-1252 initially"
+ );
+
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u00A4"),
+ 95,
+ "Child doc should be windows-1252 initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\u3042"),
+ 151,
+ "Parent doc should decode as EUC-JP subsequently"
+ );
+ is(
+ content.frames[0].document.documentElement.textContent.indexOf("\u3042"),
+ 95,
+ "Child doc should decode as EUC-JP subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "EUC-JP",
+ "Parent doc should report EUC-JP subsequently"
+ );
+ is(
+ content.frames[0].document.characterSet,
+ "EUC-JP",
+ "Child doc should report EUC-JP subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug673087-2.js b/docshell/test/browser/browser_bug673087-2.js
new file mode 100644
index 0000000000..13a7a2a82c
--- /dev/null
+++ b/docshell/test/browser/browser_bug673087-2.js
@@ -0,0 +1,36 @@
+function test() {
+ var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+ runCharsetTest(
+ rootDir + "file_bug673087-2.html",
+ afterOpen,
+ afterChangeCharset
+ );
+}
+
+function afterOpen() {
+ is(
+ content.document.documentElement.textContent.indexOf("\uFFFD"),
+ 0,
+ "Doc should decode as replacement initially"
+ );
+
+ is(
+ content.document.characterSet,
+ "replacement",
+ "Doc should report replacement initially"
+ );
+}
+
+function afterChangeCharset() {
+ is(
+ content.document.documentElement.textContent.indexOf("\uFFFD"),
+ 0,
+ "Doc should decode as replacement subsequently"
+ );
+
+ is(
+ content.document.characterSet,
+ "replacement",
+ "Doc should report replacement subsequently"
+ );
+}
diff --git a/docshell/test/browser/browser_bug673467.js b/docshell/test/browser/browser_bug673467.js
new file mode 100644
index 0000000000..507410254c
--- /dev/null
+++ b/docshell/test/browser/browser_bug673467.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test for bug 673467. In a new tab, load a page which inserts a new iframe
+// before the load and then sets its location during the load. This should
+// create just one SHEntry.
+
+var doc =
+ "data:text/html,<html><body onload='load()'>" +
+ "<script>" +
+ " var iframe = document.createElement('iframe');" +
+ " iframe.id = 'iframe';" +
+ " document.documentElement.appendChild(iframe);" +
+ " function load() {" +
+ " iframe.src = 'data:text/html,Hello!';" +
+ " }" +
+ "</script>" +
+ "</body></html>";
+
+function test() {
+ waitForExplicitFinish();
+
+ let taskFinished;
+
+ let tab = BrowserTestUtils.addTab(gBrowser, doc, {}, tab => {
+ taskFinished = ContentTask.spawn(tab.linkedBrowser, null, () => {
+ return new Promise(resolve => {
+ addEventListener(
+ "load",
+ function () {
+ // The main page has loaded. Now wait for the iframe to load.
+ let iframe = content.document.getElementById("iframe");
+ iframe.addEventListener(
+ "load",
+ function listener(aEvent) {
+ // Wait for the iframe to load the new document, not about:blank.
+ if (!iframe.src) {
+ return;
+ }
+
+ iframe.removeEventListener("load", listener, true);
+ let shistory = content.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ ).sessionHistory;
+
+ Assert.equal(shistory.count, 1, "shistory count should be 1.");
+ resolve();
+ },
+ true
+ );
+ },
+ true
+ );
+ });
+ });
+ });
+
+ taskFinished.then(() => {
+ gBrowser.removeTab(tab);
+ finish();
+ });
+}
diff --git a/docshell/test/browser/browser_bug852909.js b/docshell/test/browser/browser_bug852909.js
new file mode 100644
index 0000000000..108baa0626
--- /dev/null
+++ b/docshell/test/browser/browser_bug852909.js
@@ -0,0 +1,35 @@
+var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/";
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ rootDir + "file_bug852909.png"
+ );
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(image);
+}
+
+function image(event) {
+ ok(
+ !gBrowser.selectedTab.mayEnableCharacterEncodingMenu,
+ "Docshell should say the menu should be disabled for images."
+ );
+
+ gBrowser.removeCurrentTab();
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ rootDir + "file_bug852909.pdf"
+ );
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(pdf);
+}
+
+function pdf(event) {
+ ok(
+ !gBrowser.selectedTab.mayEnableCharacterEncodingMenu,
+ "Docshell should say the menu should be disabled for PDF.js."
+ );
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/docshell/test/browser/browser_bug92473.js b/docshell/test/browser/browser_bug92473.js
new file mode 100644
index 0000000000..7e386f5ee9
--- /dev/null
+++ b/docshell/test/browser/browser_bug92473.js
@@ -0,0 +1,70 @@
+/* The test text as octets for reference
+ * %83%86%83%6a%83%52%81%5b%83%68%82%cd%81%41%82%b7%82%d7%82%c4%82%cc%95%b6%8e%9a%82%c9%8c%c5%97%4c%82%cc%94%d4%8d%86%82%f0%95%74%97%5e%82%b5%82%dc%82%b7
+ */
+
+function testContent(text) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [text], text => {
+ Assert.equal(
+ content.document.getElementById("testpar").innerHTML,
+ text,
+ "<p> contains expected text"
+ );
+ Assert.equal(
+ content.document.getElementById("testtextarea").innerHTML,
+ text,
+ "<textarea> contains expected text"
+ );
+ Assert.equal(
+ content.document.getElementById("testinput").value,
+ text,
+ "<input> contains expected text"
+ );
+ });
+}
+
+function afterOpen() {
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(
+ afterChangeCharset
+ );
+
+ /* The test text decoded incorrectly as Windows-1251. This is the "right" wrong
+ text; anything else is unexpected. */
+ const wrongText =
+ "\u0453\u2020\u0453\u006A\u0453\u0052\u0403\u005B\u0453\u0068\u201A\u041D\u0403\u0041\u201A\u00B7\u201A\u0427\u201A\u0414\u201A\u041C\u2022\u00B6\u040B\u0459\u201A\u0419\u040A\u0415\u2014\u004C\u201A\u041C\u201D\u0424\u040C\u2020\u201A\u0440\u2022\u0074\u2014\u005E\u201A\u00B5\u201A\u042C\u201A\u00B7";
+
+ /* Test that the content on load is the expected wrong decoding */
+ testContent(wrongText).then(() => {
+ BrowserForceEncodingDetection();
+ });
+}
+
+function afterChangeCharset() {
+ /* The test text decoded correctly as Shift_JIS */
+ const rightText =
+ "\u30E6\u30CB\u30B3\u30FC\u30C9\u306F\u3001\u3059\u3079\u3066\u306E\u6587\u5B57\u306B\u56FA\u6709\u306E\u756A\u53F7\u3092\u4ED8\u4E0E\u3057\u307E\u3059";
+
+ /* test that the content is decoded correctly */
+ testContent(rightText).then(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ // Get the local directory. This needs to be a file: URI because chrome: URIs
+ // are always UTF-8 (bug 617339) and we are testing decoding from other
+ // charsets.
+ var jar = getJar(getRootDirectory(gTestPath));
+ var dir = jar
+ ? extractJarToTmp(jar)
+ : getChromeDir(getResolvedURI(gTestPath));
+ var rootDir = Services.io.newFileURI(dir).spec;
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ rootDir + "test-form_sjis.html"
+ );
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(afterOpen);
+}
diff --git a/docshell/test/browser/browser_click_link_within_view_source.js b/docshell/test/browser/browser_click_link_within_view_source.js
new file mode 100644
index 0000000000..73542f977a
--- /dev/null
+++ b/docshell/test/browser/browser_click_link_within_view_source.js
@@ -0,0 +1,78 @@
+"use strict";
+
+/**
+ * Test for Bug 1359204
+ *
+ * Loading a local file, then view-source on that file. Make sure that
+ * clicking a link within that view-source page is not blocked by security checks.
+ */
+
+add_task(async function test_click_link_within_view_source() {
+ let TEST_FILE = "file_click_link_within_view_source.html";
+ let TEST_FILE_URI = getChromeDir(getResolvedURI(gTestPath));
+ TEST_FILE_URI.append(TEST_FILE);
+ TEST_FILE_URI = Services.io.newFileURI(TEST_FILE_URI).spec;
+
+ let DUMMY_FILE = "dummy_page.html";
+ let DUMMY_FILE_URI = getChromeDir(getResolvedURI(gTestPath));
+ DUMMY_FILE_URI.append(DUMMY_FILE);
+ DUMMY_FILE_URI = Services.io.newFileURI(DUMMY_FILE_URI).spec;
+
+ await BrowserTestUtils.withNewTab(TEST_FILE_URI, async function (aBrowser) {
+ let tabSpec = gBrowser.selectedBrowser.currentURI.spec;
+ info("loading: " + tabSpec);
+ ok(
+ tabSpec.startsWith("file://") && tabSpec.endsWith(TEST_FILE),
+ "sanity check to make sure html loaded"
+ );
+
+ info("click view-source of html");
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ document.getElementById("View:PageSource").doCommand();
+
+ let tab = await tabPromise;
+ tabSpec = gBrowser.selectedBrowser.currentURI.spec;
+ info("loading: " + tabSpec);
+ ok(
+ tabSpec.startsWith("view-source:file://") && tabSpec.endsWith(TEST_FILE),
+ "loading view-source of html succeeded"
+ );
+
+ info("click testlink within view-source page");
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ url => url.endsWith("dummy_page.html")
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ if (content.document.readyState != "complete") {
+ await ContentTaskUtils.waitForEvent(
+ content.document,
+ "readystatechange",
+ false,
+ () => content.document.readyState == "complete"
+ );
+ }
+ // document.getElementById() does not work on a view-source page, hence we use document.links
+ let linksOnPage = content.document.links;
+ is(
+ linksOnPage.length,
+ 1,
+ "sanity check: make sure only one link is present on page"
+ );
+ let myLink = content.document.links[0];
+ myLink.click();
+ });
+
+ await loadPromise;
+
+ tabSpec = gBrowser.selectedBrowser.currentURI.spec;
+ info("loading: " + tabSpec);
+ ok(
+ tabSpec.startsWith("view-source:file://") && tabSpec.endsWith(DUMMY_FILE),
+ "loading view-source of html succeeded"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/docshell/test/browser/browser_cross_process_csp_inheritance.js b/docshell/test/browser/browser_cross_process_csp_inheritance.js
new file mode 100644
index 0000000000..76be61d0f9
--- /dev/null
+++ b/docshell/test/browser/browser_cross_process_csp_inheritance.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_URI = TEST_PATH + "file_cross_process_csp_inheritance.html";
+const DATA_URI =
+ "data:text/html,<html>test-same-diff-process-csp-inhertiance</html>";
+
+const FISSION_ENABLED = SpecialPowers.useRemoteSubframes;
+
+function getCurrentPID(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+}
+
+function getCurrentURI(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], () => {
+ let channel = content.docShell.currentDocumentChannel;
+ return channel.URI.asciiSpec;
+ });
+}
+
+function verifyResult(
+ aTestName,
+ aBrowser,
+ aDataURI,
+ aPID,
+ aSamePID,
+ aFissionEnabled
+) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [{ aTestName, aDataURI, aPID, aSamePID, aFissionEnabled }],
+ async function ({ aTestName, aDataURI, aPID, aSamePID, aFissionEnabled }) {
+ // sanity, to make sure the correct URI was loaded
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.URI.asciiSpec,
+ aDataURI,
+ aTestName + ": correct data uri loaded"
+ );
+
+ // check that the process ID is the same/different when opening the new tab
+ let pid = Services.appinfo.processID;
+ if (aSamePID) {
+ is(pid, aPID, aTestName + ": process ID needs to be identical");
+ } else if (aFissionEnabled) {
+ // TODO: Fission discards dom.noopener.newprocess.enabled and puts
+ // data: URIs in the same process. Unfortunately todo_isnot is not
+ // defined in that scope, hence we have to use a workaround.
+ todo(
+ false,
+ pid == aPID,
+ ": process ID needs to be different in fission"
+ );
+ } else {
+ isnot(pid, aPID, aTestName + ": process ID needs to be different");
+ }
+
+ // finally, evaluate that the CSP was set.
+ let cspOBJ = JSON.parse(content.document.cspJSON);
+ let policies = cspOBJ["csp-policies"];
+ is(policies.length, 1, "should be one policy");
+ let policy = policies[0];
+ is(
+ policy["script-src"],
+ "'none'",
+ aTestName + ": script-src directive matches"
+ );
+ }
+ );
+}
+
+async function simulateCspInheritanceForNewTab(aTestName, aSamePID) {
+ await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) {
+ // do some sanity checks
+ let currentURI = await getCurrentURI(gBrowser.selectedBrowser);
+ is(currentURI, TEST_URI, aTestName + ": correct test uri loaded");
+
+ let pid = await getCurrentPID(gBrowser.selectedBrowser);
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI, true);
+ // simulate click
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testLink",
+ {},
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+ await verifyResult(
+ aTestName,
+ gBrowser.selectedBrowser,
+ DATA_URI,
+ pid,
+ aSamePID,
+ FISSION_ENABLED
+ );
+ await BrowserTestUtils.removeTab(tab);
+ });
+}
+
+add_task(async function test_csp_inheritance_diff_process() {
+ // forcing the new data: URI load to happen in a *new* process by flipping the pref
+ // to force <a rel="noopener" ...> to be loaded in a new process.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.noopener.newprocess.enabled", true]],
+ });
+ await simulateCspInheritanceForNewTab("diff-process-inheritance", false);
+});
+
+add_task(async function test_csp_inheritance_same_process() {
+ // forcing the new data: URI load to happen in a *same* process by resetting the pref
+ // and loaded <a rel="noopener" ...> in the *same* process.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.noopener.newprocess.enabled", false]],
+ });
+ await simulateCspInheritanceForNewTab("same-process-inheritance", true);
+});
diff --git a/docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js b/docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js
new file mode 100644
index 0000000000..d0b92084ec
--- /dev/null
+++ b/docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+/**
+ * Test that javascript URIs in CSP-sandboxed contexts can't be used to bypass
+ * script restrictions.
+ */
+add_task(async function test_csp_sandbox_no_script_js_uri() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "dummy_page.html",
+ async browser => {
+ info("Register observer and wait for javascript-uri-blocked message.");
+ let observerPromise = SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ SpecialPowers.addObserver(function obs(subject) {
+ ok(
+ subject == content,
+ "Should block script spawned via javascript uri"
+ );
+ SpecialPowers.removeObserver(
+ obs,
+ "javascript-uri-blocked-by-sandbox"
+ );
+ resolve();
+ }, "javascript-uri-blocked-by-sandbox");
+ });
+ });
+
+ info("Spawn csp-sandboxed iframe with javascript URI");
+ let frameBC = await SpecialPowers.spawn(
+ browser,
+ [TEST_PATH + "file_csp_sandbox_no_script_js_uri.html"],
+ async url => {
+ let frame = content.document.createElement("iframe");
+ let loadPromise = ContentTaskUtils.waitForEvent(frame, "load", true);
+ frame.src = url;
+ content.document.body.appendChild(frame);
+ await loadPromise;
+ return frame.browsingContext;
+ }
+ );
+
+ info("Click javascript URI link in iframe");
+ BrowserTestUtils.synthesizeMouseAtCenter("a", {}, frameBC);
+ await observerPromise;
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_csp_uir.js b/docshell/test/browser/browser_csp_uir.js
new file mode 100644
index 0000000000..2eb36d3d4d
--- /dev/null
+++ b/docshell/test/browser/browser_csp_uir.js
@@ -0,0 +1,89 @@
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_URI = TEST_PATH + "file_csp_uir.html"; // important to be http: to test upgrade-insecure-requests
+const RESULT_URI =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ TEST_PATH.replace("http://", "https://") + "file_csp_uir_dummy.html";
+
+function verifyCSP(aTestName, aBrowser, aResultURI) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [{ aTestName, aResultURI }],
+ async function ({ aTestName, aResultURI }) {
+ let channel = content.docShell.currentDocumentChannel;
+ is(channel.URI.asciiSpec, aResultURI, "testing CSP for " + aTestName);
+ }
+ );
+}
+
+add_task(async function test_csp_inheritance_regular_click() {
+ await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) {
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ RESULT_URI
+ );
+ // set the data href + simulate click
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ {},
+ gBrowser.selectedBrowser
+ );
+ await loadPromise;
+ await verifyCSP("click()", gBrowser.selectedBrowser, RESULT_URI);
+ });
+});
+
+add_task(async function test_csp_inheritance_ctrl_click() {
+ await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ RESULT_URI,
+ true
+ );
+ // set the data href + simulate ctrl+click
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ { ctrlKey: true, metaKey: true },
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+ await verifyCSP("ctrl-click()", gBrowser.selectedBrowser, RESULT_URI);
+ await BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(
+ async function test_csp_inheritance_right_click_open_link_in_new_tab() {
+ await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, RESULT_URI);
+ // set the data href + simulate right-click open link in tab
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-openlinkintab").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+ await verifyCSP(
+ "right-click-open-in-new-tab()",
+ gBrowser.selectedBrowser,
+ RESULT_URI
+ );
+ await BrowserTestUtils.removeTab(tab);
+ });
+ }
+);
diff --git a/docshell/test/browser/browser_dataURI_unique_opaque_origin.js b/docshell/test/browser/browser_dataURI_unique_opaque_origin.js
new file mode 100644
index 0000000000..12cc3e06fb
--- /dev/null
+++ b/docshell/test/browser/browser_dataURI_unique_opaque_origin.js
@@ -0,0 +1,30 @@
+add_task(async function test_dataURI_unique_opaque_origin() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com");
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let pagePrincipal = browser.contentPrincipal;
+ info("pagePrincial " + pagePrincipal.origin);
+
+ BrowserTestUtils.startLoadingURIString(browser, "data:text/html,hi");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ principal: pagePrincipal }],
+ async function (args) {
+ info("data URI principal: " + content.document.nodePrincipal.origin);
+ Assert.ok(
+ content.document.nodePrincipal.isNullPrincipal,
+ "data: URI should have NullPrincipal."
+ );
+ Assert.ok(
+ !content.document.nodePrincipal.equals(args.principal),
+ "data: URI should have unique opaque origin."
+ );
+ }
+ );
+
+ gBrowser.removeTab(tab);
+});
diff --git a/docshell/test/browser/browser_data_load_inherit_csp.js b/docshell/test/browser/browser_data_load_inherit_csp.js
new file mode 100644
index 0000000000..b2bc86e0ea
--- /dev/null
+++ b/docshell/test/browser/browser_data_load_inherit_csp.js
@@ -0,0 +1,110 @@
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const HTML_URI = TEST_PATH + "file_data_load_inherit_csp.html";
+const DATA_URI = "data:text/html;html,<html><body>foo</body></html>";
+
+function setDataHrefOnLink(aBrowser, aDataURI) {
+ return SpecialPowers.spawn(aBrowser, [aDataURI], function (uri) {
+ let link = content.document.getElementById("testlink");
+ link.href = uri;
+ });
+}
+
+function verifyCSP(aTestName, aBrowser, aDataURI) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [{ aTestName, aDataURI }],
+ async function ({ aTestName, aDataURI }) {
+ let channel = content.docShell.currentDocumentChannel;
+ is(channel.URI.spec, aDataURI, "testing CSP for " + aTestName);
+ let cspJSON = content.document.cspJSON;
+ let cspOBJ = JSON.parse(cspJSON);
+ let policies = cspOBJ["csp-policies"];
+ is(policies.length, 1, "should be one policy");
+ let policy = policies[0];
+ is(
+ policy["script-src"],
+ "'unsafe-inline'",
+ "script-src directive matches"
+ );
+ }
+ );
+}
+
+add_setup(async function () {
+ // allow top level data: URI navigations, otherwise clicking data: link fails
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+});
+
+add_task(async function test_data_csp_inheritance_regular_click() {
+ await BrowserTestUtils.withNewTab(HTML_URI, async function (browser) {
+ let loadPromise = BrowserTestUtils.browserLoaded(browser, false, DATA_URI);
+ // set the data href + simulate click
+ await setDataHrefOnLink(gBrowser.selectedBrowser, DATA_URI);
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ {},
+ gBrowser.selectedBrowser
+ );
+ await loadPromise;
+ await verifyCSP("click()", gBrowser.selectedBrowser, DATA_URI);
+ });
+});
+
+add_task(async function test_data_csp_inheritance_ctrl_click() {
+ await BrowserTestUtils.withNewTab(HTML_URI, async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI, true);
+ // set the data href + simulate ctrl+click
+ await setDataHrefOnLink(gBrowser.selectedBrowser, DATA_URI);
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ { ctrlKey: true, metaKey: true },
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+ await verifyCSP("ctrl-click()", gBrowser.selectedBrowser, DATA_URI);
+ await BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(
+ async function test_data_csp_inheritance_right_click_open_link_in_new_tab() {
+ await BrowserTestUtils.withNewTab(HTML_URI, async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ DATA_URI,
+ true
+ );
+ // set the data href + simulate right-click open link in tab
+ await setDataHrefOnLink(gBrowser.selectedBrowser, DATA_URI);
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-openlinkintab").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+ await verifyCSP(
+ "right-click-open-in-new-tab()",
+ gBrowser.selectedBrowser,
+ DATA_URI
+ );
+ await BrowserTestUtils.removeTab(tab);
+ });
+ }
+);
diff --git a/docshell/test/browser/browser_fall_back_to_https.js b/docshell/test/browser/browser_fall_back_to_https.js
new file mode 100644
index 0000000000..7d64a781d1
--- /dev/null
+++ b/docshell/test/browser/browser_fall_back_to_https.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * This test is for bug 1002724.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1002724
+ *
+ * When a user enters a host name or IP address in the URL bar, "http" is
+ * assumed. If the host rejects connections on port 80, we try HTTPS as a
+ * fall-back and only fail if HTTPS connection fails.
+ *
+ * This tests that when a user enters "example.com", it attempts to load
+ * http://example.com:80 (not rejected), and when trying secureonly.example.com
+ * (which rejects connections on port 80), it fails then loads
+ * https://secureonly.example.com:443 instead.
+ */
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+const bug1002724_tests = [
+ {
+ original: "example.com",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ expected: "http://example.com",
+ explanation: "Should load HTTP version of example.com",
+ },
+ {
+ original: "secureonly.example.com",
+ expected: "https://secureonly.example.com",
+ explanation:
+ "Should reject secureonly.example.com on HTTP but load the HTTPS version",
+ },
+];
+
+async function test_one(test_obj) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ gURLBar.focus();
+ gURLBar.value = test_obj.original;
+
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ ok(
+ tab.linkedBrowser.currentURI.spec.startsWith(test_obj.expected),
+ test_obj.explanation
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_bug1002724() {
+ await SpecialPowers.pushPrefEnv(
+ // Disable HSTS preload just in case.
+ {
+ set: [
+ ["network.stricttransportsecurity.preloadlist", false],
+ ["network.dns.native-is-localhost", true],
+ ["dom.security.https_first_schemeless", false],
+ ],
+ }
+ );
+
+ for (let test of bug1002724_tests) {
+ await test_one(test);
+ }
+
+ // Test with HTTPS-First upgrading of schemeless enabled
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first_schemeless", true]],
+ });
+
+ bug1002724_tests[0].expected = "https://example.com";
+ await test_one(bug1002724_tests[0]);
+});
diff --git a/docshell/test/browser/browser_frameloader_swap_with_bfcache.js b/docshell/test/browser/browser_frameloader_swap_with_bfcache.js
new file mode 100644
index 0000000000..d9f5a41573
--- /dev/null
+++ b/docshell/test/browser/browser_frameloader_swap_with_bfcache.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+add_task(async function test() {
+ if (
+ !SpecialPowers.Services.appinfo.sessionHistoryInParent ||
+ !SpecialPowers.Services.prefs.getBoolPref("fission.bfcacheInParent")
+ ) {
+ ok(
+ true,
+ "This test is currently only for the bfcache in the parent process."
+ );
+ return;
+ }
+ const uri =
+ "data:text/html,<script>onpageshow = function(e) { document.documentElement.setAttribute('persisted', e.persisted); }; </script>";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri, true);
+ let browser1 = tab1.linkedBrowser;
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri, true);
+ let browser2 = tab2.linkedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser2, uri + "nextpage");
+ await BrowserTestUtils.browserLoaded(browser2, false);
+
+ gBrowser.swapBrowsersAndCloseOther(tab1, tab2);
+ is(tab1.linkedBrowser, browser1, "Tab's browser should stay the same.");
+ browser1.goBack(false);
+ await BrowserTestUtils.browserLoaded(browser1, false);
+ let persisted = await SpecialPowers.spawn(browser1, [], async function () {
+ return content.document.documentElement.getAttribute("persisted");
+ });
+
+ is(persisted, "false", "BFCache should be evicted when swapping browsers.");
+
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/docshell/test/browser/browser_history_triggeringprincipal_viewsource.js b/docshell/test/browser/browser_history_triggeringprincipal_viewsource.js
new file mode 100644
index 0000000000..d542ef892c
--- /dev/null
+++ b/docshell/test/browser/browser_history_triggeringprincipal_viewsource.js
@@ -0,0 +1,88 @@
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const HTML_URI = TEST_PATH + "dummy_page.html";
+const VIEW_SRC_URI = "view-source:" + HTML_URI;
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ info("load baseline html in new tab");
+ await BrowserTestUtils.withNewTab(HTML_URI, async function (aBrowser) {
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ HTML_URI,
+ "sanity check to make sure html loaded"
+ );
+
+ info("right-click -> view-source of html");
+ let vSrcCtxtMenu = document.getElementById("contentAreaContextMenu");
+ let popupPromise = BrowserTestUtils.waitForEvent(
+ vSrcCtxtMenu,
+ "popupshown"
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "contextmenu", button: 2 },
+ aBrowser
+ );
+ await popupPromise;
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, VIEW_SRC_URI);
+ let vSrcItem = vSrcCtxtMenu.querySelector("#context-viewsource");
+ vSrcCtxtMenu.activateItem(vSrcItem);
+ let tab = await tabPromise;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ VIEW_SRC_URI,
+ "loading view-source of html succeeded"
+ );
+
+ info("load html file again before going .back()");
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ HTML_URI
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [HTML_URI], HTML_URI => {
+ content.document.location = HTML_URI;
+ });
+ await loadPromise;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ HTML_URI,
+ "loading html another time succeeded"
+ );
+
+ info(
+ "click .back() to view-source of html again and make sure the history entry has a triggeringPrincipal"
+ );
+ let backCtxtMenu = document.getElementById("contentAreaContextMenu");
+ popupPromise = BrowserTestUtils.waitForEvent(backCtxtMenu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "contextmenu", button: 2 },
+ aBrowser
+ );
+ await popupPromise;
+ loadPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ let backItem = backCtxtMenu.querySelector("#context-back");
+ backCtxtMenu.activateItem(backItem);
+ await loadPromise;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ VIEW_SRC_URI,
+ "clicking .back() to view-source of html succeeded"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/docshell/test/browser/browser_isInitialDocument.js b/docshell/test/browser/browser_isInitialDocument.js
new file mode 100644
index 0000000000..03265c8e3e
--- /dev/null
+++ b/docshell/test/browser/browser_isInitialDocument.js
@@ -0,0 +1,319 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tag every new WindowGlobalParent with an expando indicating whether or not
+// they were an initial document when they were created for the duration of this
+// test.
+function wasInitialDocumentObserver(subject) {
+ subject._test_wasInitialDocument = subject.isInitialDocument;
+}
+Services.obs.addObserver(wasInitialDocumentObserver, "window-global-created");
+SimpleTest.registerCleanupFunction(function () {
+ Services.obs.removeObserver(
+ wasInitialDocumentObserver,
+ "window-global-created"
+ );
+});
+
+add_task(async function new_about_blank_tab() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ is(
+ browser.browsingContext.currentWindowGlobal.isInitialDocument,
+ false,
+ "After loading an actual, final about:blank in the tab, the field is false"
+ );
+ });
+});
+
+add_task(async function iframe_initial_about_blank() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/document-builder.sjs?html=com",
+ async browser => {
+ info("Create an iframe without any explicit location");
+ await SpecialPowers.spawn(browser, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ // Add the iframe to the DOM tree in order to be able to have its browsingContext
+ content.document.body.appendChild(iframe);
+ const { browsingContext } = iframe;
+
+ is(
+ iframe.contentDocument.isInitialDocument,
+ true,
+ "The field is true on just-created iframes"
+ );
+ let beforeLoadPromise = SpecialPowers.spawnChrome(
+ [browsingContext],
+ bc => [
+ bc.currentWindowGlobal.isInitialDocument,
+ bc.currentWindowGlobal._test_wasInitialDocument,
+ ]
+ );
+
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+ is(
+ iframe.contentDocument.isInitialDocument,
+ false,
+ "The field is false after having loaded the final about:blank document"
+ );
+ let afterLoadPromise = SpecialPowers.spawnChrome(
+ [browsingContext],
+ bc => [
+ bc.currentWindowGlobal.isInitialDocument,
+ bc.currentWindowGlobal._test_wasInitialDocument,
+ ]
+ );
+
+ // Wait to await the parent process promises, so we can't miss the "load" event.
+ let [beforeIsInitial, beforeWasInitial] = await beforeLoadPromise;
+ is(beforeIsInitial, true, "before load is initial in parent");
+ is(beforeWasInitial, true, "before load was initial in parent");
+ let [afterIsInitial, afterWasInitial] = await afterLoadPromise;
+ is(afterIsInitial, false, "after load is not initial in parent");
+ is(afterWasInitial, true, "after load was initial in parent");
+ iframe.remove();
+ });
+
+ info("Create an iframe with a cross origin location");
+ const iframeBC = await SpecialPowers.spawn(browser, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ iframe.src =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/document-builder.sjs?html=org-iframe";
+ content.document.body.appendChild(iframe);
+ });
+
+ return iframe.browsingContext;
+ });
+
+ is(
+ iframeBC.currentWindowGlobal.isInitialDocument,
+ false,
+ "The field is true after having loaded the final document"
+ );
+ }
+ );
+});
+
+add_task(async function window_open() {
+ async function testWindowOpen({ browser, args, isCrossOrigin, willLoad }) {
+ info(`Open popup with ${JSON.stringify(args)}`);
+ const onNewTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ args[0] || "about:blank"
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [args, isCrossOrigin, willLoad],
+ async (args, crossOrigin, willLoad) => {
+ const win = content.window.open(...args);
+ is(
+ win.document.isInitialDocument,
+ true,
+ "The field is true right after calling window.open()"
+ );
+ let beforeLoadPromise = SpecialPowers.spawnChrome(
+ [win.browsingContext],
+ bc => [
+ bc.currentWindowGlobal.isInitialDocument,
+ bc.currentWindowGlobal._test_wasInitialDocument,
+ ]
+ );
+
+ // In cross origin, it is harder to watch for new document load, and if
+ // no argument is passed no load will happen.
+ if (!crossOrigin && willLoad) {
+ await new Promise(r =>
+ win.addEventListener("load", r, { once: true })
+ );
+ is(
+ win.document.isInitialDocument,
+ false,
+ "The field becomes false right after the popup document is loaded"
+ );
+ }
+
+ // Perform the await after the load to avoid missing it.
+ let [beforeIsInitial, beforeWasInitial] = await beforeLoadPromise;
+ is(beforeIsInitial, true, "before load is initial in parent");
+ is(beforeWasInitial, true, "before load was initial in parent");
+ }
+ );
+ const newTab = await onNewTab;
+ const windowGlobal =
+ newTab.linkedBrowser.browsingContext.currentWindowGlobal;
+ if (willLoad) {
+ is(
+ windowGlobal.isInitialDocument,
+ false,
+ "The field is false in the parent process after having loaded the final document"
+ );
+ } else {
+ is(
+ windowGlobal.isInitialDocument,
+ true,
+ "The field remains true in the parent process as nothing will be loaded"
+ );
+ }
+ BrowserTestUtils.removeTab(newTab);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/document-builder.sjs?html=com",
+ async browser => {
+ info("Use window.open() with cross-origin document");
+ await testWindowOpen({
+ browser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ args: ["http://example.org/document-builder.sjs?html=org-popup"],
+ isCrossOrigin: true,
+ willLoad: true,
+ });
+
+ info("Use window.open() with same-origin document");
+ await testWindowOpen({
+ browser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ args: ["http://example.com/document-builder.sjs?html=com-popup"],
+ isCrossOrigin: false,
+ willLoad: true,
+ });
+
+ info("Use window.open() with final about:blank document");
+ await testWindowOpen({
+ browser,
+ args: ["about:blank"],
+ isCrossOrigin: false,
+ willLoad: true,
+ });
+
+ info("Use window.open() with no argument");
+ await testWindowOpen({
+ browser,
+ args: [],
+ isCrossOrigin: false,
+ willLoad: false,
+ });
+ }
+ );
+});
+
+add_task(async function document_open() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/document-builder.sjs?html=com",
+ async browser => {
+ is(browser.browsingContext.currentWindowGlobal.isInitialDocument, false);
+ await SpecialPowers.spawn(browser, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ // Add the iframe to the DOM tree in order to be able to have its browsingContext
+ content.document.body.appendChild(iframe);
+ const { browsingContext } = iframe;
+
+ // Check the state before the call in both parent and content.
+ is(
+ iframe.contentDocument.isInitialDocument,
+ true,
+ "Is an initial document before calling document.open"
+ );
+ let beforeOpenParentPromise = SpecialPowers.spawnChrome(
+ [browsingContext],
+ bc => [
+ bc.currentWindowGlobal.isInitialDocument,
+ bc.currentWindowGlobal._test_wasInitialDocument,
+ bc.currentWindowGlobal.innerWindowId,
+ ]
+ );
+
+ // Run the `document.open` call with reduced permissions.
+ iframe.contentWindow.eval(`
+ document.open();
+ document.write("new document");
+ document.close();
+ `);
+
+ is(
+ iframe.contentDocument.isInitialDocument,
+ false,
+ "Is no longer an initial document after calling document.open"
+ );
+ let [afterIsInitial, afterWasInitial, afterID] =
+ await SpecialPowers.spawnChrome([browsingContext], bc => [
+ bc.currentWindowGlobal.isInitialDocument,
+ bc.currentWindowGlobal._test_wasInitialDocument,
+ bc.currentWindowGlobal.innerWindowId,
+ ]);
+ let [beforeIsInitial, beforeWasInitial, beforeID] =
+ await beforeOpenParentPromise;
+ is(beforeIsInitial, true, "Should be initial before in the parent");
+ is(beforeWasInitial, true, "Was initial before in the parent");
+ is(afterIsInitial, false, "Should not be initial after in the parent");
+ is(afterWasInitial, true, "Was initial after in the parent");
+ is(beforeID, afterID, "Should be the same WindowGlobalParent");
+ });
+ }
+ );
+});
+
+add_task(async function windowless_browser() {
+ info("Create a Windowless browser");
+ const browser = Services.appShell.createWindowlessBrowser(false);
+ const { browsingContext } = browser;
+ is(
+ browsingContext.currentWindowGlobal.isInitialDocument,
+ true,
+ "The field is true for a freshly created WindowlessBrowser"
+ );
+ is(
+ browser.currentURI.spec,
+ "about:blank",
+ "The location is immediately set to about:blank"
+ );
+
+ const principal = Services.scriptSecurityManager.getSystemPrincipal();
+ browser.docShell.createAboutBlankDocumentViewer(principal, principal);
+ is(
+ browsingContext.currentWindowGlobal.isInitialDocument,
+ false,
+ "The field becomes false when creating an artificial blank document"
+ );
+
+ info("Load a final about:blank document in it");
+ const onLocationChange = new Promise(resolve => {
+ let wpl = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ onLocationChange() {
+ browsingContext.webProgress.removeProgressListener(
+ wpl,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+ resolve();
+ },
+ };
+ browsingContext.webProgress.addProgressListener(
+ wpl,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+ });
+ browser.loadURI(Services.io.newURI("about:blank"), {
+ triggeringPrincipal: principal,
+ });
+ info("Wait for the location change");
+ await onLocationChange;
+ is(
+ browsingContext.currentWindowGlobal.isInitialDocument,
+ false,
+ "The field is false after the location change event"
+ );
+ browser.close();
+});
diff --git a/docshell/test/browser/browser_loadURI_postdata.js b/docshell/test/browser/browser_loadURI_postdata.js
new file mode 100644
index 0000000000..b2dbb7a641
--- /dev/null
+++ b/docshell/test/browser/browser_loadURI_postdata.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const gPostData = "postdata=true";
+const gUrl =
+ "http://mochi.test:8888/browser/docshell/test/browser/print_postdata.sjs";
+
+add_task(async function test_loadURI_persists_postData() {
+ waitForExplicitFinish();
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ });
+
+ var dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ dataStream.data = gPostData;
+
+ var postStream = Cc[
+ "@mozilla.org/network/mime-input-stream;1"
+ ].createInstance(Ci.nsIMIMEInputStream);
+ postStream.addHeader("Content-Type", "application/x-www-form-urlencoded");
+ postStream.setData(dataStream);
+ var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].getService(
+ Ci.nsIPrincipal
+ );
+
+ tab.linkedBrowser.loadURI(Services.io.newURI(gUrl), {
+ triggeringPrincipal: systemPrincipal,
+ postData: postStream,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, gUrl);
+ let body = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.body.textContent
+ );
+ is(body, gPostData, "post data was submitted correctly");
+ finish();
+});
diff --git a/docshell/test/browser/browser_multiple_pushState.js b/docshell/test/browser/browser_multiple_pushState.js
new file mode 100644
index 0000000000..5d07747543
--- /dev/null
+++ b/docshell/test/browser/browser_multiple_pushState.js
@@ -0,0 +1,25 @@
+add_task(async function test_multiple_pushState() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/docshell/test/browser/file_multiple_pushState.html",
+ },
+ async function (browser) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const kExpected = "http://example.org/bar/ABC/DEF?key=baz";
+
+ let contentLocation = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ return content.document.location.href;
+ }
+ );
+
+ is(contentLocation, kExpected);
+ is(browser.documentURI.spec, kExpected);
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_onbeforeunload_frame.js b/docshell/test/browser/browser_onbeforeunload_frame.js
new file mode 100644
index 0000000000..d697af718d
--- /dev/null
+++ b/docshell/test/browser/browser_onbeforeunload_frame.js
@@ -0,0 +1,45 @@
+"use strict";
+
+// We need to test a lot of permutations here, and there isn't any sensible way
+// to split them up or run them faster.
+requestLongerTimeout(12);
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "head_browser_onbeforeunload.js",
+ this
+);
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ for (let actions of PERMUTATIONS) {
+ info(
+ `Testing frame actions: [${actions.map(action =>
+ ACTION_NAMES.get(action)
+ )}]`
+ );
+
+ for (let startIdx = 0; startIdx < FRAMES.length; startIdx++) {
+ info(`Testing content reload from frame ${startIdx}`);
+
+ await doTest(actions, startIdx, (tab, frames) => {
+ return SpecialPowers.spawn(frames[startIdx], [], () => {
+ let eventLoopSpun = false;
+ SpecialPowers.Services.tm.dispatchToMainThread(() => {
+ eventLoopSpun = true;
+ });
+
+ content.location.reload();
+
+ return { eventLoopSpun };
+ });
+ });
+ }
+ }
+});
+
+add_task(async function cleanup() {
+ await TabPool.cleanup();
+});
diff --git a/docshell/test/browser/browser_onbeforeunload_navigation.js b/docshell/test/browser/browser_onbeforeunload_navigation.js
new file mode 100644
index 0000000000..23dc3b528e
--- /dev/null
+++ b/docshell/test/browser/browser_onbeforeunload_navigation.js
@@ -0,0 +1,165 @@
+"use strict";
+
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_bug1046022.html";
+const TARGETED_PAGE =
+ "data:text/html," +
+ encodeURIComponent("<body>Shouldn't be seeing this</body>");
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+var loadStarted = false;
+var tabStateListener = {
+ resolveLoad: null,
+ expectLoad: null,
+
+ onStateChange(webprogress, request, flags, status) {
+ const WPL = Ci.nsIWebProgressListener;
+ if (flags & WPL.STATE_IS_WINDOW) {
+ if (flags & WPL.STATE_START) {
+ loadStarted = true;
+ } else if (flags & WPL.STATE_STOP) {
+ let url = request.QueryInterface(Ci.nsIChannel).URI.spec;
+ is(url, this.expectLoad, "Should only see expected document loads");
+ if (url == this.expectLoad) {
+ this.resolveLoad();
+ }
+ }
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function promiseLoaded(url, callback) {
+ if (tabStateListener.expectLoad) {
+ throw new Error("Can't wait for multiple loads at once");
+ }
+ tabStateListener.expectLoad = url;
+ return new Promise(resolve => {
+ tabStateListener.resolveLoad = resolve;
+ if (callback) {
+ callback();
+ }
+ }).then(() => {
+ tabStateListener.expectLoad = null;
+ tabStateListener.resolveLoad = null;
+ });
+}
+
+function promiseStayOnPagePrompt(browser, acceptNavigation) {
+ return PromptTestUtils.handleNextPrompt(
+ browser,
+ { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
+ { buttonNumClick: acceptNavigation ? 0 : 1 }
+ );
+}
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE,
+ false,
+ true
+ );
+ let browser = testTab.linkedBrowser;
+ browser.addProgressListener(
+ tabStateListener,
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
+ );
+
+ const NUM_TESTS = 7;
+ await SpecialPowers.spawn(browser, [NUM_TESTS], testCount => {
+ let { testFns } = this.content.wrappedJSObject;
+ Assert.equal(
+ testFns.length,
+ testCount,
+ "Should have the correct number of test functions"
+ );
+ });
+
+ for (let allowNavigation of [false, true]) {
+ for (let i = 0; i < NUM_TESTS; i++) {
+ info(
+ `Running test ${i} with navigation ${
+ allowNavigation ? "allowed" : "forbidden"
+ }`
+ );
+
+ if (allowNavigation) {
+ // If we're allowing navigations, we need to re-load the test
+ // page after each test, since the tests will each navigate away
+ // from it.
+ await promiseLoaded(TEST_PAGE, () => {
+ browser.loadURI(Services.io.newURI(TEST_PAGE), {
+ triggeringPrincipal: document.nodePrincipal,
+ });
+ });
+ }
+
+ let promptPromise = promiseStayOnPagePrompt(browser, allowNavigation);
+ let loadPromise;
+ if (allowNavigation) {
+ loadPromise = promiseLoaded(TARGETED_PAGE);
+ }
+
+ let winID = await SpecialPowers.spawn(
+ browser,
+ [i, TARGETED_PAGE],
+ (testIdx, url) => {
+ let { testFns } = this.content.wrappedJSObject;
+ this.content.onbeforeunload = testFns[testIdx];
+ this.content.location = url;
+ return this.content.windowGlobalChild.innerWindowId;
+ }
+ );
+
+ await promptPromise;
+ await loadPromise;
+
+ if (allowNavigation) {
+ await SpecialPowers.spawn(
+ browser,
+ [TARGETED_PAGE, winID],
+ (url, winID) => {
+ this.content.onbeforeunload = null;
+ Assert.equal(
+ this.content.location.href,
+ url,
+ "Page should have navigated to the correct URL"
+ );
+ Assert.notEqual(
+ this.content.windowGlobalChild.innerWindowId,
+ winID,
+ "Page should have a new inner window"
+ );
+ }
+ );
+ } else {
+ await SpecialPowers.spawn(browser, [TEST_PAGE, winID], (url, winID) => {
+ this.content.onbeforeunload = null;
+ Assert.equal(
+ this.content.location.href,
+ url,
+ "Page should have the same URL"
+ );
+ Assert.equal(
+ this.content.windowGlobalChild.innerWindowId,
+ winID,
+ "Page should have the same inner window"
+ );
+ });
+ }
+ }
+ }
+
+ gBrowser.removeTab(testTab);
+});
diff --git a/docshell/test/browser/browser_onbeforeunload_parent.js b/docshell/test/browser/browser_onbeforeunload_parent.js
new file mode 100644
index 0000000000..72c5004334
--- /dev/null
+++ b/docshell/test/browser/browser_onbeforeunload_parent.js
@@ -0,0 +1,48 @@
+"use strict";
+
+// We need to test a lot of permutations here, and there isn't any sensible way
+// to split them up or run them faster.
+requestLongerTimeout(6);
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "head_browser_onbeforeunload.js",
+ this
+);
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ for (let actions of PERMUTATIONS) {
+ info(
+ `Testing frame actions: [${actions.map(action =>
+ ACTION_NAMES.get(action)
+ )}]`
+ );
+
+ info(`Testing tab close from parent process`);
+ await doTest(actions, -1, (tab, frames) => {
+ let eventLoopSpun = false;
+ Services.tm.dispatchToMainThread(() => {
+ eventLoopSpun = true;
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ let result = { eventLoopSpun };
+
+ // Make an extra couple of trips through the event loop to give us time
+ // to process SpecialPowers.spawn responses before resolving.
+ return new Promise(resolve => {
+ executeSoon(() => {
+ executeSoon(() => resolve(result));
+ });
+ });
+ });
+ }
+});
+
+add_task(async function cleanup() {
+ await TabPool.cleanup();
+});
diff --git a/docshell/test/browser/browser_onunload_stop.js b/docshell/test/browser/browser_onunload_stop.js
new file mode 100644
index 0000000000..e5bacfee06
--- /dev/null
+++ b/docshell/test/browser/browser_onunload_stop.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_1 =
+ "http://mochi.test:8888/browser/docshell/test/browser/dummy_page.html";
+
+const TEST_PAGE_2 =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/docshell/test/browser/dummy_page.html";
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE_1, async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, TEST_PAGE_2);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.addEventListener("unload", e => e.currentTarget.stop(), true);
+ });
+ BrowserTestUtils.startLoadingURIString(browser, TEST_PAGE_2);
+ await loaded;
+ ok(true, "Page loaded successfully");
+ });
+});
diff --git a/docshell/test/browser/browser_overlink.js b/docshell/test/browser/browser_overlink.js
new file mode 100644
index 0000000000..f9371d59ce
--- /dev/null
+++ b/docshell/test/browser/browser_overlink.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function test_stripAuthCredentials() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "overlink_test.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], function () {
+ content.document.getElementById("link").focus();
+ });
+
+ await TestUtils.waitForCondition(
+ () =>
+ XULBrowserWindow.overLink ==
+ UrlbarTestUtils.trimURL("https://example.com"),
+ "Overlink should be missing auth credentials"
+ );
+
+ ok(true, "Test successful");
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_platform_emulation.js b/docshell/test/browser/browser_platform_emulation.js
new file mode 100644
index 0000000000..017b615e15
--- /dev/null
+++ b/docshell/test/browser/browser_platform_emulation.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "data:text/html;charset=utf-8,<iframe id='test-iframe'></iframe>";
+
+async function contentTaskNoOverride() {
+ let docshell = docShell;
+ is(
+ docshell.browsingContext.customPlatform,
+ "",
+ "There should initially be no customPlatform"
+ );
+}
+
+async function contentTaskOverride() {
+ let docshell = docShell;
+ is(
+ docshell.browsingContext.customPlatform,
+ "foo",
+ "The platform should be changed to foo"
+ );
+
+ is(
+ content.navigator.platform,
+ "foo",
+ "The platform should be changed to foo"
+ );
+
+ let frameWin = content.document.querySelector("#test-iframe").contentWindow;
+ is(
+ frameWin.navigator.platform,
+ "foo",
+ "The platform should be passed on to frames."
+ );
+
+ let newFrame = content.document.createElement("iframe");
+ content.document.body.appendChild(newFrame);
+
+ let newFrameWin = newFrame.contentWindow;
+ is(
+ newFrameWin.navigator.platform,
+ "foo",
+ "Newly created frames should use the new platform"
+ );
+
+ newFrameWin.location.reload();
+ await ContentTaskUtils.waitForEvent(newFrame, "load");
+
+ is(
+ newFrameWin.navigator.platform,
+ "foo",
+ "New platform should persist across reloads"
+ );
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], contentTaskNoOverride);
+
+ let browsingContext = BrowserTestUtils.getBrowsingContextFrom(browser);
+ browsingContext.customPlatform = "foo";
+
+ await SpecialPowers.spawn(browser, [], contentTaskOverride);
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_search_notification.js b/docshell/test/browser/browser_search_notification.js
new file mode 100644
index 0000000000..f98ad22a40
--- /dev/null
+++ b/docshell/test/browser/browser_search_notification.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+add_task(async function () {
+ // Our search would be handled by the urlbar normally and not by the docshell,
+ // thus we must force going through dns first, so that the urlbar thinks
+ // the value may be a url, and asks the docshell to visit it.
+ // On NS_ERROR_UNKNOWN_HOST the docshell will fix it up.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.dns_first_for_single_words", true]],
+ });
+ const kSearchEngineID = "test_urifixup_search_engine";
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: kSearchEngineID,
+ search_url: "http://localhost/",
+ search_url_get_params: "search={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ let selectedName = (await Services.search.getDefault()).name;
+ Assert.equal(
+ selectedName,
+ kSearchEngineID,
+ "Check fake search engine is selected"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gBrowser.selectedTab = tab;
+
+ gURLBar.value = "firefox";
+ gURLBar.handleCommand();
+
+ let [subject, data] = await TestUtils.topicObserved("keyword-search");
+
+ let engine = subject.QueryInterface(Ci.nsISupportsString).data;
+
+ Assert.equal(engine, kSearchEngineID, "Should be the search engine id");
+ Assert.equal(data, "firefox", "Notification data is search term.");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/docshell/test/browser/browser_tab_replace_while_loading.js b/docshell/test/browser/browser_tab_replace_while_loading.js
new file mode 100644
index 0000000000..3c85f72192
--- /dev/null
+++ b/docshell/test/browser/browser_tab_replace_while_loading.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Test for bug 1578379. */
+
+add_task(async function test_window_open_about_blank() {
+ const URL =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_open_about_blank.html";
+ let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Opening about:blank using a click");
+ await SpecialPowers.spawn(firstTab.linkedBrowser, [""], async function () {
+ content.document.querySelector("#open").click();
+ });
+
+ info("Waiting for the second tab to be opened");
+ let secondTab = await promiseTabOpened;
+
+ info("Detaching tab");
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(secondTab);
+ let win = await windowOpenedPromise;
+
+ info("Asserting document is visible");
+ let tab = win.gBrowser.selectedTab;
+ await SpecialPowers.spawn(tab.linkedBrowser, [""], async function () {
+ is(
+ content.document.visibilityState,
+ "visible",
+ "Document should be visible"
+ );
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+ await BrowserTestUtils.removeTab(firstTab);
+});
+
+add_task(async function test_detach_loading_page() {
+ const URL =
+ "http://mochi.test:8888/browser/docshell/test/browser/file_slow_load.sjs";
+ // Open a dummy tab so that detaching the second tab works.
+ let dummyTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ let slowLoadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ URL,
+ /* waitForLoad = */ false
+ );
+
+ info("Wait for content document to be created");
+ await BrowserTestUtils.waitForCondition(async function () {
+ return SpecialPowers.spawn(
+ slowLoadingTab.linkedBrowser,
+ [URL],
+ async function (url) {
+ return content.document.documentURI == url;
+ }
+ );
+ });
+
+ info("Detaching tab");
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(slowLoadingTab);
+ let win = await windowOpenedPromise;
+
+ info("Asserting document is visible");
+ let tab = win.gBrowser.selectedTab;
+ await SpecialPowers.spawn(tab.linkedBrowser, [""], async function () {
+ is(content.document.readyState, "loading");
+ is(content.document.visibilityState, "visible");
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+ await BrowserTestUtils.removeTab(dummyTab);
+});
diff --git a/docshell/test/browser/browser_tab_touch_events.js b/docshell/test/browser/browser_tab_touch_events.js
new file mode 100644
index 0000000000..c73b01e45c
--- /dev/null
+++ b/docshell/test/browser/browser_tab_touch_events.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const URI = "data:text/html;charset=utf-8,<iframe id='test-iframe'></iframe>";
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URI },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], test_init);
+
+ browser.browsingContext.touchEventsOverride = "disabled";
+
+ await SpecialPowers.spawn(browser, [], test_body);
+ }
+ );
+});
+
+async function test_init() {
+ is(
+ content.browsingContext.touchEventsOverride,
+ "none",
+ "touchEventsOverride flag should be initially set to NONE"
+ );
+}
+
+async function test_body() {
+ let bc = content.browsingContext;
+ is(
+ bc.touchEventsOverride,
+ "disabled",
+ "touchEventsOverride flag should be changed to DISABLED"
+ );
+
+ let frameWin = content.document.querySelector("#test-iframe").contentWindow;
+ bc = frameWin.browsingContext;
+ is(
+ bc.touchEventsOverride,
+ "disabled",
+ "touchEventsOverride flag should be passed on to frames."
+ );
+
+ let newFrame = content.document.createElement("iframe");
+ content.document.body.appendChild(newFrame);
+
+ let newFrameWin = newFrame.contentWindow;
+ bc = newFrameWin.browsingContext;
+ is(
+ bc.touchEventsOverride,
+ "disabled",
+ "Newly created frames should use the new touchEventsOverride flag"
+ );
+
+ // Wait for the non-transient about:blank to load.
+ await ContentTaskUtils.waitForEvent(newFrame, "load");
+ newFrameWin = newFrame.contentWindow;
+ bc = newFrameWin.browsingContext;
+ is(
+ bc.touchEventsOverride,
+ "disabled",
+ "Newly created frames should use the new touchEventsOverride flag"
+ );
+
+ newFrameWin.location.reload();
+ await ContentTaskUtils.waitForEvent(newFrame, "load");
+ newFrameWin = newFrame.contentWindow;
+ bc = newFrameWin.browsingContext;
+ is(
+ bc.touchEventsOverride,
+ "disabled",
+ "New touchEventsOverride flag should persist across reloads"
+ );
+}
diff --git a/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js b/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js
new file mode 100644
index 0000000000..9d5a91bef9
--- /dev/null
+++ b/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js
@@ -0,0 +1,285 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test exercises the behaviour where user-initiated link clicks on
+ * the top-level document result in pageloads in a _blank target in a new
+ * browser window.
+ */
+
+const TEST_PAGE = "https://example.com/browser/";
+const TEST_PAGE_2 = "https://example.com/browser/components/";
+const TEST_IFRAME_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_iframe_page.html";
+
+// There is an <a> element with this href=".." in the TEST_PAGE
+// that we will click, which should take us up a level.
+const LINK_URL = "https://example.com/";
+
+/**
+ * Test that a user-initiated link click results in targeting to a new
+ * <browser> element, and that this properly sets the referrer on the newly
+ * loaded document.
+ */
+add_task(async function target_to_new_blank_browser() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let originalTab = win.gBrowser.selectedTab;
+ let originalBrowser = originalTab.linkedBrowser;
+ BrowserTestUtils.startLoadingURIString(originalBrowser, TEST_PAGE);
+ await BrowserTestUtils.browserLoaded(originalBrowser, false, TEST_PAGE);
+
+ // Now set the targetTopLevelLinkClicksToBlank property to true, since it
+ // defaults to false.
+ originalBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true;
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL);
+ await SpecialPowers.spawn(originalBrowser, [], async () => {
+ let anchor = content.document.querySelector(`a[href=".."]`);
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ anchor.click();
+ } finally {
+ userInput.destruct();
+ }
+ });
+ let newTab = await newTabPromise;
+ let newBrowser = newTab.linkedBrowser;
+
+ Assert.ok(
+ originalBrowser !== newBrowser,
+ "A new browser should have been created."
+ );
+ await SpecialPowers.spawn(newBrowser, [TEST_PAGE], async referrer => {
+ Assert.equal(
+ content.document.referrer,
+ referrer,
+ "Should have gotten the right referrer set"
+ );
+ });
+ await BrowserTestUtils.switchTab(win.gBrowser, originalTab);
+ BrowserTestUtils.removeTab(newTab);
+
+ // Now do the same thing with a subframe targeting "_top". This should also
+ // get redirected to "_blank".
+ BrowserTestUtils.startLoadingURIString(originalBrowser, TEST_IFRAME_PAGE);
+ await BrowserTestUtils.browserLoaded(
+ originalBrowser,
+ false,
+ TEST_IFRAME_PAGE
+ );
+
+ newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL);
+ let frameBC1 = originalBrowser.browsingContext.children[0];
+ Assert.ok(frameBC1, "Should have found a subframe BrowsingContext");
+
+ await SpecialPowers.spawn(frameBC1, [LINK_URL], async linkUrl => {
+ let anchor = content.document.createElement("a");
+ anchor.setAttribute("href", linkUrl);
+ anchor.setAttribute("target", "_top");
+ content.document.body.appendChild(anchor);
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ anchor.click();
+ } finally {
+ userInput.destruct();
+ }
+ });
+ newTab = await newTabPromise;
+ newBrowser = newTab.linkedBrowser;
+
+ Assert.ok(
+ originalBrowser !== newBrowser,
+ "A new browser should have been created."
+ );
+ await SpecialPowers.spawn(
+ newBrowser,
+ [frameBC1.currentURI.spec],
+ async referrer => {
+ Assert.equal(
+ content.document.referrer,
+ referrer,
+ "Should have gotten the right referrer set"
+ );
+ }
+ );
+ await BrowserTestUtils.switchTab(win.gBrowser, originalTab);
+ BrowserTestUtils.removeTab(newTab);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Test that we don't target to _blank loads caused by:
+ * 1. POST requests
+ * 2. Any load that isn't "normal" (in the nsIDocShell.LOAD_CMD_NORMAL sense)
+ * 3. Any loads that are caused by location.replace
+ * 4. Any loads that were caused by setting location.href
+ * 5. Link clicks fired without user interaction.
+ */
+add_task(async function skip_blank_target_for_some_loads() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let currentBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(currentBrowser, TEST_PAGE);
+ await BrowserTestUtils.browserLoaded(currentBrowser, false, TEST_PAGE);
+
+ // Now set the targetTopLevelLinkClicksToBlank property to true, since it
+ // defaults to false.
+ currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true;
+
+ let ensureSingleBrowser = () => {
+ Assert.equal(
+ win.gBrowser.browsers.length,
+ 1,
+ "There should only be 1 browser."
+ );
+
+ Assert.ok(
+ currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank,
+ "Should still be targeting top-level clicks to _blank"
+ );
+ };
+
+ // First we'll test a POST request
+ let sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ TEST_PAGE
+ );
+ await SpecialPowers.spawn(currentBrowser, [], async () => {
+ let doc = content.document;
+ let form = doc.createElement("form");
+ form.setAttribute("method", "post");
+ doc.body.appendChild(form);
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ form.submit();
+ } finally {
+ userInput.destruct();
+ }
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+
+ // Next, we'll try a non-normal load - specifically, we'll try a reload.
+ // Since we've got a page loaded via a POST request, an attempt to reload
+ // will cause the "repost" dialog to appear, so we temporarily allow the
+ // repost to go through with the always_accept testing pref.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.confirm_repost.testing.always_accept", true]],
+ });
+ sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ TEST_PAGE
+ );
+ await SpecialPowers.spawn(currentBrowser, [], async () => {
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ content.location.reload();
+ } finally {
+ userInput.destruct();
+ }
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+ await SpecialPowers.popPrefEnv();
+
+ // Next, we'll try a location.replace
+ sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ TEST_PAGE_2
+ );
+ await SpecialPowers.spawn(currentBrowser, [TEST_PAGE_2], async page2 => {
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ content.location.replace(page2);
+ } finally {
+ userInput.destruct();
+ }
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+
+ // Finally we'll try setting location.href
+ sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ TEST_PAGE
+ );
+ await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async page1 => {
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ content.location.href = page1;
+ } finally {
+ userInput.destruct();
+ }
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+
+ // Now that we're back at TEST_PAGE, let's try a scripted link click. This
+ // shouldn't target to _blank.
+ sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ LINK_URL
+ );
+ await SpecialPowers.spawn(currentBrowser, [], async () => {
+ let anchor = content.document.querySelector(`a[href=".."]`);
+ anchor.click();
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+
+ // A javascript:void(0); link should also not target to _blank.
+ sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ TEST_PAGE
+ );
+ await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => {
+ let anchor = content.document.querySelector(`a[href=".."]`);
+ anchor.href = "javascript:void(0);";
+ anchor.addEventListener("click", e => {
+ content.location.href = newPageURL;
+ });
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ anchor.click();
+ } finally {
+ userInput.destruct();
+ }
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+
+ // Let's also try a non-void javascript: location.
+ sameBrowserLoad = BrowserTestUtils.browserLoaded(
+ currentBrowser,
+ false,
+ TEST_PAGE
+ );
+ await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => {
+ let anchor = content.document.querySelector(`a[href=".."]`);
+ anchor.href = `javascript:"string-to-navigate-to"`;
+ anchor.addEventListener("click", e => {
+ content.location.href = newPageURL;
+ });
+ let userInput = content.windowUtils.setHandlingUserInput(true);
+ try {
+ anchor.click();
+ } finally {
+ userInput.destruct();
+ }
+ });
+ await sameBrowserLoad;
+ ensureSingleBrowser();
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/docshell/test/browser/browser_title_in_session_history.js b/docshell/test/browser/browser_title_in_session_history.js
new file mode 100644
index 0000000000..bdcbbb7dfe
--- /dev/null
+++ b/docshell/test/browser/browser_title_in_session_history.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test() {
+ const TEST_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let titles = await ContentTask.spawn(browser, null, () => {
+ return new Promise(resolve => {
+ let titles = [];
+ content.document.body.innerHTML = "<div id='foo'>foo</div>";
+ content.document.title = "Initial";
+ content.history.pushState("1", "1", "1");
+ content.document.title = "1";
+ content.history.pushState("2", "2", "2");
+ content.document.title = "2";
+ content.location.hash = "hash";
+ content.document.title = "3-hash";
+ content.addEventListener(
+ "popstate",
+ () => {
+ content.addEventListener(
+ "popstate",
+ () => {
+ titles.push(content.document.title);
+ resolve(titles);
+ },
+ { once: true }
+ );
+
+ titles.push(content.document.title);
+ // Test going forward a few steps.
+ content.history.go(2);
+ },
+ { once: true }
+ );
+ // Test going back a few steps.
+ content.history.go(-3);
+ });
+ });
+ is(
+ titles[0],
+ "3-hash",
+ "Document.title should have the value to which it was last time set."
+ );
+ is(
+ titles[1],
+ "3-hash",
+ "Document.title should have the value to which it was last time set."
+ );
+ let sh = browser.browsingContext.sessionHistory;
+ let count = sh.count;
+ is(sh.getEntryAtIndex(count - 1).title, "3-hash");
+ is(sh.getEntryAtIndex(count - 2).title, "2");
+ is(sh.getEntryAtIndex(count - 3).title, "1");
+ is(sh.getEntryAtIndex(count - 4).title, "Initial");
+ });
+});
diff --git a/docshell/test/browser/browser_ua_emulation.js b/docshell/test/browser/browser_ua_emulation.js
new file mode 100644
index 0000000000..f1e9657cd3
--- /dev/null
+++ b/docshell/test/browser/browser_ua_emulation.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "data:text/html;charset=utf-8,<iframe id='test-iframe'></iframe>";
+
+// Test that the docShell UA emulation works
+async function contentTaskNoOverride() {
+ let docshell = docShell;
+ is(
+ docshell.browsingContext.customUserAgent,
+ "",
+ "There should initially be no customUserAgent"
+ );
+}
+
+async function contentTaskOverride() {
+ let docshell = docShell;
+ is(
+ docshell.browsingContext.customUserAgent,
+ "foo",
+ "The user agent should be changed to foo"
+ );
+
+ is(
+ content.navigator.userAgent,
+ "foo",
+ "The user agent should be changed to foo"
+ );
+
+ let frameWin = content.document.querySelector("#test-iframe").contentWindow;
+ is(
+ frameWin.navigator.userAgent,
+ "foo",
+ "The UA should be passed on to frames."
+ );
+
+ let newFrame = content.document.createElement("iframe");
+ content.document.body.appendChild(newFrame);
+
+ let newFrameWin = newFrame.contentWindow;
+ is(
+ newFrameWin.navigator.userAgent,
+ "foo",
+ "Newly created frames should use the new UA"
+ );
+
+ newFrameWin.location.reload();
+ await ContentTaskUtils.waitForEvent(newFrame, "load");
+
+ is(
+ newFrameWin.navigator.userAgent,
+ "foo",
+ "New UA should persist across reloads"
+ );
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], contentTaskNoOverride);
+
+ let browsingContext = BrowserTestUtils.getBrowsingContextFrom(browser);
+ browsingContext.customUserAgent = "foo";
+
+ await SpecialPowers.spawn(browser, [], contentTaskOverride);
+ }
+ );
+});
diff --git a/docshell/test/browser/browser_uriFixupAlternateRedirects.js b/docshell/test/browser/browser_uriFixupAlternateRedirects.js
new file mode 100644
index 0000000000..193e8cf489
--- /dev/null
+++ b/docshell/test/browser/browser_uriFixupAlternateRedirects.js
@@ -0,0 +1,66 @@
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+const REDIRECTURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.example.com/browser/docshell/test/browser/redirect_to_example.sjs";
+
+add_task(async function () {
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ gURLBar.value = value;
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value,
+ });
+ },
+ ];
+ for (let setValueFn of setValueFns) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ // Enter search terms and start a search.
+ gURLBar.focus();
+ await setValueFn(REDIRECTURL);
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await errorPageLoaded;
+ let [contentURL, originalURL] = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return [
+ content.document.documentURI,
+ content.document.mozDocumentURIIfNotForErrorPages.spec,
+ ];
+ }
+ );
+ info("Page that loaded: " + contentURL);
+ const errorURI = "about:neterror?";
+ ok(contentURL.startsWith(errorURI), "Should be on an error page");
+
+ const contentPrincipal = tab.linkedBrowser.contentPrincipal;
+ ok(
+ contentPrincipal.spec.startsWith(errorURI),
+ "Principal should be for the error page"
+ );
+
+ originalURL = new URL(originalURL);
+ is(
+ originalURL.host,
+ "example",
+ "Should be an error for http://example, not http://www.example.com/"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/docshell/test/browser/browser_uriFixupIntegration.js b/docshell/test/browser/browser_uriFixupIntegration.js
new file mode 100644
index 0000000000..619fd9f61f
--- /dev/null
+++ b/docshell/test/browser/browser_uriFixupIntegration.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+const kSearchEngineID = "browser_urifixup_search_engine";
+const kSearchEngineURL = "https://example.com/?search={searchTerms}";
+const kPrivateSearchEngineID = "browser_urifixup_search_engine_private";
+const kPrivateSearchEngineURL = "https://example.com/?private={searchTerms}";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ],
+ });
+
+ // Add new fake search engines.
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: kSearchEngineID,
+ search_url: "https://example.com/",
+ search_url_get_params: "search={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: kPrivateSearchEngineID,
+ search_url: "https://example.com/",
+ search_url_get_params: "private={searchTerms}",
+ },
+ { setAsDefaultPrivate: true }
+ );
+});
+
+add_task(async function test() {
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ (value, win) => {
+ win.gURLBar.value = value;
+ },
+ (value, win) => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus: SimpleTest.waitForFocus,
+ value,
+ });
+ },
+ ];
+
+ for (let value of ["foo bar", "brokenprotocol:somethingelse"]) {
+ for (let setValueFn of setValueFns) {
+ for (let inPrivateWindow of [false, true]) {
+ await do_test(value, setValueFn, inPrivateWindow);
+ }
+ }
+ }
+});
+
+async function do_test(value, setValueFn, inPrivateWindow) {
+ info(`Search ${value} in a ${inPrivateWindow ? "private" : "normal"} window`);
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: inPrivateWindow,
+ });
+ // Enter search terms and start a search.
+ win.gURLBar.focus();
+ await setValueFn(value, win);
+
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+
+ // Check that we load the correct URL.
+ let escapedValue = encodeURIComponent(value).replace("%20", "+");
+ let searchEngineUrl = inPrivateWindow
+ ? kPrivateSearchEngineURL
+ : kSearchEngineURL;
+ let expectedURL = searchEngineUrl.replace("{searchTerms}", escapedValue);
+ await BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ expectedURL
+ );
+ // There should be at least one test.
+ Assert.equal(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ expectedURL,
+ "New tab should have loaded with expected url."
+ );
+
+ // Cleanup.
+ await BrowserTestUtils.closeWindow(win);
+}
diff --git a/docshell/test/browser/browser_viewsource_chrome_to_content.js b/docshell/test/browser/browser_viewsource_chrome_to_content.js
new file mode 100644
index 0000000000..933af2a4af
--- /dev/null
+++ b/docshell/test/browser/browser_viewsource_chrome_to_content.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_URI = `view-source:${TEST_PATH}dummy_page.html`;
+
+add_task(async function chrome_to_content_view_source() {
+ await BrowserTestUtils.withNewTab("about:mozilla", async browser => {
+ is(browser.documentURI.spec, "about:mozilla");
+
+ // This process switch would previously crash in debug builds due to assertion failures.
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URI);
+ await BrowserTestUtils.browserLoaded(browser);
+ is(browser.documentURI.spec, TEST_URI);
+ });
+});
diff --git a/docshell/test/browser/browser_viewsource_multipart.js b/docshell/test/browser/browser_viewsource_multipart.js
new file mode 100644
index 0000000000..748cf4bce0
--- /dev/null
+++ b/docshell/test/browser/browser_viewsource_multipart.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const MULTIPART_URI = `${TEST_PATH}file_basic_multipart.sjs`;
+
+add_task(async function viewsource_multipart_uri() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ BrowserTestUtils.startLoadingURIString(browser, MULTIPART_URI);
+ await BrowserTestUtils.browserLoaded(browser);
+ is(browser.currentURI.spec, MULTIPART_URI);
+
+ // Continue probing the URL until we find the h1 we're expecting. This
+ // should handle cases where we somehow beat the second document having
+ // loaded.
+ await TestUtils.waitForCondition(async () => {
+ let value = await SpecialPowers.spawn(browser, [], async () => {
+ let headers = content.document.querySelectorAll("h1");
+ is(headers.length, 1, "only one h1 should be present");
+ return headers[0].textContent;
+ });
+
+ ok(value == "First" || value == "Second", "some other value was found?");
+ return value == "Second";
+ });
+
+ // Load a view-source version of the page, which should show the full
+ // content, not handling multipart.
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `view-source:${MULTIPART_URI}`
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let viewSourceContent = await SpecialPowers.spawn(browser, [], async () => {
+ return content.document.body.textContent;
+ });
+
+ ok(viewSourceContent.includes("<h1>First</h1>"), "first header");
+ ok(viewSourceContent.includes("<h1>Second</h1>"), "second header");
+ ok(viewSourceContent.includes("BOUNDARY"), "boundary");
+ });
+});
diff --git a/docshell/test/browser/dummy_iframe_page.html b/docshell/test/browser/dummy_iframe_page.html
new file mode 100644
index 0000000000..12ce921856
--- /dev/null
+++ b/docshell/test/browser/dummy_iframe_page.html
@@ -0,0 +1,8 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ just a dummy html file with an iframe
+ <iframe id="frame1" src="dummy_page.html?sub_entry=0"></iframe>
+ <iframe id="frame2" src="dummy_page.html?sub_entry=0"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/browser/dummy_page.html b/docshell/test/browser/dummy_page.html
new file mode 100644
index 0000000000..59bf2a5f8f
--- /dev/null
+++ b/docshell/test/browser/dummy_page.html
@@ -0,0 +1,6 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ just a dummy html file
+ </body>
+</html>
diff --git a/docshell/test/browser/favicon_bug655270.ico b/docshell/test/browser/favicon_bug655270.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/docshell/test/browser/favicon_bug655270.ico
Binary files differ
diff --git a/docshell/test/browser/file_backforward_restore_scroll.html b/docshell/test/browser/file_backforward_restore_scroll.html
new file mode 100644
index 0000000000..5a40b36c10
--- /dev/null
+++ b/docshell/test/browser/file_backforward_restore_scroll.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe src="http://mochi.test:8888/"></iframe>
+ <iframe src="http://example.com/"></iframe>
+</body>
+</html>
diff --git a/docshell/test/browser/file_backforward_restore_scroll.html^headers^ b/docshell/test/browser/file_backforward_restore_scroll.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/browser/file_backforward_restore_scroll.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/browser/file_basic_multipart.sjs b/docshell/test/browser/file_basic_multipart.sjs
new file mode 100644
index 0000000000..5e89b93948
--- /dev/null
+++ b/docshell/test/browser/file_basic_multipart.sjs
@@ -0,0 +1,24 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader(
+ "Content-Type",
+ "multipart/x-mixed-replace;boundary=BOUNDARY",
+ false
+ );
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ response.write(`--BOUNDARY
+Content-Type: text/html
+
+<h1>First</h1>
+Will be replaced
+--BOUNDARY
+Content-Type: text/html
+
+<h1>Second</h1>
+This will stick around
+--BOUNDARY
+--BOUNDARY--
+`);
+}
diff --git a/docshell/test/browser/file_bug1046022.html b/docshell/test/browser/file_bug1046022.html
new file mode 100644
index 0000000000..27a1e1f079
--- /dev/null
+++ b/docshell/test/browser/file_bug1046022.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Bug 1046022 - test navigating inside onbeforeunload</title>
+ </head>
+ <body>
+ Waiting for onbeforeunload to hit...
+ </body>
+
+ <script>
+var testFns = [
+ function(e) {
+ e.target.location.href = "otherpage-href-set.html";
+ return "stop";
+ },
+ function(e) {
+ e.target.location.reload();
+ return "stop";
+ },
+ function(e) {
+ e.currentTarget.stop();
+ return "stop";
+ },
+ function(e) {
+ e.target.location.replace("otherpage-location-replaced.html");
+ return "stop";
+ },
+ function(e) {
+ var link = e.target.createElement("a");
+ link.href = "otherpage.html";
+ e.target.body.appendChild(link);
+ link.click();
+ return "stop";
+ },
+ function(e) {
+ var link = e.target.createElement("a");
+ link.href = "otherpage.html";
+ link.setAttribute("target", "_blank");
+ e.target.body.appendChild(link);
+ link.click();
+ return "stop";
+ },
+ function(e) {
+ var link = e.target.createElement("a");
+ link.href = e.target.location.href;
+ e.target.body.appendChild(link);
+ link.setAttribute("target", "somearbitrarywindow");
+ link.click();
+ return "stop";
+ },
+];
+ </script>
+</html>
diff --git a/docshell/test/browser/file_bug1206879.html b/docshell/test/browser/file_bug1206879.html
new file mode 100644
index 0000000000..5313902a9b
--- /dev/null
+++ b/docshell/test/browser/file_bug1206879.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test page for bug 1206879</title>
+ </head>
+ <body>
+ <iframe src="http://example.com/"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/browser/file_bug1328501.html b/docshell/test/browser/file_bug1328501.html
new file mode 100644
index 0000000000..517ef53e02
--- /dev/null
+++ b/docshell/test/browser/file_bug1328501.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Page with iframes</title>
+ <script type="application/javascript">
+ let promiseResolvers = {
+ "testFrame1": {},
+ "testFrame2": {},
+ };
+ let promises = [
+ new Promise(r => promiseResolvers.testFrame1.resolve = r),
+ new Promise(r => promiseResolvers.testFrame2.resolve = r),
+ ];
+ function frameLoaded(frame) {
+ promiseResolvers[frame].resolve();
+ }
+ Promise.all(promises).then(() => window.dispatchEvent(new Event("frames-loaded")));
+ </script>
+ </head>
+ <body onunload="">
+ <div>
+ <iframe id="testFrame1" src="dummy_page.html" onload="frameLoaded(this.id);" ></iframe>
+ <iframe id="testFrame2" src="dummy_page.html" onload="frameLoaded(this.id);" ></iframe>
+ </div>
+ </body>
+</html>
diff --git a/docshell/test/browser/file_bug1328501_frame.html b/docshell/test/browser/file_bug1328501_frame.html
new file mode 100644
index 0000000000..156dd41eaa
--- /dev/null
+++ b/docshell/test/browser/file_bug1328501_frame.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<html lang="en">
+ <body>Subframe page for testing</body>
+</html>
diff --git a/docshell/test/browser/file_bug1328501_framescript.js b/docshell/test/browser/file_bug1328501_framescript.js
new file mode 100644
index 0000000000..19c86c75e7
--- /dev/null
+++ b/docshell/test/browser/file_bug1328501_framescript.js
@@ -0,0 +1,38 @@
+// Forward iframe loaded event.
+
+/* eslint-env mozilla/frame-script */
+
+addEventListener(
+ "frames-loaded",
+ e => sendAsyncMessage("test:frames-loaded"),
+ true,
+ true
+);
+
+let requestObserver = {
+ observe(subject, topic, data) {
+ if (topic == "http-on-opening-request") {
+ // Get DOMWindow on all child docshells to force about:blank
+ // content viewers being created.
+ getChildDocShells().map(ds => {
+ ds
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsILoadContext).associatedWindow;
+ });
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+};
+Services.obs.addObserver(requestObserver, "http-on-opening-request");
+addEventListener("unload", e => {
+ if (e.target == this) {
+ Services.obs.removeObserver(requestObserver, "http-on-opening-request");
+ }
+});
+
+function getChildDocShells() {
+ return docShell.getAllDocShellsInSubtree(
+ Ci.nsIDocShellTreeItem.typeAll,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS
+ );
+}
diff --git a/docshell/test/browser/file_bug1543077-3-child.html b/docshell/test/browser/file_bug1543077-3-child.html
new file mode 100644
index 0000000000..858a4623ed
--- /dev/null
+++ b/docshell/test/browser/file_bug1543077-3-child.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in parent or child</title>
+</head>
+<body>
+<p>Hiragana letter a if decoded as ISO-2022-JP: $B$"(B</p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug1543077-3.html b/docshell/test/browser/file_bug1543077-3.html
new file mode 100644
index 0000000000..c4f467dd3f
--- /dev/null
+++ b/docshell/test/browser/file_bug1543077-3.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in parent or child</title>
+</head>
+<body>
+<h1>No encoding declaration in parent or child</h1>
+
+<p>Hiragana letter a if decoded as ISO-2022-JP: $B$"(B</p>
+
+<iframe src="file_bug1543077-3-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug1622420.html b/docshell/test/browser/file_bug1622420.html
new file mode 100644
index 0000000000..63beb38302
--- /dev/null
+++ b/docshell/test/browser/file_bug1622420.html
@@ -0,0 +1 @@
+<iframe src="http://example.com/"></iframe>
diff --git a/docshell/test/browser/file_bug1648464-1-child.html b/docshell/test/browser/file_bug1648464-1-child.html
new file mode 100644
index 0000000000..7bb1ad965b
--- /dev/null
+++ b/docshell/test/browser/file_bug1648464-1-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset=windows-1252>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>windows-1252 in parent and child, actually EUC-JP</title>
+</head>
+<body>
+<p>Hiragana letter a if decoded as EUC-JP: </p>
+<p>ʸ¸Ǥ</p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug1648464-1.html b/docshell/test/browser/file_bug1648464-1.html
new file mode 100644
index 0000000000..2051cf61ed
--- /dev/null
+++ b/docshell/test/browser/file_bug1648464-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset=windows-1252>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>windows-1252 in parent and child, actually EUC-JP</title>
+</head>
+<body>
+<h1>windows-1252 in parent and child, actually EUC-JP</h1>
+
+<p>Hiragana letter a if decoded as EUC-JP: </p>
+<p>ʸ¸Ǥ</p>
+
+<iframe src="file_bug1648464-1-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug1673702.json b/docshell/test/browser/file_bug1673702.json
new file mode 100644
index 0000000000..3f562dd675
--- /dev/null
+++ b/docshell/test/browser/file_bug1673702.json
@@ -0,0 +1 @@
+{ "version": 1 }
diff --git a/docshell/test/browser/file_bug1673702.json^headers^ b/docshell/test/browser/file_bug1673702.json^headers^
new file mode 100644
index 0000000000..6010bfd188
--- /dev/null
+++ b/docshell/test/browser/file_bug1673702.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/docshell/test/browser/file_bug1688368-1.sjs b/docshell/test/browser/file_bug1688368-1.sjs
new file mode 100644
index 0000000000..0693b7970c
--- /dev/null
+++ b/docshell/test/browser/file_bug1688368-1.sjs
@@ -0,0 +1,44 @@
+"use strict";
+
+const DELAY = 1 * 1000; // Delay one second before completing the request.
+
+let nsTimer = Components.Constructor(
+ "@mozilla.org/timer;1",
+ "nsITimer",
+ "initWithCallback"
+);
+
+let timer;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(`<!DOCTYPE html>
+<html>
+<head>
+ <title>UTF-8 file, 1024 bytes long!</title>
+</head>
+<body>`);
+
+ // Note: We need to store a reference to the timer to prevent it from being
+ // canceled when it's GCed.
+ timer = new nsTimer(
+ () => {
+ var snowmen =
+ "\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083";
+ response.write(
+ snowmen +
+ `
+</body>
+</html>
+
+`
+ );
+ response.finish();
+ },
+ DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/docshell/test/browser/file_bug1691153.html b/docshell/test/browser/file_bug1691153.html
new file mode 100644
index 0000000000..dea144eb41
--- /dev/null
+++ b/docshell/test/browser/file_bug1691153.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>bug 1691153</title>
+</head>
+<body>
+<h1>bug 1691153</h1>
+<script>
+function toBlobURL(data, mimeType) {
+ return URL.createObjectURL(
+ new Blob([data], {
+ type: mimeType,
+ })
+ );
+}
+// closing script element literal split up to not end the parent script element
+let testurl = toBlobURL("<body></body>", "text/html");
+addEventListener("message", event => {
+ if (event.data == "getblob") {
+ postMessage({ bloburl: testurl }, "*");
+ }
+});
+// the blob URL should have a content principal
+</script>
+</body>
+</html>
diff --git a/docshell/test/browser/file_bug1716290-1.sjs b/docshell/test/browser/file_bug1716290-1.sjs
new file mode 100644
index 0000000000..83e6eede3d
--- /dev/null
+++ b/docshell/test/browser/file_bug1716290-1.sjs
@@ -0,0 +1,21 @@
+function handleRequest(request, response) {
+ if (getState("reloaded") == "reloaded") {
+ response.setHeader(
+ "Content-Type",
+ "text/html; charset=windows-1254",
+ false
+ );
+ response.write("\u00E4");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=Shift_JIS", false);
+ if (getState("loaded") == "loaded") {
+ setState("reloaded", "reloaded");
+ } else {
+ setState("loaded", "loaded");
+ }
+ // kilobyte to force late-detection reload
+ response.write("a".repeat(1024));
+ response.write("<body>");
+ response.write("\u00E4");
+ }
+}
diff --git a/docshell/test/browser/file_bug1716290-2.sjs b/docshell/test/browser/file_bug1716290-2.sjs
new file mode 100644
index 0000000000..e695259e30
--- /dev/null
+++ b/docshell/test/browser/file_bug1716290-2.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+ if (getState("reloaded") == "reloaded") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<meta charset=iso-2022-kr>\u00E4");
+ } else {
+ response.setHeader("Content-Type", "text/html", false);
+ if (getState("loaded") == "loaded") {
+ setState("reloaded", "reloaded");
+ } else {
+ setState("loaded", "loaded");
+ }
+ response.write("<meta charset=Shift_JIS>");
+ // kilobyte to force late-detection reload
+ response.write("a".repeat(1024));
+ response.write("<body>");
+ response.write("\u00E4");
+ }
+}
diff --git a/docshell/test/browser/file_bug1716290-3.sjs b/docshell/test/browser/file_bug1716290-3.sjs
new file mode 100644
index 0000000000..7a302e05e4
--- /dev/null
+++ b/docshell/test/browser/file_bug1716290-3.sjs
@@ -0,0 +1,17 @@
+function handleRequest(request, response) {
+ if (getState("reloaded") == "reloaded") {
+ response.setHeader("Content-Type", "text/html; charset=iso-2022-kr", false);
+ response.write("\u00E4");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=Shift_JIS", false);
+ if (getState("loaded") == "loaded") {
+ setState("reloaded", "reloaded");
+ } else {
+ setState("loaded", "loaded");
+ }
+ // kilobyte to force late-detection reload
+ response.write("a".repeat(1024));
+ response.write("<body>");
+ response.write("\u00E4");
+ }
+}
diff --git a/docshell/test/browser/file_bug1716290-4.sjs b/docshell/test/browser/file_bug1716290-4.sjs
new file mode 100644
index 0000000000..36753ef532
--- /dev/null
+++ b/docshell/test/browser/file_bug1716290-4.sjs
@@ -0,0 +1,17 @@
+function handleRequest(request, response) {
+ if (getState("reloaded") == "reloaded") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("\u00FE\u00FF\u00E4");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=Shift_JIS", false);
+ if (getState("loaded") == "loaded") {
+ setState("reloaded", "reloaded");
+ } else {
+ setState("loaded", "loaded");
+ }
+ // kilobyte to force late-detection reload
+ response.write("a".repeat(1024));
+ response.write("<body>");
+ response.write("\u00E4");
+ }
+}
diff --git a/docshell/test/browser/file_bug1736248-1.html b/docshell/test/browser/file_bug1736248-1.html
new file mode 100644
index 0000000000..177acb8f77
--- /dev/null
+++ b/docshell/test/browser/file_bug1736248-1.html
@@ -0,0 +1,4 @@
+Kilobyte of ASCII followed by UTF-8.
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+
+Hej världen!
diff --git a/docshell/test/browser/file_bug234628-1-child.html b/docshell/test/browser/file_bug234628-1-child.html
new file mode 100644
index 0000000000..c36197ac4f
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-1-child.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in parent or child</title>
+</head>
+<body>
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-1.html b/docshell/test/browser/file_bug234628-1.html
new file mode 100644
index 0000000000..11c523ccd9
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in parent or child</title>
+</head>
+<body>
+<h1>No encoding declaration in parent or child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-1-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-10-child.xhtml b/docshell/test/browser/file_bug234628-10-child.xhtml
new file mode 100644
index 0000000000..cccf6f2bc0
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-10-child.xhtml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head><title>XML child with no encoding declaration</title></head>
+<body><p>Euro sign if decoded as UTF-8: €</p></body>
+</html>
diff --git a/docshell/test/browser/file_bug234628-10.html b/docshell/test/browser/file_bug234628-10.html
new file mode 100644
index 0000000000..78b8f0035d
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-10.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in HTML parent or XHTML child</title>
+</head>
+<body>
+<h1>No encoding declaration in HTML parent or XHTML child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-10-child.xhtml"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-11-child.xhtml b/docshell/test/browser/file_bug234628-11-child.xhtml
new file mode 100644
index 0000000000..11ef668b0c
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-11-child.xhtml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head><title>No encoding declaration in HTML parent and HTTP declaration in XHTML child</title></head>
+<body><p>Euro sign if decoded as UTF-8: €</p></body>
+</html>
diff --git a/docshell/test/browser/file_bug234628-11-child.xhtml^headers^ b/docshell/test/browser/file_bug234628-11-child.xhtml^headers^
new file mode 100644
index 0000000000..30fb304056
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-11-child.xhtml^headers^
@@ -0,0 +1 @@
+Content-Type: application/xhtml+xml; charset=utf-8
diff --git a/docshell/test/browser/file_bug234628-11.html b/docshell/test/browser/file_bug234628-11.html
new file mode 100644
index 0000000000..21c5b733e0
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-11.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in HTML parent and HTTP declaration in XHTML child</title>
+</head>
+<body>
+<h1>No encoding declaration in HTML parent and HTTP declaration in XHTML child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-11-child.xhtml"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-2-child.html b/docshell/test/browser/file_bug234628-2-child.html
new file mode 100644
index 0000000000..0acd2e0b27
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-2-child.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in parent or child</title>
+</head>
+<body>
+<p>Euro sign if decoded as UTF-8: €</p>
+<p>a with diaeresis if decoded as UTF-8: ä</p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-2.html b/docshell/test/browser/file_bug234628-2.html
new file mode 100644
index 0000000000..a87d29e126
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>No encoding declaration in parent or child</title>
+</head>
+<body>
+<h1>No encoding declaration in parent or child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-2-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-3-child.html b/docshell/test/browser/file_bug234628-3-child.html
new file mode 100644
index 0000000000..a6ad832310
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-3-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and child</title>
+</head>
+<body>
+<p>Euro sign if decoded as UTF-8: €</p>
+<p>a with diaeresis if decoded as UTF-8: ä</p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-3.html b/docshell/test/browser/file_bug234628-3.html
new file mode 100644
index 0000000000..8caab60402
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-3.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="windows-1252">
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and child</title>
+</head>
+<body>
+<h1>meta declaration in parent and child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-3-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-4-child.html b/docshell/test/browser/file_bug234628-4-child.html
new file mode 100644
index 0000000000..f0e7c2c058
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-4-child.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and BOM in child</title>
+</head>
+<body>
+<p>Euro sign if decoded as UTF-8: €</p>
+<p>a with diaeresis if decoded as UTF-8: ä</p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-4.html b/docshell/test/browser/file_bug234628-4.html
new file mode 100644
index 0000000000..0137579010
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-4.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="windows-1252">
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and BOM in child</title>
+</head>
+<body>
+<h1>meta declaration in parent and BOM in child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-4-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-5-child.html b/docshell/test/browser/file_bug234628-5-child.html
new file mode 100644
index 0000000000..a650552f63
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-5-child.html
Binary files differ
diff --git a/docshell/test/browser/file_bug234628-5.html b/docshell/test/browser/file_bug234628-5.html
new file mode 100644
index 0000000000..987e6420be
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-5.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="windows-1252">
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and UTF-16 BOM in child</title>
+</head>
+<body>
+<h1>meta declaration in parent and UTF-16 BOM in child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-5-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-6-child.html b/docshell/test/browser/file_bug234628-6-child.html
new file mode 100644
index 0000000000..52c37f2596
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-6-child.html
Binary files differ
diff --git a/docshell/test/browser/file_bug234628-6-child.html^headers^ b/docshell/test/browser/file_bug234628-6-child.html^headers^
new file mode 100644
index 0000000000..bfdcf487fb
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-6-child.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=utf-16be
diff --git a/docshell/test/browser/file_bug234628-6.html b/docshell/test/browser/file_bug234628-6.html
new file mode 100644
index 0000000000..9d7fc580c3
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-6.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="windows-1252">
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and BOMless UTF-16 with HTTP charset in child</title>
+</head>
+<body>
+<h1>meta declaration in parent and BOMless UTF-16 with HTTP charset in child</h1>
+
+<p>Euro sign if decoded as Windows-1252: </p>
+<p>a with diaeresis if decoded as Windows-1252: </p>
+
+<iframe src="file_bug234628-6-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-8-child.html b/docshell/test/browser/file_bug234628-8-child.html
new file mode 100644
index 0000000000..254e0fb2b3
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-8-child.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and no declaration in child</title>
+</head>
+<body>
+<p>Capital dje if decoded as Windows-1251: </p>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-8.html b/docshell/test/browser/file_bug234628-8.html
new file mode 100644
index 0000000000..b44e91801c
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-8.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="windows-1251">
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>meta declaration in parent and no declaration in child</title>
+</head>
+<body>
+<h1>meta declaration in parent and no declaration in child</h1>
+
+<p>Capital dje if decoded as Windows-1251: </p>
+
+<iframe src="file_bug234628-8-child.html"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-9-child.html b/docshell/test/browser/file_bug234628-9-child.html
new file mode 100644
index 0000000000..a86b14d7ee
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-9-child.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>UTF-16 with BOM in parent and no declaration in child</title>
+</head>
+<body>
+<p>Euro sign if decoded as Windows-1251: </p>
+
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug234628-9.html b/docshell/test/browser/file_bug234628-9.html
new file mode 100644
index 0000000000..8a469da3aa
--- /dev/null
+++ b/docshell/test/browser/file_bug234628-9.html
Binary files differ
diff --git a/docshell/test/browser/file_bug420605.html b/docshell/test/browser/file_bug420605.html
new file mode 100644
index 0000000000..8424b92f8f
--- /dev/null
+++ b/docshell/test/browser/file_bug420605.html
@@ -0,0 +1,31 @@
+<head>
+<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/>
+ <title>Page Title for Bug 420605</title>
+</head>
+<body>
+ <h1>Fragment links</h1>
+
+ <p>This page has a bunch of fragment links to sections below:</p>
+
+ <ul>
+ <li><a id="firefox-link" href="#firefox">Firefox</a></li>
+ <li><a id="thunderbird-link" href="#thunderbird">Thunderbird</a></li>
+ <li><a id="seamonkey-link" href="#seamonkey">Seamonkey</a></li>
+ </ul>
+
+ <p>And here are the sections:</p>
+
+ <h2 id="firefox">Firefox</h2>
+
+ <p>Firefox is a browser.</p>
+
+ <h2 id="thunderbird">Thunderbird</h2>
+
+ <p>Thunderbird is an email client</p>
+
+ <h2 id="seamonkey">Seamonkey</h2>
+
+ <p>Seamonkey is the all-in-one application.</p>
+
+</body>
+</html>
diff --git a/docshell/test/browser/file_bug503832.html b/docshell/test/browser/file_bug503832.html
new file mode 100644
index 0000000000..338631c8a0
--- /dev/null
+++ b/docshell/test/browser/file_bug503832.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test page for https://bugzilla.mozilla.org/show_bug.cgi?id=503832
+-->
+<head>
+ <title>Page Title for Bug 503832</title>
+</head>
+<body>
+ <h1>Fragment links</h1>
+
+ <p>This page has a bunch of fragment links to sections below:</p>
+
+ <ul>
+ <li><a id="firefox-link" href="#firefox">Firefox</a></li>
+ <li><a id="thunderbird-link" href="#thunderbird">Thunderbird</a></li>
+ <li><a id="seamonkey-link" href="#seamonkey">Seamonkey</a></li>
+ </ul>
+
+ <p>And here are the sections:</p>
+
+ <h2 id="firefox">Firefox</h2>
+
+ <p>Firefox is a browser.</p>
+
+ <h2 id="thunderbird">Thunderbird</h2>
+
+ <p>Thunderbird is an email client</p>
+
+ <h2 id="seamonkey">Seamonkey</h2>
+
+ <p>Seamonkey is the all-in-one application.</p>
+
+</body>
+</html>
diff --git a/docshell/test/browser/file_bug655270.html b/docshell/test/browser/file_bug655270.html
new file mode 100644
index 0000000000..0c08d982b1
--- /dev/null
+++ b/docshell/test/browser/file_bug655270.html
@@ -0,0 +1,11 @@
+<html>
+
+<head>
+ <link rel='icon' href='favicon_bug655270.ico'>
+</head>
+
+<body>
+Nothing to see here...
+</body>
+
+</html>
diff --git a/docshell/test/browser/file_bug670318.html b/docshell/test/browser/file_bug670318.html
new file mode 100644
index 0000000000..a78e8fcb19
--- /dev/null
+++ b/docshell/test/browser/file_bug670318.html
@@ -0,0 +1,23 @@
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<script>
+function load() {
+ function next() {
+ if (count < 5)
+ iframe.src = "data:text/html;charset=utf-8,iframe " + (++count);
+ }
+
+ var count = 0;
+ var iframe = document.createElement("iframe");
+ iframe.onload = function() { setTimeout(next, 0); };
+ document.body.appendChild(iframe);
+
+ setTimeout(next, 0);
+}
+</script>
+</head>
+
+<body onload="load()">
+Testcase
+</body>
+</html>
diff --git a/docshell/test/browser/file_bug673087-1-child.html b/docshell/test/browser/file_bug673087-1-child.html
new file mode 100644
index 0000000000..7bb1ad965b
--- /dev/null
+++ b/docshell/test/browser/file_bug673087-1-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset=windows-1252>
+<meta content="width=device-width, initial-scale=1" name="viewport">
+<title>windows-1252 in parent and child, actually EUC-JP</title>
+</head>
+<body>
+<p>Hiragana letter a if decoded as EUC-JP: </p>
+<p>ʸ¸Ǥ</p>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_bug673087-1.html b/docshell/test/browser/file_bug673087-1.html
new file mode 100644
index 0000000000..3dbea43d66
--- /dev/null
+++ b/docshell/test/browser/file_bug673087-1.html
Binary files differ
diff --git a/docshell/test/browser/file_bug673087-1.html^headers^ b/docshell/test/browser/file_bug673087-1.html^headers^
new file mode 100644
index 0000000000..2340a89c93
--- /dev/null
+++ b/docshell/test/browser/file_bug673087-1.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=windows-1252
diff --git a/docshell/test/browser/file_bug673087-2.html b/docshell/test/browser/file_bug673087-2.html
new file mode 100644
index 0000000000..ccbf896ca6
--- /dev/null
+++ b/docshell/test/browser/file_bug673087-2.html
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="ISO-2022-KR">
+FAIL \ No newline at end of file
diff --git a/docshell/test/browser/file_bug852909.pdf b/docshell/test/browser/file_bug852909.pdf
new file mode 100644
index 0000000000..89066463f1
--- /dev/null
+++ b/docshell/test/browser/file_bug852909.pdf
Binary files differ
diff --git a/docshell/test/browser/file_bug852909.png b/docshell/test/browser/file_bug852909.png
new file mode 100644
index 0000000000..c7510d388f
--- /dev/null
+++ b/docshell/test/browser/file_bug852909.png
Binary files differ
diff --git a/docshell/test/browser/file_click_link_within_view_source.html b/docshell/test/browser/file_click_link_within_view_source.html
new file mode 100644
index 0000000000..d78e4ba0ff
--- /dev/null
+++ b/docshell/test/browser/file_click_link_within_view_source.html
@@ -0,0 +1,6 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <a id="testlink" href="dummy_page.html">clickme</a>
+ </body>
+</html>
diff --git a/docshell/test/browser/file_cross_process_csp_inheritance.html b/docshell/test/browser/file_cross_process_csp_inheritance.html
new file mode 100644
index 0000000000..d87761a609
--- /dev/null
+++ b/docshell/test/browser/file_cross_process_csp_inheritance.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test CSP inheritance if load happens in same and different process</title>
+ <meta http-equiv="Content-Security-Policy" content="script-src 'none'">
+</head>
+<body>
+ <a href="data:text/html,<html>test-same-diff-process-csp-inhertiance</html>" id="testLink" target="_blank" rel="noopener">click to test same/diff process CSP inheritance</a>
+</body>
+</html>
diff --git a/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html
new file mode 100644
index 0000000000..49341f7481
--- /dev/null
+++ b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test Javascript URI with no script</title>
+</head>
+<body>
+<noscript>no scripts allowed here</noscript>
+<a href="javascript:alert(`origin=${origin} location=${location}`)" target="_parent">click me</a>
+</body>
+</html>
diff --git a/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^ b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^
new file mode 100644
index 0000000000..461f7f99ce
--- /dev/null
+++ b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-same-origin allow-top-navigation;
diff --git a/docshell/test/browser/file_csp_uir.html b/docshell/test/browser/file_csp_uir.html
new file mode 100644
index 0000000000..be60f41a80
--- /dev/null
+++ b/docshell/test/browser/file_csp_uir.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1542858 - Test CSP upgrade-insecure-requests</title>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+</head>
+<body>
+ <a id="testlink" href="file_csp_uir_dummy.html">testlink</a>
+</body>
+</html>
diff --git a/docshell/test/browser/file_csp_uir_dummy.html b/docshell/test/browser/file_csp_uir_dummy.html
new file mode 100644
index 0000000000..f0ab6775c0
--- /dev/null
+++ b/docshell/test/browser/file_csp_uir_dummy.html
@@ -0,0 +1 @@
+<html><body>foo</body></html>
diff --git a/docshell/test/browser/file_data_load_inherit_csp.html b/docshell/test/browser/file_data_load_inherit_csp.html
new file mode 100644
index 0000000000..1efe738e4c
--- /dev/null
+++ b/docshell/test/browser/file_data_load_inherit_csp.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1358009 - Inherit CSP into data URI</title>
+ <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'">
+</head>
+<body>
+ <a id="testlink">testlink</a>
+</body>
+</html>
diff --git a/docshell/test/browser/file_multiple_pushState.html b/docshell/test/browser/file_multiple_pushState.html
new file mode 100644
index 0000000000..6592f3f53f
--- /dev/null
+++ b/docshell/test/browser/file_multiple_pushState.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test multiple calls to history.pushState</title>
+ </head>
+ <body>
+ <h1>Ohai</h1>
+ </body>
+ <script type="text/javascript">
+ window.history.pushState({}, "", "/bar/ABC?key=baz");
+ let data = new Array(100000).join("a");
+ window.history.pushState({ data }, "", "/bar/ABC/DEF?key=baz");
+ // Test also Gecko specific state object size limit.
+ try {
+ let largeData = new Array(20000000).join("a");
+ window.history.pushState({ largeData }, "", "/bar/ABC/DEF/GHI?key=baz");
+ } catch (ex) {}
+ </script>
+</html>
diff --git a/docshell/test/browser/file_onbeforeunload_0.html b/docshell/test/browser/file_onbeforeunload_0.html
new file mode 100644
index 0000000000..7d9acf057d
--- /dev/null
+++ b/docshell/test/browser/file_onbeforeunload_0.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+ <iframe src="http://example.com/browser/docshell/test/browser/file_onbeforeunload_1.html"></iframe>
+</body>
+</html>
diff --git a/docshell/test/browser/file_onbeforeunload_1.html b/docshell/test/browser/file_onbeforeunload_1.html
new file mode 100644
index 0000000000..edd27783e4
--- /dev/null
+++ b/docshell/test/browser/file_onbeforeunload_1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+ <iframe src="http://mochi.test:8888/browser/docshell/test/browser/file_onbeforeunload_2.html"></iframe>
+</body>
+</html>
diff --git a/docshell/test/browser/file_onbeforeunload_2.html b/docshell/test/browser/file_onbeforeunload_2.html
new file mode 100644
index 0000000000..a52a4ace5c
--- /dev/null
+++ b/docshell/test/browser/file_onbeforeunload_2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+ <iframe src="http://example.com/browser/docshell/test/browser/file_onbeforeunload_3.html"></iframe>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_onbeforeunload_3.html b/docshell/test/browser/file_onbeforeunload_3.html
new file mode 100644
index 0000000000..9914f0cd85
--- /dev/null
+++ b/docshell/test/browser/file_onbeforeunload_3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+</body>
+</html>
+
diff --git a/docshell/test/browser/file_open_about_blank.html b/docshell/test/browser/file_open_about_blank.html
new file mode 100644
index 0000000000..134384e2f7
--- /dev/null
+++ b/docshell/test/browser/file_open_about_blank.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<button id="open" onclick="window.open('')">Open child window</button>
diff --git a/docshell/test/browser/file_slow_load.sjs b/docshell/test/browser/file_slow_load.sjs
new file mode 100644
index 0000000000..4c6dd6d5b9
--- /dev/null
+++ b/docshell/test/browser/file_slow_load.sjs
@@ -0,0 +1,8 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.processAsync();
+ response.setHeader("Content-Type", "text/html");
+ response.write("<!doctype html>Loading... ");
+ // We don't block on this, so it's fine to never finish the response.
+}
diff --git a/docshell/test/browser/head.js b/docshell/test/browser/head.js
new file mode 100644
index 0000000000..38f2528a2f
--- /dev/null
+++ b/docshell/test/browser/head.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Helper function for encoding override tests, loads URL, runs check1,
+ * forces encoding detection, runs check2.
+ */
+function runCharsetTest(url, check1, check2) {
+ waitForExplicitFinish();
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, url, true).then(afterOpen);
+
+ function afterOpen() {
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(
+ afterChangeCharset
+ );
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], check1).then(() => {
+ BrowserForceEncodingDetection();
+ });
+ }
+
+ function afterChangeCharset() {
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], check2).then(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }
+}
+
+/**
+ * Helper function for charset tests. It loads |url| in a new tab,
+ * runs |check|.
+ */
+function runCharsetCheck(url, check) {
+ waitForExplicitFinish();
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, url, true).then(afterOpen);
+
+ function afterOpen() {
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], check).then(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }
+}
+
+async function pushState(url, frameId) {
+ info(
+ `Doing a pushState, expecting to load ${url} ${
+ frameId ? "in an iframe" : ""
+ }`
+ );
+ let browser = gBrowser.selectedBrowser;
+ let bc = browser.browsingContext;
+ if (frameId) {
+ bc = await SpecialPowers.spawn(bc, [frameId], function (id) {
+ return content.document.getElementById(id).browsingContext;
+ });
+ }
+ let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ await SpecialPowers.spawn(bc, [url], function (url) {
+ content.history.pushState({}, "", url);
+ });
+ await loaded;
+ info(`Loaded ${url} ${frameId ? "in an iframe" : ""}`);
+}
+
+async function loadURI(url) {
+ info(`Doing a top-level loadURI, expecting to load ${url}`);
+ let browser = gBrowser.selectedBrowser;
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+ info(`Loaded ${url}`);
+}
+
+async function followLink(url, frameId) {
+ info(
+ `Creating and following a link to ${url} ${frameId ? "in an iframe" : ""}`
+ );
+ let browser = gBrowser.selectedBrowser;
+ let bc = browser.browsingContext;
+ if (frameId) {
+ bc = await SpecialPowers.spawn(bc, [frameId], function (id) {
+ return content.document.getElementById(id).browsingContext;
+ });
+ }
+ let loaded = BrowserTestUtils.browserLoaded(browser, !!frameId, url);
+ await SpecialPowers.spawn(bc, [url], function (url) {
+ let a = content.document.createElement("a");
+ a.href = url;
+ content.document.body.appendChild(a);
+ a.click();
+ });
+ await loaded;
+ info(`Loaded ${url} ${frameId ? "in an iframe" : ""}`);
+}
+
+async function goForward(url, useFrame = false) {
+ info(
+ `Clicking the forward button, expecting to load ${url} ${
+ useFrame ? "in an iframe" : ""
+ }`
+ );
+ let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ let forwardButton = document.getElementById("forward-button");
+ forwardButton.click();
+ await loaded;
+ info(`Loaded ${url} ${useFrame ? "in an iframe" : ""}`);
+}
+
+async function goBack(url, useFrame = false) {
+ info(
+ `Clicking the back button, expecting to load ${url} ${
+ useFrame ? "in an iframe" : ""
+ }`
+ );
+ let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ let backButton = document.getElementById("back-button");
+ backButton.click();
+ await loaded;
+ info(`Loaded ${url} ${useFrame ? "in an iframe" : ""}`);
+}
+
+function assertBackForwardState(canGoBack, canGoForward) {
+ let backButton = document.getElementById("back-button");
+ let forwardButton = document.getElementById("forward-button");
+
+ is(
+ backButton.disabled,
+ !canGoBack,
+ `${gBrowser.currentURI.spec}: back button is ${
+ canGoBack ? "not" : ""
+ } disabled`
+ );
+ is(
+ forwardButton.disabled,
+ !canGoForward,
+ `${gBrowser.currentURI.spec}: forward button is ${
+ canGoForward ? "not" : ""
+ } disabled`
+ );
+}
+
+class SHListener {
+ static NewEntry = 0;
+ static Reload = 1;
+ static GotoIndex = 2;
+ static Purge = 3;
+ static ReplaceEntry = 4;
+ static async waitForHistory(history, event) {
+ return new Promise(resolve => {
+ let listener = {
+ OnHistoryNewEntry: () => {},
+ OnHistoryReload: () => {
+ return true;
+ },
+ OnHistoryGotoIndex: () => {},
+ OnHistoryPurge: () => {},
+ OnHistoryReplaceEntry: () => {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ function finish() {
+ history.removeSHistoryListener(listener);
+ resolve();
+ }
+ switch (event) {
+ case this.NewEntry:
+ listener.OnHistoryNewEntry = finish;
+ break;
+ case this.Reload:
+ listener.OnHistoryReload = () => {
+ finish();
+ return true;
+ };
+ break;
+ case this.GotoIndex:
+ listener.OnHistoryGotoIndex = finish;
+ break;
+ case this.Purge:
+ listener.OnHistoryPurge = finish;
+ break;
+ case this.ReplaceEntry:
+ listener.OnHistoryReplaceEntry = finish;
+ break;
+ }
+
+ history.addSHistoryListener(listener);
+ });
+ }
+}
diff --git a/docshell/test/browser/head_browser_onbeforeunload.js b/docshell/test/browser/head_browser_onbeforeunload.js
new file mode 100644
index 0000000000..6bb334b793
--- /dev/null
+++ b/docshell/test/browser/head_browser_onbeforeunload.js
@@ -0,0 +1,271 @@
+"use strict";
+
+const BASE_URL = "http://mochi.test:8888/browser/docshell/test/browser/";
+
+const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html";
+
+const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+async function withTabModalPromptCount(expected, task) {
+ const DIALOG_TOPIC = CONTENT_PROMPT_SUBDIALOG
+ ? "common-dialog-loaded"
+ : "tabmodal-dialog-loaded";
+
+ let count = 0;
+ function observer() {
+ count++;
+ }
+
+ Services.obs.addObserver(observer, DIALOG_TOPIC);
+ try {
+ return await task();
+ } finally {
+ Services.obs.removeObserver(observer, DIALOG_TOPIC);
+ is(count, expected, "Should see expected number of tab modal prompts");
+ }
+}
+
+function promiseAllowUnloadPrompt(browser, allowNavigation) {
+ return PromptTestUtils.handleNextPrompt(
+ browser,
+ { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
+ { buttonNumClick: allowNavigation ? 0 : 1 }
+ );
+}
+
+// Maintain a pool of background tabs with our test document loaded so
+// we don't have to wait for a load prior to each test step (potentially
+// tearing down and recreating content processes in the process).
+const TabPool = {
+ poolSize: 5,
+
+ pendingCount: 0,
+
+ readyTabs: [],
+
+ readyPromise: null,
+ resolveReadyPromise: null,
+
+ spawnTabs() {
+ while (this.pendingCount + this.readyTabs.length < this.poolSize) {
+ this.pendingCount++;
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ this.readyTabs.push(tab);
+ this.pendingCount--;
+
+ if (this.resolveReadyPromise) {
+ this.readyPromise = null;
+ this.resolveReadyPromise();
+ this.resolveReadyPromise = null;
+ }
+
+ this.spawnTabs();
+ });
+ }
+ },
+
+ getReadyPromise() {
+ if (!this.readyPromise) {
+ this.readyPromise = new Promise(resolve => {
+ this.resolveReadyPromise = resolve;
+ });
+ }
+ return this.readyPromise;
+ },
+
+ async getTab() {
+ while (!this.readyTabs.length) {
+ this.spawnTabs();
+ await this.getReadyPromise();
+ }
+
+ let tab = this.readyTabs.shift();
+ this.spawnTabs();
+
+ gBrowser.selectedTab = tab;
+ return tab;
+ },
+
+ async cleanup() {
+ this.poolSize = 0;
+
+ while (this.pendingCount) {
+ await this.getReadyPromise();
+ }
+
+ while (this.readyTabs.length) {
+ await BrowserTestUtils.removeTab(this.readyTabs.shift());
+ }
+ },
+};
+
+const ACTIONS = {
+ NONE: 0,
+ LISTEN_AND_ALLOW: 1,
+ LISTEN_AND_BLOCK: 2,
+};
+
+const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k]));
+
+function* generatePermutations(depth) {
+ if (depth == 0) {
+ yield [];
+ return;
+ }
+ for (let subActions of generatePermutations(depth - 1)) {
+ for (let action of Object.values(ACTIONS)) {
+ yield [action, ...subActions];
+ }
+ }
+}
+
+const PERMUTATIONS = Array.from(generatePermutations(4));
+
+const FRAMES = [
+ { process: 0 },
+ { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
+ { process: 0 },
+ { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
+];
+
+function addListener(bc, block) {
+ return SpecialPowers.spawn(bc, [block], block => {
+ return new Promise(resolve => {
+ function onbeforeunload(event) {
+ if (block) {
+ event.preventDefault();
+ }
+ resolve({ event: "beforeunload" });
+ }
+ content.addEventListener("beforeunload", onbeforeunload, { once: true });
+ content.unlisten = () => {
+ content.removeEventListener("beforeunload", onbeforeunload);
+ };
+
+ content.addEventListener(
+ "unload",
+ () => {
+ resolve({ event: "unload" });
+ },
+ { once: true }
+ );
+ });
+ });
+}
+
+function descendants(bc) {
+ if (bc) {
+ return [bc, ...descendants(bc.children[0])];
+ }
+ return [];
+}
+
+async function addListeners(frames, actions, startIdx) {
+ let process = startIdx >= 0 ? FRAMES[startIdx].process : -1;
+
+ let roundTripPromises = [];
+
+ let expectNestedEventLoop = false;
+ let numBlockers = 0;
+ let unloadPromises = [];
+ let beforeUnloadPromises = [];
+
+ for (let [i, frame] of frames.entries()) {
+ let action = actions[i];
+ if (action === ACTIONS.NONE) {
+ continue;
+ }
+
+ let block = action === ACTIONS.LISTEN_AND_BLOCK;
+ let promise = addListener(frame, block);
+ if (startIdx <= i) {
+ if (block || FRAMES[i].process !== process) {
+ expectNestedEventLoop = true;
+ }
+ beforeUnloadPromises.push(promise);
+ numBlockers += block;
+ } else {
+ unloadPromises.push(promise);
+ }
+
+ roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {}));
+ }
+
+ // Wait for round trip messages to any processes with event listeners to
+ // return so we're sure that all listeners are registered and their state
+ // flags are propagated before we continue.
+ await Promise.all(roundTripPromises);
+
+ return {
+ expectNestedEventLoop,
+ expectPrompt: !!numBlockers,
+ unloadPromises,
+ beforeUnloadPromises,
+ };
+}
+
+async function doTest(actions, startIdx, navigate) {
+ let tab = await TabPool.getTab();
+ let browser = tab.linkedBrowser;
+
+ let frames = descendants(browser.browsingContext);
+ let expected = await addListeners(frames, actions, startIdx);
+
+ let awaitingPrompt = false;
+ let promptPromise;
+ if (expected.expectPrompt) {
+ awaitingPrompt = true;
+ promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => {
+ awaitingPrompt = false;
+ });
+ }
+
+ let promptCount = expected.expectPrompt ? 1 : 0;
+ await withTabModalPromptCount(promptCount, async () => {
+ await navigate(tab, frames).then(result => {
+ ok(
+ !awaitingPrompt,
+ "Navigation should not complete while we're still expecting a prompt"
+ );
+
+ is(
+ result.eventLoopSpun,
+ expected.expectNestedEventLoop,
+ "Should have nested event loop?"
+ );
+ });
+
+ for (let result of await Promise.all(expected.beforeUnloadPromises)) {
+ is(
+ result.event,
+ "beforeunload",
+ "Should have seen beforeunload event before unload"
+ );
+ }
+ await promptPromise;
+
+ await Promise.all(
+ frames.map(frame =>
+ SpecialPowers.spawn(frame, [], () => {
+ if (content.unlisten) {
+ content.unlisten();
+ }
+ }).catch(() => {})
+ )
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+ });
+
+ for (let result of await Promise.all(expected.unloadPromises)) {
+ is(result.event, "unload", "Should have seen unload event");
+ }
+}
diff --git a/docshell/test/browser/onload_message.html b/docshell/test/browser/onload_message.html
new file mode 100644
index 0000000000..23f6e37396
--- /dev/null
+++ b/docshell/test/browser/onload_message.html
@@ -0,0 +1,25 @@
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script>
+ addEventListener("load", function() {
+ // This file is used in couple of different tests.
+ if (opener) {
+ opener.postMessage("load", "*");
+ } else {
+ var bc = new BroadcastChannel("browser_browsingContext");
+ bc.onmessage = function(event) {
+ if (event.data == "back") {
+ bc.close();
+ history.back();
+ }
+ };
+ bc.postMessage({event: "load", framesLength: frames.length });
+ }
+ });
+ </script>
+ </head>
+ <body>
+ This file posts a message containing "load" to opener or BroadcastChannel on load completion.
+ </body>
+</html>
diff --git a/docshell/test/browser/onpageshow_message.html b/docshell/test/browser/onpageshow_message.html
new file mode 100644
index 0000000000..105c06f8db
--- /dev/null
+++ b/docshell/test/browser/onpageshow_message.html
@@ -0,0 +1,41 @@
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script>
+ addEventListener("pageshow", function() {
+ var bc = new BroadcastChannel("browser_browsingContext");
+ function frameData() {
+ var win = SpecialPowers.wrap(frames[0]);
+ bc.postMessage(
+ { framesLength: frames.length,
+ browsingContextId: win.docShell.browsingContext.id,
+ outerWindowId: win.docShell.outerWindowID
+ });
+ }
+
+ bc.onmessage = function(event) {
+ if (event.data == "createiframe") {
+ let frame = document.createElement("iframe");
+ frame.src = "dummy_page.html";
+ document.body.appendChild(frame);
+ frame.onload = frameData;
+ } else if (event.data == "nextpage") {
+ bc.close();
+ location.href = "onload_message.html";
+ } else if (event.data == "queryframes") {
+ frameData();
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ }
+ bc.postMessage("pageshow");
+ });
+ </script>
+ </head>
+ <body>
+ This file posts a message containing "pageshow" to a BroadcastChannel and
+ keep the channel open for commands.
+ </body>
+</html>
diff --git a/docshell/test/browser/overlink_test.html b/docshell/test/browser/overlink_test.html
new file mode 100644
index 0000000000..5efd689311
--- /dev/null
+++ b/docshell/test/browser/overlink_test.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <head> <meta charset="utf-8"> </head>
+ <body>
+ <a id="link" href="https://user:password@example.com">Link</a>
+ </body>
+</html>
diff --git a/docshell/test/browser/print_postdata.sjs b/docshell/test/browser/print_postdata.sjs
new file mode 100644
index 0000000000..0e3ef38419
--- /dev/null
+++ b/docshell/test/browser/print_postdata.sjs
@@ -0,0 +1,25 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/docshell/test/browser/redirect_to_example.sjs b/docshell/test/browser/redirect_to_example.sjs
new file mode 100644
index 0000000000..283e4793db
--- /dev/null
+++ b/docshell/test/browser/redirect_to_example.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 302, "Moved Permanently");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ response.setHeader("Location", "http://example");
+}
diff --git a/docshell/test/browser/test-form_sjis.html b/docshell/test/browser/test-form_sjis.html
new file mode 100644
index 0000000000..91c375deef
--- /dev/null
+++ b/docshell/test/browser/test-form_sjis.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/REC-html401-19991224/strict.dtd">
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=windows-1251">
+ <title>Shift_JIS in body and text area</title>
+ </head>
+ <body>
+ <h1>Incorrect meta charset</h1>
+ <h2>This page is encoded in Shift_JIS, but has an incorrect meta charset
+ claiming that it is windows-1251</h2>
+ <p id="testpar">jR[h́AׂĂ̕ɌŗL̔ԍt^܂</p>
+ <form>
+ <p>
+ <textarea id="testtextarea" rows=6 cols=60>jR[h́AׂĂ̕ɌŗL̔ԍt^܂</textarea>
+ <input id="testinput" type="text" size=60 value="jR[h́AׂĂ̕ɌŗL̔ԍt^܂">
+ </p>
+ </form>
+ <h2>Expected text on load:</h2>
+ <p>&#x453;&#x2020;&#x453;&#x6A;&#x453;&#x52;&#x403;&#x5B;&#x453;&#x68;&#x201A;&#x41D;&#x403;&#x41;&#x201A;&#xB7;&#x201A;&#x427;&#x201A;&#x414;&#x201A;&#x41C;&#x2022;&#xB6;&#x40B;&#x459;&#x201A;&#x419;&#x40A;&#x415;&#x2014;&#x4C;&#x201A;&#x41C;&#x201D;&#x424;&#x40C;&#x2020;&#x201A;&#x440;&#x2022;&#x74;&#x2014;&#x5E;&#x201A;&#xB5;&#x201A;&#x42C;&#x201A;&#xB7;</p>
+ <h2>Expected text on resetting the encoding to Shift_JIS:</h2>
+ <p>&#x30E6;&#x30CB;&#x30B3;&#x30FC;&#x30C9;&#x306F;&#x3001;&#x3059;&#x3079;&#x3066;&#x306E;&#x6587;&#x5B57;&#x306B;&#x56FA;&#x6709;&#x306E;&#x756A;&#x53F7;&#x3092;&#x4ED8;&#x4E0E;&#x3057;&#x307E;&#x3059;</p>
+ </body>
+</html>
diff --git a/docshell/test/chrome/112564_nocache.html b/docshell/test/chrome/112564_nocache.html
new file mode 100644
index 0000000000..29fb990b86
--- /dev/null
+++ b/docshell/test/chrome/112564_nocache.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<title>test1</title>
+</head>
+<body>
+<p>
+This document will be sent with a no-cache cache-control header. When sent over a secure connection, it should not be stored in bfcache.
+</p>
+</body>
+</html>
diff --git a/docshell/test/chrome/112564_nocache.html^headers^ b/docshell/test/chrome/112564_nocache.html^headers^
new file mode 100644
index 0000000000..c829a41ae9
--- /dev/null
+++ b/docshell/test/chrome/112564_nocache.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-cache
diff --git a/docshell/test/chrome/215405_nocache.html b/docshell/test/chrome/215405_nocache.html
new file mode 100644
index 0000000000..c7d48c4eba
--- /dev/null
+++ b/docshell/test/chrome/215405_nocache.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html style="height: 100%">
+<head>
+ <title>test1</title>
+</head>
+<body style="height: 100%">
+ <input type="text" id="inp" value="">
+ </input>
+ <div style="height: 50%">Some text</div>
+ <div style="height: 50%">Some text</div>
+ <div style="height: 50%">Some text</div>
+ <div style="height: 50%; width: 300%">Some more text</div>
+</body>
+</html>
diff --git a/docshell/test/chrome/215405_nocache.html^headers^ b/docshell/test/chrome/215405_nocache.html^headers^
new file mode 100644
index 0000000000..c829a41ae9
--- /dev/null
+++ b/docshell/test/chrome/215405_nocache.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-cache
diff --git a/docshell/test/chrome/215405_nostore.html b/docshell/test/chrome/215405_nostore.html
new file mode 100644
index 0000000000..4f5bd0f4f0
--- /dev/null
+++ b/docshell/test/chrome/215405_nostore.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html style="height: 100%">
+<head>
+ <title>test1</title>
+</head>
+<body style="height: 100%">
+ <input type="text" id="inp" value="">
+ </input>
+ <div style="height: 50%">Some text</div>
+ <div style="height: 50%">Some text</div>
+ <div style="height: 50%">Some text</div>
+ <div style="height: 50%; width: 350%">Some more text</div>
+</body>
+</html>
diff --git a/docshell/test/chrome/215405_nostore.html^headers^ b/docshell/test/chrome/215405_nostore.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/chrome/215405_nostore.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/chrome/582176_dummy.html b/docshell/test/chrome/582176_dummy.html
new file mode 100644
index 0000000000..3b18e512db
--- /dev/null
+++ b/docshell/test/chrome/582176_dummy.html
@@ -0,0 +1 @@
+hello world
diff --git a/docshell/test/chrome/582176_xml.xml b/docshell/test/chrome/582176_xml.xml
new file mode 100644
index 0000000000..d3dd576dfe
--- /dev/null
+++ b/docshell/test/chrome/582176_xml.xml
@@ -0,0 +1,2 @@
+<?xml-stylesheet type="text/xsl" href="582176_xslt.xsl"?>
+<out/>
diff --git a/docshell/test/chrome/582176_xslt.xsl b/docshell/test/chrome/582176_xslt.xsl
new file mode 100644
index 0000000000..5957416899
--- /dev/null
+++ b/docshell/test/chrome/582176_xslt.xsl
@@ -0,0 +1,8 @@
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="out">
+ <html>
+ <head><title>XSLT result doc</title></head>
+ <body><p>xslt result</p></body>
+ </html>
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/docshell/test/chrome/662200a.html b/docshell/test/chrome/662200a.html
new file mode 100644
index 0000000000..0b9ead6f3e
--- /dev/null
+++ b/docshell/test/chrome/662200a.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>A</title>
+ </head>
+ <body>
+ <a id="link" href="662200b.html">Next</a>
+ </body>
+</html>
diff --git a/docshell/test/chrome/662200b.html b/docshell/test/chrome/662200b.html
new file mode 100644
index 0000000000..91e6b971d6
--- /dev/null
+++ b/docshell/test/chrome/662200b.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>B</title>
+ </head>
+ <body>
+ <a id="link" href="662200c.html">Next</a>
+ </body>
+</html>
diff --git a/docshell/test/chrome/662200c.html b/docshell/test/chrome/662200c.html
new file mode 100644
index 0000000000..bc00e6b14b
--- /dev/null
+++ b/docshell/test/chrome/662200c.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ <title>C</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/chrome/89419.html b/docshell/test/chrome/89419.html
new file mode 100644
index 0000000000..b36b8d788c
--- /dev/null
+++ b/docshell/test/chrome/89419.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+<title>Bug 89419</title>
+</head>
+<body>
+<img src="http://mochi.test:8888/tests/docshell/test/chrome/bug89419.sjs">
+</body>
diff --git a/docshell/test/chrome/92598_nostore.html b/docshell/test/chrome/92598_nostore.html
new file mode 100644
index 0000000000..47bb90441e
--- /dev/null
+++ b/docshell/test/chrome/92598_nostore.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<title>test1</title>
+</head>
+<body>
+<p>
+This document will be sent with a no-store cache-control header. It should not be stored in bfcache.
+</p>
+</body>
+</html>
diff --git a/docshell/test/chrome/92598_nostore.html^headers^ b/docshell/test/chrome/92598_nostore.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/chrome/92598_nostore.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/chrome/DocShellHelpers.sys.mjs b/docshell/test/chrome/DocShellHelpers.sys.mjs
new file mode 100644
index 0000000000..5f4eee8724
--- /dev/null
+++ b/docshell/test/chrome/DocShellHelpers.sys.mjs
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+export class DocShellHelpersParent extends JSWindowActorParent {
+ static eventListener;
+
+ // These static variables should be set when registering the actor
+ // (currently doPageNavigation in docshell_helpers.js).
+ static eventsToListenFor;
+ static observers;
+
+ constructor() {
+ super();
+ }
+ receiveMessage({ name, data }) {
+ if (name == "docshell_helpers:event") {
+ let { event, originalTargetIsHTMLDocument } = data;
+
+ if (this.constructor.eventsToListenFor.includes(event.type)) {
+ this.constructor.eventListener(event, originalTargetIsHTMLDocument);
+ }
+ } else if (name == "docshell_helpers:observe") {
+ let { topic } = data;
+
+ this.constructor.observers.get(topic).call();
+ }
+ }
+}
+
+export class DocShellHelpersChild extends JSWindowActorChild {
+ constructor() {
+ super();
+ }
+ receiveMessage({ name, data }) {
+ if (name == "docshell_helpers:preventBFCache") {
+ // Add an RTCPeerConnection to prevent the page from being bfcached.
+ let win = this.contentWindow;
+ win.blockBFCache = new win.RTCPeerConnection();
+ }
+ }
+ handleEvent(event) {
+ if (
+ Document.isInstance(event.originalTarget) &&
+ event.originalTarget.isInitialDocument
+ ) {
+ dump(`TEST: ignoring a ${event.type} event for an initial about:blank\n`);
+ return;
+ }
+
+ this.sendAsyncMessage("docshell_helpers:event", {
+ event: {
+ type: event.type,
+ persisted: event.persisted,
+ originalTarget: {
+ title: event.originalTarget.title,
+ location: event.originalTarget.location.href,
+ visibilityState: event.originalTarget.visibilityState,
+ hidden: event.originalTarget.hidden,
+ },
+ },
+ originalTargetIsHTMLDocument: HTMLDocument.isInstance(
+ event.originalTarget
+ ),
+ });
+ }
+ observe(subject, topic) {
+ if (Window.isInstance(subject) && subject.document.isInitialDocument) {
+ dump(`TEST: ignoring a topic notification for an initial about:blank\n`);
+ return;
+ }
+
+ this.sendAsyncMessage("docshell_helpers:observe", { topic });
+ }
+}
diff --git a/docshell/test/chrome/allowContentRetargeting.sjs b/docshell/test/chrome/allowContentRetargeting.sjs
new file mode 100644
index 0000000000..96e467ef68
--- /dev/null
+++ b/docshell/test/chrome/allowContentRetargeting.sjs
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ resp.setHeader("Content-Type", "application/octet-stream", false);
+ resp.write("hi");
+}
diff --git a/docshell/test/chrome/blue.png b/docshell/test/chrome/blue.png
new file mode 100644
index 0000000000..8df58f3a5f
--- /dev/null
+++ b/docshell/test/chrome/blue.png
Binary files differ
diff --git a/docshell/test/chrome/bug112564_window.xhtml b/docshell/test/chrome/bug112564_window.xhtml
new file mode 100644
index 0000000000..04c25763b3
--- /dev/null
+++ b/docshell/test/chrome/bug112564_window.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="112564Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="onLoad();"
+ title="112564 test">
+
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ var gTestsIterator;
+
+ function onLoad() {
+ gTestsIterator = testsIterator();
+ nextTest();
+ }
+
+ function nextTest() {
+ gTestsIterator.next();
+ }
+
+ function* testsIterator() {
+ // Load a secure page with a no-cache header, followed by a simple page.
+ // no-cache should not interfere with the bfcache in the way no-store
+ // does.
+ var test1DocURI = "https://example.com:443/chrome/docshell/test/chrome/112564_nocache.html";
+
+ doPageNavigation({
+ uri: test1DocURI,
+ eventsToListenFor: ["load", "pageshow"],
+ expectedEvents: [ { type: "load",
+ title: "test1" },
+ { type: "pageshow",
+ title: "test1",
+ persisted: false } ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ var test2DocURI = "data:text/html,<html><head><title>test2</title></head>" +
+ "<body>test2</body></html>";
+
+ doPageNavigation({
+ uri: test2DocURI,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "test1",
+ persisted: true },
+ { type: "load",
+ title: "test2" },
+ { type: "pageshow",
+ title: "test2",
+ persisted: false } ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ // Now go back in history. First page has been cached.
+ // Check persisted property to confirm
+ doPageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "test2",
+ persisted: true },
+ { type: "pageshow",
+ title: "test1",
+ persisted: true } ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ finish();
+ }
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug113934_window.xhtml b/docshell/test/chrome/bug113934_window.xhtml
new file mode 100644
index 0000000000..c5b676b6b8
--- /dev/null
+++ b/docshell/test/chrome/bug113934_window.xhtml
@@ -0,0 +1,156 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 113934" onload="doTheTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <hbox>
+ <vbox id="box1">
+ </vbox>
+ <vbox id="box2">
+ </vbox>
+ <spacer flex="1"/>
+ </hbox>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ /* globals SimpleTest, is, isnot, ok, snapshotWindow, compareSnapshots,
+ onerror */
+ var imports = [ "SimpleTest", "is", "isnot", "ok", "snapshotWindow",
+ "compareSnapshots", "onerror" ];
+ for (var name of imports) {
+ window[name] = window.arguments[0][name];
+ }
+
+ function $(id) {
+ return document.getElementById(id);
+ }
+
+ function addBrowser(parent, id, width, height) {
+ var b =
+ document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "browser");
+ var type = window.location.search.slice(1);
+ is(type == "chrome" || type == "content", true, "Unexpected type");
+ b.setAttribute("type", type);
+ b.setAttribute("id", id);
+ b.style.width = width + "px";
+ b.style.height = height + "px";
+ $(parent).appendChild(b);
+ }
+ addBrowser("box1", "f1", 300, 200);
+ addBrowser("box1", "f2", 300, 200);
+ addBrowser("box2", "f3", 30, 200);
+
+ /** Test for Bug 113934 */
+ var doc1 =
+ "data:text/html,<html><body onbeforeunload='document.documentElement.textContent = \"\"' onunload='document.documentElement.textContent = \"\"' onpagehide='document.documentElement.textContent = \"\"'>This is a test</body></html>";
+ var doc2 = "data:text/html,<html><head></head><body>This is a second test</body></html>";
+
+
+ $("f1").setAttribute("src", doc1);
+ $("f2").setAttribute("src", doc2);
+ $("f3").setAttribute("src", doc2);
+
+ async function doTheTest() {
+ var s2 = await snapshotWindow($("f2").contentWindow);
+ // var s3 = await snapshotWindow($("f3").contentWindow);
+
+ // This test is broken - see bug 1090274
+ //ok(!compareSnapshots(s2, s3, true)[0],
+ // "Should look different due to different sizing");
+
+ function getDOM(id) {
+ return $(id).contentDocument.documentElement.innerHTML;
+ }
+
+ var dom1 = getDOM("f1");
+
+ var dom2 = getDOM("f2");
+ $("f2").contentDocument.body.textContent = "Modified the text";
+ var dom2star = getDOM("f2");
+ isnot(dom2, dom2star, "We changed the DOM!");
+
+ $("f1").swapDocShells($("f2"));
+ // now we have doms 2*, 1, 2 in the frames
+
+ is(getDOM("f1"), dom2star, "Shouldn't have changed the DOM on swap");
+ is(getDOM("f2"), dom1, "Shouldn't have fired event handlers");
+
+ // Test for bug 480149
+ // The DOMLink* events are dispatched asynchronously, thus I cannot
+ // just include the <link> element in the initial DOM and swap the
+ // docshells. Instead, the link element is added now. Then, when the
+ // first DOMLinkAdded event (which is a result of the actual addition)
+ // is dispatched, the docshells are swapped and the pageshow and pagehide
+ // events are tested. Only then, we wait for the DOMLink* events,
+ // which are a result of swapping the docshells.
+ var DOMLinkListener = {
+ _afterFirst: false,
+ _addedDispatched: false,
+ async handleEvent(aEvent) {
+ if (!this._afterFirst) {
+ is(aEvent.type, "DOMLinkAdded");
+
+ var strs = { "f1": "", "f3" : "" };
+ function attachListener(node, type) {
+ var listener = function(e) {
+ if (strs[node.id]) strs[node.id] += " ";
+ strs[node.id] += node.id + ".page" + type;
+ }
+ node.addEventListener("page" + type, listener);
+
+ listener.detach = function() {
+ node.removeEventListener("page" + type, listener);
+ }
+ return listener;
+ }
+
+ var l1 = attachListener($("f1"), "show");
+ var l2 = attachListener($("f1"), "hide");
+ var l3 = attachListener($("f3"), "show");
+ var l4 = attachListener($("f3"), "hide");
+
+ $("f1").swapDocShells($("f3"));
+ // now we have DOMs 2, 1, 2* in the frames
+
+ l1.detach();
+ l2.detach();
+ l3.detach();
+ l4.detach();
+
+ // swapDocShells reflows asynchronously, ensure layout is
+ // clean so that the viewport of f1 is the right size.
+ $("f1").getBoundingClientRect();
+ var s1_new = await snapshotWindow($("f1").contentWindow);
+ var [same, first, second] = compareSnapshots(s1_new, s2, true);
+ ok(same, "Should reflow on swap. Expected " + second + " but got " + first);
+
+ is(strs.f1, "f1.pagehide f1.pageshow");
+ is(strs.f3, "f3.pagehide f3.pageshow");
+ this._afterFirst = true;
+ return;
+ }
+ if (aEvent.type == "DOMLinkAdded") {
+ is(this._addedDispatched, false);
+ this._addedDispatched = true;
+ }
+
+ if (this._addedDispatched) {
+ $("f1").removeEventListener("DOMLinkAdded", this);
+ $("f3").removeEventListener("DOMLinkAdded", this);
+ window.close();
+ SimpleTest.finish();
+ }
+ }
+ };
+
+ $("f1").addEventListener("DOMLinkAdded", DOMLinkListener);
+ $("f3").addEventListener("DOMLinkAdded", DOMLinkListener);
+
+ var linkElement = $("f1").contentDocument.createElement("link");
+ linkElement.setAttribute("rel", "alternate");
+ linkElement.setAttribute("href", "about:blank");
+ $("f1").contentDocument.documentElement.firstChild.appendChild(linkElement);
+ }
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/bug215405_window.xhtml b/docshell/test/chrome/bug215405_window.xhtml
new file mode 100644
index 0000000000..ddbe2630b9
--- /dev/null
+++ b/docshell/test/chrome/bug215405_window.xhtml
@@ -0,0 +1,179 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="215405Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="onLoad();"
+ title="215405 test">
+
+ <script type="application/javascript"><![CDATA[
+ const {BrowserTestUtils} = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+ );
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ /* globals SimpleTest, is, isnot, ok */
+ var imports = [ "SimpleTest", "is", "isnot", "ok"];
+ for (var name of imports) {
+ window[name] = window.arguments[0][name];
+ }
+
+ const text="MOZILLA";
+ const nostoreURI = "http://mochi.test:8888/tests/docshell/test/chrome/" +
+ "215405_nostore.html";
+ const nocacheURI = "https://example.com:443/tests/docshell/test/chrome/" +
+ "215405_nocache.html";
+
+ var gBrowser;
+ var gTestsIterator;
+ var currScrollX = 0;
+ var currScrollY = 0;
+
+ function finish() {
+ gBrowser.removeEventListener("pageshow", eventListener, true);
+ // Work around bug 467960
+ let historyPurged;
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let history = gBrowser.browsingContext.sessionHistory;
+ history.purgeHistory(history.count);
+ historyPurged = Promise.resolve();
+ } else {
+ historyPurged = SpecialPowers.spawn(gBrowser, [], () => {
+ let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory
+ .legacySHistory;
+ history.purgeHistory(history.count);
+ });
+ }
+
+ historyPurged.then(_ => {
+ window.close();
+ window.arguments[0].SimpleTest.finish();
+ });
+ }
+
+ function onLoad(e) {
+ gBrowser = document.getElementById("content");
+ gBrowser.addEventListener("pageshow", eventListener, true);
+
+ gTestsIterator = testsIterator();
+ nextTest();
+ }
+
+ function eventListener(event) {
+ setTimeout(nextTest, 0);
+ }
+
+ function nextTest() {
+ gTestsIterator.next();
+ }
+
+ function* testsIterator() {
+ // No-store tests
+ var testName = "[nostore]";
+
+ // Load a page with a no-store header
+ BrowserTestUtils.startLoadingURIString(gBrowser, nostoreURI);
+ yield undefined;
+
+
+ // Now that the page has loaded, amend the form contents
+ var form = gBrowser.contentDocument.getElementById("inp");
+ form.value = text;
+
+ // Attempt to scroll the page
+ var originalXPosition = gBrowser.contentWindow.scrollX;
+ var originalYPosition = gBrowser.contentWindow.scrollY;
+ var scrollToX = gBrowser.contentWindow.scrollMaxX;
+ var scrollToY = gBrowser.contentWindow.scrollMaxY;
+ gBrowser.contentWindow.scrollBy(scrollToX, scrollToY);
+
+ // Save the scroll position for future comparison
+ currScrollX = gBrowser.contentWindow.scrollX;
+ currScrollY = gBrowser.contentWindow.scrollY;
+ isnot(currScrollX, originalXPosition,
+ testName + " failed to scroll window horizontally");
+ isnot(currScrollY, originalYPosition,
+ testName + " failed to scroll window vertically");
+
+ // Load a new document into the browser
+ var simple = "data:text/html,<html><head><title>test2</title></head>" +
+ "<body>test2</body></html>";
+ BrowserTestUtils.startLoadingURIString(gBrowser, simple);
+ yield undefined;
+
+
+ // Now go back in history. First page should not have been cached.
+ gBrowser.goBack();
+ yield undefined;
+
+
+ // First uncacheable page will now be reloaded. Check scroll position
+ // restored, and form contents not
+ is(gBrowser.contentWindow.scrollX, currScrollX, testName +
+ " horizontal axis scroll position not correctly restored");
+ is(gBrowser.contentWindow.scrollY, currScrollY, testName +
+ " vertical axis scroll position not correctly restored");
+ var formValue = gBrowser.contentDocument.getElementById("inp").value;
+ isnot(formValue, text, testName + " form value incorrectly restored");
+
+
+ // https no-cache
+ testName = "[nocache]";
+
+ // Load a page with a no-cache header. This should not be
+ // restricted like no-store (bug 567365)
+ BrowserTestUtils.startLoadingURIString(gBrowser, nocacheURI);
+ yield undefined;
+
+
+ // Now that the page has loaded, amend the form contents
+ form = gBrowser.contentDocument.getElementById("inp");
+ form.value = text;
+
+ // Attempt to scroll the page
+ originalXPosition = gBrowser.contentWindow.scrollX;
+ originalYPosition = gBrowser.contentWindow.scrollY;
+ scrollToX = gBrowser.contentWindow.scrollMaxX;
+ scrollToY = gBrowser.contentWindow.scrollMaxY;
+ gBrowser.contentWindow.scrollBy(scrollToX, scrollToY);
+
+ // Save the scroll position for future comparison
+ currScrollX = gBrowser.contentWindow.scrollX;
+ currScrollY = gBrowser.contentWindow.scrollY;
+ isnot(currScrollX, originalXPosition,
+ testName + " failed to scroll window horizontally");
+ isnot(currScrollY, originalYPosition,
+ testName + " failed to scroll window vertically");
+
+ BrowserTestUtils.startLoadingURIString(gBrowser, simple);
+ yield undefined;
+
+
+ // Now go back in history to the cached page.
+ gBrowser.goBack();
+ yield undefined;
+
+
+ // First page will now be reloaded. Check scroll position
+ // and form contents are restored
+ is(gBrowser.contentWindow.scrollX, currScrollX, testName +
+ " horizontal axis scroll position not correctly restored");
+ is(gBrowser.contentWindow.scrollY, currScrollY, testName +
+ " vertical axis scroll position not correctly restored");
+ formValue = gBrowser.contentDocument.getElementById("inp").value;
+ is(formValue, text, testName + " form value not correctly restored");
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ finish();
+ }
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/docshell/test/chrome/bug293235.html b/docshell/test/chrome/bug293235.html
new file mode 100644
index 0000000000..458f88431c
--- /dev/null
+++ b/docshell/test/chrome/bug293235.html
@@ -0,0 +1,13 @@
+<html>
+ <head>
+ <title>Bug 293235 page1</title>
+ <style type="text/css">
+ a:visited, a.forcevisited.forcevisited { color: rgb(128, 0, 128); }
+ a:link, a.forcelink.forcelink { color: rgb(0, 0, 128); }
+ a:focus { color: rgb(128, 0, 0); }
+ </style>
+ </head>
+ <body>
+ <a id="link1" href="bug293235_p2.html">This is a test link.</a>
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug293235_p2.html b/docshell/test/chrome/bug293235_p2.html
new file mode 100644
index 0000000000..2de067b80e
--- /dev/null
+++ b/docshell/test/chrome/bug293235_p2.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Bug 293235 page2</title>
+ </head>
+ <body>
+ Nothing to see here, move along.
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug293235_window.xhtml b/docshell/test/chrome/bug293235_window.xhtml
new file mode 100644
index 0000000000..8bb050cebb
--- /dev/null
+++ b/docshell/test/chrome/bug293235_window.xhtml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="293235Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTests, 0);"
+ title="bug 293235 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+
+ <script type="application/javascript"><![CDATA[
+ var {NetUtil} = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+ );
+
+ // Return the Element object for the specified element id
+ function $(id) { return TestWindow.getDocument().getElementById(id); }
+
+ ////
+ // Generator function for test steps for bug 293235:
+ // A visited link should have the :visited style applied
+ // to it when displayed on a page which was fetched from
+ // the bfcache.
+ //
+ async function runTests() {
+ // Register our observer to know when the link lookup is complete.
+ let testURI = NetUtil.newURI(getHttpUrl("bug293235_p2.html"));
+ let os = SpecialPowers.Services.obs;
+ // Load a test page containing a link that should be initially
+ // blue, per the :link style.
+ await new Promise(resolve => {
+ doPageNavigation({
+ uri: getHttpUrl("bug293235.html"),
+ onNavComplete: resolve,
+ });
+ });
+
+ // Now that we've been notified, we can check our link color.
+ // Since we can't use getComputedStyle() for this because
+ // getComputedStyle lies about styles that result from :visited,
+ // we have to take snapshots.
+ // First, take two reference snapshots.
+ var link1 = $("link1");
+ link1.className = "forcelink";
+ var refLink = await snapshotWindow(TestWindow.getWindow());
+ link1.className = "forcevisited";
+ var refVisited = await snapshotWindow(TestWindow.getWindow());
+ link1.className = "";
+ function snapshotsEqual(snap1, snap2) {
+ return compareSnapshots(snap1, snap2, true)[0];
+ }
+ ok(!snapshotsEqual(refLink, refVisited), "references should not match");
+ ok(snapshotsEqual(refLink, await snapshotWindow(TestWindow.getWindow())),
+ "link should initially be blue");
+
+ let observedVisit = false, observedPageShow = false;
+ await new Promise(resolve => {
+ function maybeResolve() {
+ ok(true, "maybe run next test? visited: " + observedVisit + " pageShow: " + observedPageShow);
+ if (observedVisit && observedPageShow)
+ resolve();
+ }
+
+ // Because adding visits is async, we will not be notified immediately.
+ let visitObserver = {
+ observe(aSubject, aTopic, aData)
+ {
+ if (!testURI.equals(aSubject.QueryInterface(Ci.nsIURI))) {
+ return;
+ }
+ os.removeObserver(this, aTopic);
+ observedVisit = true;
+ maybeResolve();
+ },
+ };
+ os.addObserver(visitObserver, "uri-visit-saved");
+ // Load the page that the link on the previous page points to.
+ doPageNavigation({
+ uri: getHttpUrl("bug293235_p2.html"),
+ onNavComplete() {
+ observedPageShow = true;
+ maybeResolve();
+ }
+ });
+ })
+
+ // And the nodes get notified after the "uri-visit-saved" topic, so
+ // we need to execute soon...
+ await new Promise(SimpleTest.executeSoon);
+
+ // Go back, verify the original page was loaded from the bfcache,
+ // and verify that the link is now purple, per the
+ // :visited style.
+ await new Promise(resolve => {
+ doPageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow"],
+ expectedEvents: [ { type: "pageshow",
+ persisted: true,
+ title: "Bug 293235 page1" } ],
+ onNavComplete: resolve,
+ });
+ })
+
+ // Now we can test the link color.
+ ok(snapshotsEqual(refVisited, await snapshotWindow(TestWindow.getWindow())),
+ "visited link should be purple");
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/docshell/test/chrome/bug294258_testcase.html b/docshell/test/chrome/bug294258_testcase.html
new file mode 100644
index 0000000000..cd80fefd06
--- /dev/null
+++ b/docshell/test/chrome/bug294258_testcase.html
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>Bug 294258 Testcase</title>
+ <meta http-equiv="Content-Type" content="application/xhtml+xml"/>
+ <style type="text/css">
+ * {
+ font-family: monospace;
+ }
+ </style>
+ </head>
+ <body>
+ <div>
+ <p>
+ input type="text": <input id="text" type="text"/>
+ </p>
+ <p>
+ input type="checkbox": <input id="checkbox" type="checkbox"/>
+ </p>
+ <p>
+ input type="file": <input id="file" type="file"/>
+ </p>
+ <p>
+ input type="radio":
+ <input type="radio" id="radio1" name="radio" value="radio1"/>
+ <input id="radio2" type="radio" name="radio" value="radio2"/>
+ </p>
+ <p>
+ textarea: <textarea id="textarea" rows="4" cols="80"></textarea>
+ </p>
+ <p>
+ select -> option: <select id="select">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ </select>
+ </p>
+ </div>
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug294258_window.xhtml b/docshell/test/chrome/bug294258_window.xhtml
new file mode 100644
index 0000000000..fe47f3e70f
--- /dev/null
+++ b/docshell/test/chrome/bug294258_window.xhtml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="294258Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(nextTest, 0);"
+ title="bug 294258 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+
+ // Define the generator-iterator for the tests.
+ var tests = testIterator();
+
+ ////
+ // Execute the next test in the generator function.
+ //
+ function nextTest() {
+ tests.next();
+ }
+
+ function $(id) { return TestWindow.getDocument().getElementById(id); }
+
+ ////
+ // Generator function for test steps for bug 294258:
+ // Form values should be preserved on reload.
+ //
+ function* testIterator()
+ {
+ // Load a page containing a form.
+ doPageNavigation( {
+ uri: getHttpUrl("bug294258_testcase.html"),
+ onNavComplete: nextTest
+ } );
+ yield undefined;
+
+ // Change the data for each of the form fields, and reload.
+ $("text").value = "text value";
+ $("checkbox").checked = true;
+ var file = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("294258_test.file");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let filePath = file.path;
+ $("file").value = filePath;
+ $("textarea").value = "textarea value";
+ $("radio1").checked = true;
+ $("select").selectedIndex = 2;
+ doPageNavigation( {
+ reload: true,
+ onNavComplete: nextTest
+ } );
+ yield undefined;
+
+ // Verify that none of the form data has changed.
+ is($("text").value, "text value", "Text value changed");
+ is($("checkbox").checked, true, "Checkbox value changed");
+ is($("file").value, filePath, "File value changed");
+ is($("textarea").value, "textarea value", "Textarea value changed");
+ is($("radio1").checked, true, "Radio value changed");
+ is($("select").selectedIndex, 2, "Select value changed");
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/docshell/test/chrome/bug298622_window.xhtml b/docshell/test/chrome/bug298622_window.xhtml
new file mode 100644
index 0000000000..38abf35107
--- /dev/null
+++ b/docshell/test/chrome/bug298622_window.xhtml
@@ -0,0 +1,135 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="298622Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTest, 0);"
+ title="bug 298622 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src= "docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ // Global variable that holds a reference to the find bar.
+ var gFindBar;
+
+ ////
+ // Test for bug 298622:
+ // Find should work correctly on a page loaded from the
+ // bfcache.
+ //
+ async function runTest()
+ {
+ // Make sure bfcache is on.
+ enableBFCache(true);
+
+ // Load a test page which contains some text to be found.
+ await promisePageNavigation({
+ uri: "data:text/html,<html><head><title>test1</title></head>" +
+ "<body>find this!</body></html>",
+ });
+
+ // Load a second, dummy page, verifying that the original
+ // page gets stored in the bfcache.
+ await promisePageNavigation({
+ uri: getHttpUrl("generic.html"),
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "test1",
+ persisted: true },
+ { type: "pageshow",
+ title: "generic page" } ],
+ });
+
+ // Make sure we unsuppress painting before continuing
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(resolve);
+ });
+
+ // Search for some text that's on the second page (but not on
+ // the first page), and verify that it can be found.
+ gFindBar = document.getElementById("FindToolbar");
+ document.getElementById("cmd_find").doCommand();
+ ok(!gFindBar.hidden, "failed to open findbar");
+ gFindBar._findField.value = "A generic page";
+ gFindBar._find();
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(resolve);
+ });
+
+ // Make sure Find bar's internal status is not 'notfound'
+ isnot(gFindBar._findField.getAttribute("status"), "notfound",
+ "Findfield status attribute should not have been 'notfound'" +
+ " after Find");
+
+ // Make sure the key events above have time to be processed
+ // before continuing
+ await promiseTrue(() =>
+ SpecialPowers.spawn(document.getElementById("content"), [], function() {
+ return content.document.getSelection().toString().toLowerCase() == "a generic page";
+ }), 20
+ );
+
+ is(gFindBar._findField.value, "A generic page",
+ "expected text not present in find input field");
+ is(await SpecialPowers.spawn(document.getElementById("content"), [], async function() {
+ return content.document.getSelection().toString().toLowerCase();
+ }),
+ "a generic page",
+ "find failed on second page loaded");
+
+ // Go back to the original page and verify it's loaded from the
+ // bfcache.
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow"],
+ expectedEvents: [ { type: "pageshow",
+ title: "test1",
+ persisted: true } ],
+ });
+
+ // Search for some text that's on the original page (but not
+ // the dummy page loaded above), and verify that it can
+ // be found.
+ gFindBar = document.getElementById("FindToolbar");
+ document.getElementById("cmd_find").doCommand();
+ ok(!gFindBar.hidden, "failed to open findbar");
+ gFindBar._findField.value = "find this";
+ gFindBar._find();
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(resolve);
+ });
+
+ // Make sure Find bar's internal status is not 'notfound'
+ isnot(gFindBar._findField.getAttribute("status"), "notfound",
+ "Findfield status attribute should not have been 'notfound'" +
+ " after Find");
+
+ // Make sure the key events above have time to be processed
+ // before continuing
+ await promiseTrue(() =>
+ SpecialPowers.spawn(document.getElementById("content"), [], function() {
+ return content.document.getSelection().toString().toLowerCase() == "find this";
+ }), 20
+ );
+
+ is(await SpecialPowers.spawn(document.getElementById("content"), [], async function() {
+ return content.document.getSelection().toString().toLowerCase();
+ }),
+ "find this",
+ "find failed on page loaded from bfcache");
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <commandset>
+ <command id="cmd_find"
+ oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
+ </commandset>
+ <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" remote="true" maychangeremoteness="true" />
+ <findbar id="FindToolbar" browserid="content"/>
+</window>
diff --git a/docshell/test/chrome/bug301397_1.html b/docshell/test/chrome/bug301397_1.html
new file mode 100644
index 0000000000..9943c2efe6
--- /dev/null
+++ b/docshell/test/chrome/bug301397_1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>iframe parent</title>
+ </head>
+<body>
+ <iframe id="iframe" src="bug301397_2.html"/>
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug301397_2.html b/docshell/test/chrome/bug301397_2.html
new file mode 100644
index 0000000000..4237107060
--- /dev/null
+++ b/docshell/test/chrome/bug301397_2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>iframe content #1</title>
+ </head>
+<body>
+ iframe page 1<br/>
+ <a id="link" href="bug301397_3.html">go to next page</a>
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug301397_3.html b/docshell/test/chrome/bug301397_3.html
new file mode 100644
index 0000000000..8d36e92461
--- /dev/null
+++ b/docshell/test/chrome/bug301397_3.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>iframe content #2</title>
+ </head>
+<body>
+ iframe page 2<br/>
+ You made it!
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug301397_4.html b/docshell/test/chrome/bug301397_4.html
new file mode 100644
index 0000000000..5584a4554a
--- /dev/null
+++ b/docshell/test/chrome/bug301397_4.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>dummy page, no iframe</title>
+ </head>
+<body>
+ Just a boring test page, nothing special.
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug301397_window.xhtml b/docshell/test/chrome/bug301397_window.xhtml
new file mode 100644
index 0000000000..cbfa77f7fd
--- /dev/null
+++ b/docshell/test/chrome/bug301397_window.xhtml
@@ -0,0 +1,218 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="301397Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTest, 0);"
+ title="bug 301397 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ ////
+ // Verifies that the given string exists in the innerHTML of the iframe
+ // content.
+ //
+ async function verifyIframeInnerHtml(string) {
+ var iframeInnerHtml = await SpecialPowers.spawn(document.getElementById("content"), [string], (string) =>
+ content.document.getElementById("iframe").contentDocument.body.innerHTML);
+ ok(iframeInnerHtml.includes(string),
+ "iframe contains wrong document: " + iframeInnerHtml);
+ }
+
+ ////
+ // Generator function for test steps for bug 301397:
+ // The correct page should be displayed in an iframe when
+ // navigating back and forwards, when the parent page
+ // occupies multiple spots in the session history.
+ //
+ async function runTest()
+ {
+ // Make sure the bfcache is enabled.
+ enableBFCache(8);
+
+ // Load a dummy page.
+ await promisePageNavigation({
+ uri: getHttpUrl("generic.html"),
+ });
+
+ // Load a page containing an iframe.
+ await promisePageNavigation({
+ uri: getHttpUrl("bug301397_1.html"),
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "generic page",
+ persisted: true },
+ { type: "pageshow",
+ title: "iframe content #1",
+ persisted: false }, // false on initial load
+ { type: "pageshow",
+ title: "iframe parent",
+ persisted: false } ], // false on initial load
+ });
+
+ // Click a link in the iframe to cause the iframe to navigate
+ // to a new page, and wait until the related pagehide/pageshow
+ // events have occurred.
+ let waitForLinkNavigation = promisePageEvents({
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "iframe content #1",
+ persisted: false }, // false, subframe nav
+ { type: "pageshow",
+ title: "iframe content #2",
+ persisted: false } ], // false on initial load
+ });
+ SpecialPowers.spawn(document.getElementById("content"), [], function() {
+ let iframe = content.document.getElementById("iframe");
+ let link = iframe.contentDocument.getElementById("link");
+ let event = iframe.contentDocument.createEvent("MouseEvents");
+ event.initMouseEvent("click", true, true, iframe.contentWindow,
+ 0, 0, 0, 0, 0,
+ false, false, false, false,
+ 0, null);
+ link.dispatchEvent(event);
+ });
+ await waitForLinkNavigation;
+
+ // Load another dummy page. Verify that both the outgoing parent and
+ // iframe pages are stored in the bfcache.
+ await promisePageNavigation({
+ uri: getHttpUrl("bug301397_4.html"),
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "iframe parent",
+ persisted: true },
+ { type: "pagehide",
+ title: "iframe content #2",
+ persisted: true },
+ { type: "pageshow",
+ title: "dummy page, no iframe",
+ persisted: false } ], // false on initial load
+ });
+
+ // Go back. The iframe should show the second page loaded in it.
+ // Both the parent and the iframe pages should be loaded from
+ // the bfcache.
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "dummy page, no iframe",
+ persisted: true },
+ { type: "pageshow",
+ persisted: true,
+ title: "iframe content #2" },
+ { type: "pageshow",
+ persisted: true,
+ title: "iframe parent" } ],
+ });
+
+ verifyIframeInnerHtml("You made it");
+
+ // Go gack again. The iframe should show the first page loaded in it.
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "iframe content #2",
+ persisted: false }, // false, subframe nav
+ { type: "pageshow",
+ title: "iframe content #1",
+ // false since this page was never stored
+ // in the bfcache in the first place
+ persisted: false } ],
+ });
+
+ verifyIframeInnerHtml("go to next page");
+
+ // Go back to the generic page. Now go forward to the last page,
+ // again verifying that the iframe shows the first and second
+ // pages in order.
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "iframe parent",
+ persisted: true },
+ { type: "pagehide",
+ title: "iframe content #1",
+ persisted: true },
+ { type: "pageshow",
+ title: "generic page",
+ persisted: true } ],
+ });
+
+ await promisePageNavigation({
+ forward: true,
+ eventsToListenFor: ["pageshow"],
+ expectedEvents: [ {type: "pageshow",
+ title: "iframe content #1",
+ persisted: true},
+ {type: "pageshow",
+ title: "iframe parent",
+ persisted: true} ],
+ });
+
+ verifyIframeInnerHtml("go to next page");
+
+ await promisePageNavigation({
+ forward: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "iframe content #1",
+ persisted: false }, // false, subframe nav
+ { type: "pageshow",
+ title: "iframe content #2",
+ // false because the page wasn't stored in
+ // bfcache last time it was unloaded
+ persisted: false } ],
+ });
+
+ verifyIframeInnerHtml("You made it");
+
+ await promisePageNavigation({
+ forward: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "iframe parent",
+ persisted: true },
+ { type: "pagehide",
+ title: "iframe content #2",
+ persisted: true },
+ { type: "pageshow",
+ title: "dummy page, no iframe",
+ persisted: true } ],
+ });
+
+ // Go back once more, and again verify that the iframe shows the
+ // second page loaded in it.
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "dummy page, no iframe",
+ persisted: true },
+ { type: "pageshow",
+ persisted: true,
+ title: "iframe content #2" },
+ { type: "pageshow",
+ persisted: true,
+ title: "iframe parent" } ],
+ });
+
+ verifyIframeInnerHtml("You made it");
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug303267.html b/docshell/test/chrome/bug303267.html
new file mode 100644
index 0000000000..21b9f30311
--- /dev/null
+++ b/docshell/test/chrome/bug303267.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <title>
+ bug303267.html
+ </title>
+ </head>
+<body onpageshow="showpageshowcount()">
+<script>
+var pageshowcount = 0;
+function showpageshowcount() {
+ pageshowcount++;
+ var div1 = document.getElementById("div1");
+ while (div1.firstChild) {
+ div1.firstChild.remove();
+ }
+ div1.appendChild(document.createTextNode(
+ "pageshowcount: " + pageshowcount));
+}
+</script>
+<div id="div1">
+ </div>
+</body>
+</html>
diff --git a/docshell/test/chrome/bug303267_window.xhtml b/docshell/test/chrome/bug303267_window.xhtml
new file mode 100644
index 0000000000..741fab0021
--- /dev/null
+++ b/docshell/test/chrome/bug303267_window.xhtml
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="303267Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTests, 0);"
+ title="bug 303267 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ ////
+ // Bug 303267: When a page is displayed from the bfcache, the script globals should
+ // remain intact from the page's initial load.
+ //
+ async function runTests()
+ {
+ // Load an initial test page which should be saved in the bfcache.
+ var navData = {
+ uri: getHttpUrl("bug303267.html"),
+ eventsToListenFor: ["pageshow"],
+ expectedEvents: [ {type: "pageshow", title: "bug303267.html"} ],
+ };
+ await promisePageNavigation(navData);
+
+ // Save the HTML of the test page for later comparison.
+ var originalHTML = await getInnerHTMLById("div1");
+
+ // Load a second test page. The first test page's pagehide event should
+ // have the .persisted property set to true, indicating that it was
+ // stored in the bfcache.
+ navData = {
+ uri: "data:text/html,<html><head><title>page2</title></head>" +
+ "<body>bug303267, page2</body></html>",
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ {type: "pagehide",
+ title: "bug303267.html",
+ persisted: true},
+ {type: "pageshow",
+ title: "page2"} ],
+ };
+ await promisePageNavigation(navData);
+
+ // Go back. Verify that the pageshow event for the original test page
+ // had a .persisted property of true, indicating that it came from the
+ // bfcache.
+ navData = {
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ {type: "pagehide",
+ title: "page2"},
+ {type: "pageshow",
+ title: "bug303267.html",
+ persisted: true} ],
+ };
+ await promisePageNavigation(navData);
+
+ // After going back, if showpagecount() could access a global variable
+ // and change the test div's innerHTML, then we pass. Otherwise, it
+ // threw an exception and the following test will fail.
+ var newHTML = await getInnerHTMLById("div1");
+ isnot(originalHTML,
+ newHTML, "HTML not updated on pageshow; javascript broken?");
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ////
+ // Return the innerHTML of a particular element in the content document.
+ //
+ function getInnerHTMLById(id) {
+ return SpecialPowers.spawn(TestWindow.getBrowser(), [id], (id) => {
+ return content.document.getElementById(id).innerHTML;
+ });
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug311007_window.xhtml b/docshell/test/chrome/bug311007_window.xhtml
new file mode 100644
index 0000000000..13ae5e16e3
--- /dev/null
+++ b/docshell/test/chrome/bug311007_window.xhtml
@@ -0,0 +1,204 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="311007Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="startup();"
+ title="bug 311007 test">
+
+ <script src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js"></script>
+ <script type="application/javascript"><![CDATA[
+ // `content` is the id of the browser element used for the test.
+ /* global content */
+/*
+ Regression test for bug 283733 and bug 307027.
+
+ Bug 283733
+ "accessing a relative anchor in a secure page removes the
+ locked icon and yellow background UI"
+
+ Bug 307027
+ "Going back from secure page to error page does not clear yellow bar"
+
+ And enhancements:
+
+ Bug 478927
+ onLocationChange should notify whether or not loading an error page.
+
+ */
+
+const kDNSErrorURI = "https://example/err.html";
+const kSecureURI =
+ "https://example.com/tests/docshell/test/navigation/blank.html";
+
+/*
+ Step 1: load a network error page. <err.html> Not Secure
+ Step 2: load a secure page. <blank.html> Secure
+ Step 3: a secure page + hashchange. <blank.html#foo> Secure (bug 283733)
+ Step 4: go back to the error page. <err.html> Not Secure (bug 307027)
+ */
+
+var gListener = null;
+
+function WebProgressListener() {
+ this._callback = null;
+}
+
+WebProgressListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ setTimeout(this._callback, 0, aWebProgress, aRequest, aLocation, aFlags);
+ },
+
+ set callback(aVal) {
+ this._callback = aVal;
+ }
+};
+
+function startup() {
+ gListener = new WebProgressListener();
+
+ document.getElementById("content")
+ .webProgress
+ .addProgressListener(gListener,
+ Ci.nsIWebProgress
+ .NOTIFY_LOCATION);
+
+ setTimeout(step1A, 0);
+}
+
+/******************************************************************************
+ * Step 1: Load an error page, and confirm UA knows it's insecure.
+ ******************************************************************************/
+
+function step1A() {
+ gListener.callback = step1B;
+ content.location = kDNSErrorURI;
+}
+
+function step1B(aWebProgress, aRequest, aLocation, aFlags) {
+ is(aLocation.spec, kDNSErrorURI, "Error page's URI (1)");
+
+ ok(!(aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_SAME_DOCUMENT),
+ "DocShell loaded a document (1)");
+
+ ok((aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_ERROR_PAGE),
+ "This page is an error page.");
+
+ ok(!(document.getElementById("content")
+ .browsingContext
+ .secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_IS_SECURE),
+ "This is not a secure page (1)");
+
+ /* Go to step 2. */
+ setTimeout(step2A, 0);
+}
+
+/******************************************************************************
+ * Step 2: Load a HTTPS page, and confirm it's secure.
+ ******************************************************************************/
+
+function step2A() {
+ gListener.callback = step2B;
+ content.location = kSecureURI;
+}
+
+function step2B(aWebProgress, aRequest, aLocation, aFlags) {
+ is(aLocation.spec, kSecureURI, "A URI on HTTPS (2)");
+
+ ok(!(aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_SAME_DOCUMENT),
+ "DocShell loaded a document (2)");
+
+ ok(!(aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_ERROR_PAGE),
+ "This page is not an error page.");
+
+ ok((document.getElementById("content")
+ .browsingContext
+ .secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_IS_SECURE),
+ "This is a secure page (2)");
+
+ /* Go to step 3. */
+ setTimeout(step3A, 0);
+}
+
+/*****************************************************************************
+ * Step 3: Trigger hashchange within a secure page, and confirm UA knows
+ * it's secure. (Bug 283733)
+ *****************************************************************************/
+
+function step3A() {
+ gListener.callback = step3B;
+ content.location += "#foo";
+}
+
+function step3B(aWebProgress, aRequest, aLocation, aFlags) {
+ is(aLocation.spec, kSecureURI + "#foo", "hashchange on HTTPS (3)");
+
+ ok((aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_SAME_DOCUMENT),
+ "We are in the same document as before (3)");
+
+ ok(!(aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_ERROR_PAGE),
+ "This page is not an error page.");
+
+ ok((document.getElementById("content")
+ .browsingContext
+ .secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_IS_SECURE),
+ "This is a secure page (3)");
+
+ /* Go to step 4. */
+ setTimeout(step4A, 0);
+}
+
+/*****************************************************************************
+ * Step 4: Go back from a secure page to an error page, and confirm UA knows
+ * it's not secure. (Bug 307027)
+ *****************************************************************************/
+
+function step4A() {
+ gListener.callback = step4B;
+ content.history.go(-2);
+}
+
+function step4B(aWebProgress, aRequest, aLocation, aFlags) {
+ is(aLocation.spec, kDNSErrorURI, "Go back to the error URI (4)");
+
+ ok(!(aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_SAME_DOCUMENT),
+ "DocShell loaded a document (4)");
+
+ ok((aFlags & Ci.nsIWebProgressListener
+ .LOCATION_CHANGE_ERROR_PAGE),
+ "This page is an error page.");
+
+ ok(!(document.getElementById("content")
+ .browsingContext
+ .secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_IS_SECURE),
+ "This is not a secure page (4)");
+
+ /* End. */
+ document.getElementById("content")
+ .webProgress.removeProgressListener(gListener);
+ gListener = null;
+ finish();
+}
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/docshell/test/chrome/bug321671_window.xhtml b/docshell/test/chrome/bug321671_window.xhtml
new file mode 100644
index 0000000000..60ab5e80d0
--- /dev/null
+++ b/docshell/test/chrome/bug321671_window.xhtml
@@ -0,0 +1,128 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="321671Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTest, 0);"
+ title="bug 321671 test">
+
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ // Maximum number of entries in the bfcache for this session history.
+ // This number is hardcoded in docshell code. In the test, we'll
+ // navigate through enough pages so that we hit one that's been
+ // evicted from the bfcache because it's farther from the current
+ // page than this number.
+ const MAX_BFCACHE_PAGES = 3;
+
+ ////
+ // Bug 321671: Scroll position should be retained when moving backwards and
+ // forwards through pages when bfcache is enabled.
+ //
+ async function runTest()
+ {
+ // Variable to hold the scroll positions of the test pages.
+ var scrollPositions = [];
+
+ // Make sure bfcache is on.
+ enableBFCache(true);
+
+ // Load enough test pages that so the first one is evicted from the
+ // bfcache, scroll down on each page, and save the
+ // current scroll position before continuing. Verify that each
+ // page we're navigating away from is initially put into the bfcache.
+ for (var i = 0; i <= MAX_BFCACHE_PAGES + 1; i++) {
+ let eventsToListenFor = ["pageshow"];
+ let expectedEvents = [ { type: "pageshow",
+ title: "bug321671 page" + (i + 1) } ];
+ if (i > 0) {
+ eventsToListenFor.push("pagehide");
+ expectedEvents.unshift({ type: "pagehide",
+ persisted: true,
+ title: "bug321671 page" + i });
+ }
+ await promisePageNavigation( {
+ uri: "data:text/html,<html><head><title>bug321671 page" + (i + 1) +
+ "</title></head>" +
+ "<body><table border='1' width='300' height='1000'>" +
+ "<tbody><tr><td>" +
+ " page " + (i + 1) + ": foobar foobar foobar foobar " +
+ "</td></tr></tbody></table> " +
+ "</body></html>",
+ eventsToListenFor,
+ expectedEvents,
+ } );
+
+ let { initialScrollY, scrollY } = await SpecialPowers.spawn(TestWindow.getBrowser(), [i], (i) => {
+ let initialScrollY = content.scrollY;
+ content.scrollByLines(10 + (2 * i));
+ return { initialScrollY, scrollY: content.scrollY };
+ });
+ is(initialScrollY, 0,
+ "Page initially has non-zero scrollY position");
+ ok(scrollY > 0,
+ "Page has zero scrollY position after scrolling");
+ scrollPositions[i] = scrollY;
+ }
+
+ // Go back to the first page, one page at a time. For each 'back'
+ // action, verify that its vertical scroll position is restored
+ // correctly. Verify that the last page in the sequence
+ // does not come from the bfcache. Again verify that all pages
+ // that we navigate away from are initially
+ // stored in the bfcache.
+ for (i = MAX_BFCACHE_PAGES + 1; i > 0; i--) {
+ await promisePageNavigation( {
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "bug321671 page" + (i+1),
+ persisted: true },
+ { type: "pageshow",
+ title: "bug321671 page" + i,
+ persisted: i > 1 } ],
+ } );
+
+ is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.scrollY;
+ }), scrollPositions[i-1],
+ "Scroll position not restored while going back!");
+ }
+
+ // Traverse history forward now, and verify scroll position is still
+ // restored. Similar to the backward traversal, verify that all
+ // but the last page in the sequence comes from the bfcache. Also
+ // verify that all of the pages get stored in the bfcache when we
+ // navigate away from them.
+ for (i = 1; i <= MAX_BFCACHE_PAGES + 1; i++) {
+ await promisePageNavigation( {
+ forward: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ persisted: true,
+ title: "bug321671 page" + i },
+ { type: "pageshow",
+ persisted: i < MAX_BFCACHE_PAGES + 1,
+ title: "bug321671 page" + (i + 1) } ],
+ } );
+
+ is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.scrollY;
+ }), scrollPositions[i],
+ "Scroll position not restored while going forward!");
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug360511_case1.html b/docshell/test/chrome/bug360511_case1.html
new file mode 100644
index 0000000000..cca043bb66
--- /dev/null
+++ b/docshell/test/chrome/bug360511_case1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html style="height: 100%">
+<head>
+ <title>
+ bug360511 case 1
+ </title>
+ </head>
+<body style="height: 100%">
+<a id="link1" href="#bottom">jump to bottom</a>
+<div id="div1" style="height: 200%; border: thin solid black;">
+ hello large div
+ </div>
+ <a name="bottom">here's the bottom of the page</a>
+</body>
+</html>
diff --git a/docshell/test/chrome/bug360511_case2.html b/docshell/test/chrome/bug360511_case2.html
new file mode 100644
index 0000000000..217f47724e
--- /dev/null
+++ b/docshell/test/chrome/bug360511_case2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html style="height: 100%">
+<head>
+ <title>
+ bug360511 case 2
+ </title>
+ </head>
+<body style="height: 100%">
+<a id="link1" href="#bottom">jump to bottom</a>
+<div id="div1" style="height: 200%; border: thin solid black;">
+ hello large div
+ </div>
+ <a name="bottom">here's the bottom of the page</a>
+</body>
+</html>
diff --git a/docshell/test/chrome/bug360511_window.xhtml b/docshell/test/chrome/bug360511_window.xhtml
new file mode 100644
index 0000000000..cfb0845ef7
--- /dev/null
+++ b/docshell/test/chrome/bug360511_window.xhtml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="360511Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTest, 0);"
+ title="bug 360511 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ function getScrollY()
+ {
+ return SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.scrollY;
+ });
+ }
+ function getLocation()
+ {
+ return SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.location.href;
+ });
+ }
+
+ ////
+ // Bug 360511: Fragment uri's in session history should be restored correctly
+ // upon back navigation.
+ //
+ async function runTest()
+ {
+ // Case 1: load a page containing a fragment link; the page should be
+ // stored in the bfcache.
+ // Case 2: load a page containing a fragment link; the page should NOT
+ // be stored in the bfcache.
+ for (var i = 1; i < 3; i++)
+ {
+ var url = "bug360511_case" + i + ".html";
+ await promisePageNavigation( {
+ uri: getHttpUrl(url),
+ preventBFCache: i != 1
+ } );
+
+ // Store the original url for later comparison.
+ var originalUrl = TestWindow.getBrowser().currentURI.spec;
+ var originalDocLocation = await getLocation();
+
+ // Verify we're at the top of the page.
+ is(await getScrollY(), 0, "Page initially has a non-zero scrollY property");
+
+ // Click the on the fragment link in the browser, and use setTimeout
+ // to give the event a chance to be processed.
+ await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ var event = content.document.createEvent('MouseEvent');
+ event.initMouseEvent("click", true, true, content, 0,
+ 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ content.document.getElementById("link1").dispatchEvent(event);
+ });
+ await promiseNextPaint();
+
+ // Verify we're no longer at the top of the page.
+ await promiseTrue(async function() {
+ return await getScrollY() > 0;
+ }, 20);
+
+ // Store the fragment url for later comparison.
+ var fragmentUrl = TestWindow.getBrowser().currentURI.spec;
+ let fragDocLocation = await getLocation();
+
+ // Now navigate to any other page
+ var expectedPageTitle = "bug360511 case " + i;
+ await promisePageNavigation( {
+ uri: getHttpUrl("generic.html"),
+ eventsToListenFor: ["pagehide", "pageshow"],
+ expectedEvents: [ {type: "pagehide", title: expectedPageTitle,
+ persisted: i == 1},
+ {type: "pageshow"} ],
+ } );
+
+ // Go back
+ await promisePageNavigation( {
+ back: true,
+ eventsToListenFor: ["pageshow"],
+ expectedEvents: [ {type: "pageshow", title: expectedPageTitle,
+ persisted: i == 1} ],
+ } );
+
+ // Verify the current url is the fragment url
+ is(TestWindow.getBrowser().currentURI.spec, fragmentUrl,
+ "current url is not the previous fragment url");
+ is(await getLocation(), fragDocLocation,
+ "document.location is not the previous fragment url");
+
+ // Go back again. Since we're just going from a fragment url to
+ // parent url, no pageshow event is fired, so don't wait for any
+ // events. Rather, just wait for the page's scrollY property to
+ // change.
+ var originalScrollY = await getScrollY();
+ doPageNavigation( {
+ back: true,
+ eventsToListenFor: []
+ } );
+ await promiseTrue(
+ async function() {
+ return (await getScrollY() != originalScrollY);
+ }, 20);
+
+ // Verify the current url is the original url without fragment
+ is(TestWindow.getBrowser().currentURI.spec, originalUrl,
+ "current url is not the original url");
+ is(await getLocation(), originalDocLocation,
+ "document.location is not the original url");
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug364461_window.xhtml b/docshell/test/chrome/bug364461_window.xhtml
new file mode 100644
index 0000000000..938a015e73
--- /dev/null
+++ b/docshell/test/chrome/bug364461_window.xhtml
@@ -0,0 +1,253 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="364461Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="runTest();"
+ title="364461 test">
+
+ <script src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ var gBrowser;
+
+ async function runTest() {
+ gBrowser = document.getElementById("content");
+
+ // Tests 1 + 2:
+ // Back/forward between two simple documents. Bfcache will be used.
+
+ var test1Doc = "data:text/html,<html><head><title>test1</title></head>" +
+ "<body>test1</body></html>";
+
+ await promisePageNavigation({
+ uri: test1Doc,
+ eventsToListenFor: ["load", "pageshow"],
+ expectedEvents: [{type: "load", title: "test1"},
+ {type: "pageshow", title: "test1", persisted: false}],
+ });
+
+ var test2Doc = "data:text/html,<html><head><title>test2</title></head>" +
+ "<body>test2</body></html>";
+
+ await promisePageNavigation({
+ uri: test2Doc,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test1", persisted: true},
+ {type: "load", title: "test2"},
+ {type: "pageshow", title: "test2", persisted: false}],
+ });
+
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test2", persisted: true},
+ {type: "pageshow", title: "test1", persisted: true}],
+ });
+
+ await promisePageNavigation({
+ forward: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test1", persisted: true},
+ {type: "pageshow", title: "test2", persisted: true}],
+ });
+
+ // Tests 3 + 4:
+ // Back/forward between a two-level deep iframed document and a simple
+ // document. Bfcache will be used and events should be dispatched to
+ // all frames.
+
+ var test3Doc = "data:text/html,<html><head><title>test3</title>" +
+ "</head><body>" +
+ "<iframe src='data:text/html," +
+ "<html><head><title>test3-nested1</title></head>" +
+ "<body>test3-nested1" +
+ "<iframe src=\"data:text/html," +
+ "<html><head><title>test3-nested2</title></head>" +
+ "<body>test3-nested2</body></html>\">" +
+ "</iframe>" +
+ "</body></html>'>" +
+ "</iframe>" +
+ "</body></html>";
+
+ await promisePageNavigation({
+ uri: test3Doc,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test2", persisted: true},
+ {type: "load", title: "test3-nested2"},
+ {type: "pageshow", title: "test3-nested2", persisted: false},
+ {type: "load", title: "test3-nested1"},
+ {type: "pageshow", title: "test3-nested1", persisted: false},
+ {type: "load", title: "test3"},
+ {type: "pageshow", title: "test3", persisted: false}],
+ });
+
+ var test4Doc = "data:text/html,<html><head><title>test4</title></head>" +
+ "<body>test4</body></html>";
+
+ await promisePageNavigation({
+ uri: test4Doc,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test3", persisted: true},
+ {type: "pagehide", title: "test3-nested1", persisted: true},
+ {type: "pagehide", title: "test3-nested2", persisted: true},
+ {type: "load", title: "test4"},
+ {type: "pageshow", title: "test4", persisted: false}],
+ });
+
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test4", persisted: true},
+ {type: "pageshow", title: "test3-nested2", persisted: true},
+ {type: "pageshow", title: "test3-nested1", persisted: true},
+ {type: "pageshow", title: "test3", persisted: true}],
+ });
+
+ // This is where the two nested pagehide are not dispatched in bug 364461
+ await promisePageNavigation({
+ forward: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test3", persisted: true},
+ {type: "pagehide", title: "test3-nested1", persisted: true},
+ {type: "pagehide", title: "test3-nested2", persisted: true},
+ {type: "pageshow", title: "test4", persisted: true}],
+ });
+
+ // Tests 5 + 6:
+ // Back/forward between a document containing an unload handler and a
+ // a simple document. Bfcache won't be used for the first one (see
+ // http://developer.mozilla.org/en/docs/Using_Firefox_1.5_caching).
+
+ var test5Doc = "data:text/html,<html><head><title>test5</title></head>" +
+ "<body onunload='while(false) { /* nop */ }'>" +
+ "test5</body></html>";
+
+ await promisePageNavigation({
+ uri: test5Doc,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test4", persisted: true},
+ {type: "load", title: "test5"},
+ {type: "pageshow", title: "test5", persisted: false}],
+ });
+
+ var test6Doc = "data:text/html,<html><head><title>test6</title></head>" +
+ "<body>test6</body></html>";
+
+ await promisePageNavigation({
+ uri: test6Doc,
+ eventsToListenFor: ["load", "unload", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test5", persisted: false},
+ {type: "unload", title: "test5"},
+ {type: "load", title: "test6"},
+ {type: "pageshow", title: "test6", persisted: false}],
+ });
+
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test6", persisted: true},
+ {type: "load", title: "test5"},
+ {type: "pageshow", title: "test5", persisted: false}],
+ });
+
+ await promisePageNavigation({
+ forward: true,
+ eventsToListenFor: ["unload", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test5", persisted: false},
+ {type: "unload", title: "test5"},
+ {type: "pageshow", title: "test6", persisted: true}],
+ });
+
+ // Test 7:
+ // Testcase from https://bugzilla.mozilla.org/show_bug.cgi?id=384977#c10
+ // Check that navigation is not blocked after a document is restored
+ // from bfcache
+
+ var test7Doc = "data:text/html,<html><head><title>test7</title>" +
+ "</head><body>" +
+ "<iframe src='data:text/html," +
+ "<html><head><title>test7-nested1</title></head>" +
+ "<body>test7-nested1<br/>" +
+ "<a href=\"data:text/plain,aaa\" target=\"_top\">" +
+ "Click me, hit back, click me again</a>" +
+ "</body></html>'>" +
+ "</iframe>" +
+ "</body></html>";
+
+ await promisePageNavigation({
+ uri: test7Doc,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test6", persisted: true},
+ {type: "load", title: "test7-nested1"},
+ {type: "pageshow", title: "test7-nested1", persisted: false},
+ {type: "load", title: "test7"},
+ {type: "pageshow", title: "test7", persisted: false}],
+ });
+
+ // Simulates a click on the link inside the iframe
+ function clickIframeLink() {
+ SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ var iframe = content.document.getElementsByTagName("iframe")[0];
+ var w = iframe.contentWindow;
+ var d = iframe.contentDocument;
+
+ var evt = d.createEvent("MouseEvents");
+ evt.initMouseEvent("click", true, true, w,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ d.getElementsByTagName("a")[0].dispatchEvent(evt);
+ });
+ }
+
+ let clicked = promisePageNavigation({
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test7", persisted: true},
+ {type: "pagehide", title: "test7-nested1", persisted: true},
+ {type: "load"},
+ {type: "pageshow", persisted: false}],
+ waitForEventsOnly: true,
+ });
+ clickIframeLink();
+ await clicked;
+
+ is(gBrowser.currentURI.spec, "data:text/plain,aaa",
+ "Navigation is blocked when clicking link");
+
+ await promisePageNavigation({
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", persisted: true},
+ {type: "pageshow", title: "test7-nested1", persisted: true},
+ {type: "pageshow", title: "test7", persisted: true}],
+ });
+
+ clicked = promisePageNavigation({
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [{type: "pagehide", title: "test7", persisted: true},
+ {type: "pagehide", title: "test7-nested1", persisted: true},
+ {type: "load"},
+ {type: "pageshow", persisted: false}],
+ waitForEventsOnly: true,
+ });
+ clickIframeLink();
+ await clicked;
+
+ is(gBrowser.currentURI.spec, "data:text/plain,aaa",
+ "Navigation is blocked when clicking link");
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ finish();
+ }
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug396519_window.xhtml b/docshell/test/chrome/bug396519_window.xhtml
new file mode 100644
index 0000000000..a5ecbeb3e8
--- /dev/null
+++ b/docshell/test/chrome/bug396519_window.xhtml
@@ -0,0 +1,132 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="396519Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="runTest();"
+ title="396519 test">
+
+ <script src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ var gTestCount = 0;
+
+ async function navigateAndTest(params, expected) {
+ await promisePageNavigation(params);
+ ++gTestCount;
+ await doTest(expected);
+ }
+
+ async function doTest(expected) {
+ function check(testCount, expected) {
+ let history;
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ history = this.browsingContext.sessionHistory;
+ } else {
+ history = this.content.browsingContext.childSessionHistory.legacySHistory;
+ }
+ if (history.count == expected.length) {
+ for (let i = 0; i < history.count; i++) {
+ var shEntry = history.getEntryAtIndex(i).
+ QueryInterface(Ci.nsISHEntry);
+ is(shEntry.isInBFCache, expected[i], `BFCache for shentry[${i}], test ${testCount}`);
+ }
+
+ // Make sure none of the SHEntries share bfcache entries with one
+ // another.
+ for (let i = 0; i < history.count; i++) {
+ for (let j = 0; j < history.count; j++) {
+ if (j == i)
+ continue;
+
+ let shentry1 = history.getEntryAtIndex(i)
+ .QueryInterface(Ci.nsISHEntry);
+ let shentry2 = history.getEntryAtIndex(j)
+ .QueryInterface(Ci.nsISHEntry);
+ ok(!shentry1.sharesDocumentWith(shentry2),
+ 'Test ' + testCount + ': shentry[' + i + "] shouldn't " +
+ "share document with shentry[" + j + ']');
+ }
+ }
+ }
+ else {
+ is(history.count, expected.length, "Wrong history length in test "+testCount);
+ }
+ }
+
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ check.call(TestWindow.getBrowser(), gTestCount, expected);
+ } else {
+ await SpecialPowers.spawn(TestWindow.getBrowser(), [gTestCount, expected], check);
+ }
+ }
+
+ async function runTest() {
+ // Tests 1 + 2:
+ // Back/forward between two simple documents. Bfcache will be used.
+
+ var test1Doc = "data:text/html,<html><head><title>test1</title></head>" +
+ "<body>test1</body></html>";
+
+ await navigateAndTest({
+ uri: test1Doc,
+ }, [false]);
+
+ var test2Doc = test1Doc.replace(/1/,"2");
+
+ await navigateAndTest({
+ uri: test2Doc,
+ }, [true, false]);
+
+ await navigateAndTest({
+ uri: test1Doc,
+ }, [true, true, false]);
+
+ await navigateAndTest({
+ uri: test2Doc,
+ }, [true, true, true, false]);
+
+ await navigateAndTest({
+ uri: test1Doc,
+ }, [false, true, true, true, false]);
+
+ await navigateAndTest({
+ uri: test2Doc,
+ }, [false, false, true, true, true, false]);
+
+ await navigateAndTest({
+ back: true,
+ }, [false, false, true, true, false, true]);
+
+ await navigateAndTest({
+ forward: true,
+ }, [false, false, true, true, true, false]);
+
+ await navigateAndTest({
+ gotoIndex: 1,
+ }, [false, false, true, true, true, false]);
+
+ await navigateAndTest({
+ back: true,
+ }, [false, true, true, true, false, false]);
+
+ await navigateAndTest({
+ gotoIndex: 5,
+ }, [false, false, true, true, false, false]);
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ finish();
+ }
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug396649_window.xhtml b/docshell/test/chrome/bug396649_window.xhtml
new file mode 100644
index 0000000000..c378f11dc3
--- /dev/null
+++ b/docshell/test/chrome/bug396649_window.xhtml
@@ -0,0 +1,119 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="396649Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(nextTest, 0);"
+ title="bug 396649 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ // Define the generator-iterator for the tests.
+ var tests = testIterator();
+
+ // Maximum number of entries in the bfcache for this session history.
+ // This number is hardcoded in docshell code. In the test, we'll
+ // navigate through enough pages so that we hit one that's been
+ // evicted from the bfcache because it's farther from the current
+ // page than this number.
+ const MAX_BFCACHE_PAGES = 3;
+
+ ////
+ // Execute the next test in the generator function.
+ //
+ function nextTest() {
+ tests.next();
+ }
+
+ ////
+ // Generator function for test steps for bug 396649: Content
+ // viewers should be evicted from bfcache when going back if more
+ // than MAX_BFCACHE_PAGES from the current index.
+ //
+ function* testIterator()
+ {
+ // Make sure bfcache is on.
+ enableBFCache(true);
+
+ // Load enough pages so that the first loaded is eviced from
+ // the bfcache, since it is greater the MAX_BFCACHE_PAGES from
+ // the current position in the session history. Verify all
+ // of the pages are initially stored in the bfcache when
+ // they're unloaded.
+ for (var i = 0; i <= MAX_BFCACHE_PAGES + 1; i++) {
+ let eventsToListenFor = ["pageshow"];
+ let expectedEvents = [ { type: "pageshow",
+ title: "bug396649 page" + i } ];
+ if (i > 0) {
+ eventsToListenFor.push("pagehide");
+ expectedEvents.unshift({ type: "pagehide",
+ title: "bug396649 page" + (i-1) });
+ }
+ doPageNavigation( {
+ uri: "data:text/html,<!DOCTYPE html><html>" +
+ "<head><title>bug396649 page" + i +
+ "</title></head>" +
+ "<body>" +
+ "test page " + i +
+ "</body></html>",
+ eventsToListenFor,
+ expectedEvents,
+ onNavComplete: nextTest
+ } );
+ yield undefined;
+ }
+
+ // Go back to the first page, one page at a time. The first
+ // MAX_BFCACHE_PAGES pages loaded via back should come from the bfcache,
+ // the last should not, since it should have been evicted during the
+ // previous navigation sequence. Verify all pages are initially stored
+ // in the bfcache when they're unloaded.
+ for (i = MAX_BFCACHE_PAGES + 1; i > 0; i--) {
+ doPageNavigation( {
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "bug396649 page" + i,
+ persisted: true },
+ { type: "pageshow",
+ title: "bug396649 page" + (i - 1),
+ persisted: i > 1 } ],
+ onNavComplete: nextTest
+ } );
+ yield undefined;
+ }
+
+ // Traverse history forward now. Again, the first MAX_BFCACHE_PAGES
+ // pages should come from the bfcache, the last should not,
+ // since it should have been evicted during the backwards
+ // traversal above. Verify all pages are initially stored
+ // in the bfcache when they're unloaded.
+ for (i = 1; i <= MAX_BFCACHE_PAGES + 1; i++) {
+ doPageNavigation( {
+ forward: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "bug396649 page" + (i-1),
+ persisted: true },
+ { type: "pageshow",
+ title: "bug396649 page" + i,
+ persisted: i < MAX_BFCACHE_PAGES + 1 } ],
+ onNavComplete: nextTest
+ } );
+ yield undefined;
+ }
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug449778_window.xhtml b/docshell/test/chrome/bug449778_window.xhtml
new file mode 100644
index 0000000000..3197b7acf4
--- /dev/null
+++ b/docshell/test/chrome/bug449778_window.xhtml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 449778" onload="doTheTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <hbox id="parent">
+ </hbox>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ /* globals SimpleTest, is */
+ var imports = [ "SimpleTest", "is" ];
+ for (var name of imports) {
+ window[name] = window.arguments[0][name];
+ }
+
+ function $(id) {
+ return document.getElementById(id);
+ }
+
+ function addBrowser(parent, id, width, height) {
+ var b =
+ document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "browser");
+ b.setAttribute("type", "content");
+ b.setAttribute("id", id);
+ b.setAttribute("width", width);
+ b.setAttribute("height", height);
+ $(parent).appendChild(b);
+ }
+ addBrowser("parent", "f1", 300, 200);
+ addBrowser("parent", "f2", 300, 200);
+
+ /** Test for Bug 449778 */
+ var doc1 = "data:text/html,<html><body>This is a test</body></html>";
+ var doc2 = "data:text/html,<html><body>This is a second test</body></html>";
+ var doc3 = "data:text/html,<html><body>This is a <script>var evt = document.createEvent('Events'); evt.initEvent('testEvt', true, true); document.dispatchEvent(evt);</script>third test</body></html>";
+
+
+ $("f1").setAttribute("src", doc1);
+ $("f2").setAttribute("src", doc2);
+
+ function doTheTest() {
+ var strs = { "f1": "", "f2" : "" };
+ function attachListener(node, type) {
+ var listener = function(e) {
+ if (strs[node.id]) strs[node.id] += " ";
+ strs[node.id] += node.id + ".page" + type;
+ }
+ node.addEventListener("page" + type, listener);
+
+ listener.detach = function() {
+ node.removeEventListener("page" + type, listener);
+ }
+ return listener;
+ }
+
+ var l1 = attachListener($("f1"), "show");
+ var l2 = attachListener($("f1"), "hide");
+ var l3 = attachListener($("f2"), "show");
+ var l4 = attachListener($("f2"), "hide");
+
+ $("f1").swapDocShells($("f2"));
+
+ is(strs.f1, "f1.pagehide f1.pageshow",
+ "Expected hide then show on first loaded page");
+ is(strs.f2, "f2.pagehide f2.pageshow",
+ "Expected hide then show on second loaded page");
+
+ function listener2() {
+ $("f2").removeEventListener("testEvt", listener2);
+
+ strs = { "f1": "", "f2" : "" };
+
+ $("f1").swapDocShells($("f2"));
+ is(strs.f1, "f1.pagehide",
+ "Expected hide on already-loaded page, then nothing");
+ is(strs.f2, "f2.pageshow f2.pagehide f2.pageshow",
+ "Expected show on still-loading page, then hide on it, then show " +
+ "on already-loaded page");
+
+ strs = { "f1": "", "f2" : "" };
+
+ $("f1").addEventListener("pageshow", listener3);
+ }
+
+ function listener3() {
+ $("f1").removeEventListener("pageshow", listener3);
+
+ is(strs.f1, "f1.pageshow",
+ "Expected show as our page finishes loading");
+ is(strs.f2, "", "Expected no more events here.");
+
+ l1.detach();
+ l2.detach();
+ l3.detach();
+ l4.detach();
+
+ window.close();
+ SimpleTest.finish();
+ }
+
+ $("f2").addEventListener("testEvt", listener2, false, true);
+ $("f2").setAttribute("src", doc3);
+ }
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/bug449780_window.xhtml b/docshell/test/chrome/bug449780_window.xhtml
new file mode 100644
index 0000000000..c37bc096b2
--- /dev/null
+++ b/docshell/test/chrome/bug449780_window.xhtml
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 449780" onload="setTimeout(doTheTest, 0);"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <hbox id="parent">
+ </hbox>
+
+ <!-- test code goes here -->
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ function addBrowser(parent, width, height) {
+ var b =
+ document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "browser");
+ b.setAttribute("type", "content");
+ b.setAttribute("id", "content");
+ b.setAttribute("width", width);
+ b.setAttribute("height", height);
+ b.setAttribute("remote", SpecialPowers.Services.appinfo.sessionHistoryInParent);
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ b.setAttribute("maychangeremoteness", "true");
+ }
+ document.getElementById("parent").appendChild(b);
+ return b;
+ }
+
+ let f1 = addBrowser("parent", 300, 200);
+
+ /** Test for Bug 449780 */
+ var doc1 = "data:text/html,<html><body>This is a test</body></html>";
+ var doc2 = "data:text/html,<html><body>This is a second test</body></html>";
+
+ async function doTheTest() {
+ await promisePageNavigation({
+ uri: doc1,
+ });
+ let { origDOM, modifiedDOM } = await SpecialPowers.spawn(f1, [], () => {
+ var origDOM = content.document.documentElement.innerHTML;
+ content.document.body.textContent = "Modified";
+ var modifiedDOM = content.document.documentElement.innerHTML;
+ isnot(origDOM, modifiedDOM, "DOM should be different");
+ return { origDOM, modifiedDOM };
+ });
+
+ await promisePageNavigation({
+ uri: doc2,
+ });
+
+ await promisePageNavigation({
+ back: true,
+ });
+
+ await SpecialPowers.spawn(f1, [modifiedDOM], (modifiedDOM) => {
+ is(content.document.documentElement.innerHTML, modifiedDOM, "Should have been bfcached");
+ });
+
+ await promisePageNavigation({
+ forward: true,
+ });
+
+ f1.removeAttribute("id");
+ let f2 = addBrowser("parent", 300, 200);
+
+ // Make sure there's a document or the swap will fail.
+ await promisePageNavigation({
+ uri: "about:blank",
+ });
+
+ f1.swapDocShells(f2);
+
+ await promisePageNavigation({
+ back: true,
+ });
+
+ await SpecialPowers.spawn(f2, [origDOM], (origDOM) => {
+ is(content.document.documentElement.innerHTML, origDOM, "Should not have been bfcached");
+ });
+
+ finish();
+ }
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/bug454235-subframe.xhtml b/docshell/test/chrome/bug454235-subframe.xhtml
new file mode 100644
index 0000000000..a8b6178e65
--- /dev/null
+++ b/docshell/test/chrome/bug454235-subframe.xhtml
@@ -0,0 +1,7 @@
+<window title="Mozilla Bug 454235 SubFrame"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <deck flex="1">
+ <browser id="topBrowser" src="about:mozilla"/>
+ <browser id="burriedBrowser" src="about:mozilla"/>
+ </deck>
+</window>
diff --git a/docshell/test/chrome/bug582176_window.xhtml b/docshell/test/chrome/bug582176_window.xhtml
new file mode 100644
index 0000000000..b43220a2fe
--- /dev/null
+++ b/docshell/test/chrome/bug582176_window.xhtml
@@ -0,0 +1,74 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="303267Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="runTest();"
+ title="bug 582176 test">
+
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ ////
+ // Bug 582176.
+ //
+ async function runTest()
+ {
+ enableBFCache(true);
+
+ var notificationCount = 0;
+
+ let onGlobalCreation = () => {
+ ++notificationCount;
+ };
+
+ await promisePageNavigation({
+ uri: "http://mochi.test:8888/chrome/docshell/test/chrome/582176_dummy.html",
+ onGlobalCreation,
+ });
+ is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ let testVar = content.testVar;
+ content.testVar = 1;
+ return testVar;
+ }), undefined,
+ "variable unexpectedly there already");
+ is(notificationCount, 1, "Should notify on first navigation");
+
+ await promisePageNavigation({
+ uri: "http://mochi.test:8888/chrome/docshell/test/chrome/582176_dummy.html?2",
+ onGlobalCreation,
+ });
+ is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.testVar;
+ }), undefined,
+ "variable should no longer be there");
+ is(notificationCount, 2, "Should notify on second navigation");
+
+ await promisePageNavigation({
+ back: true,
+ });
+ is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.testVar;
+ }), 1,
+ "variable should still be there");
+ is(notificationCount, 2, "Should not notify on back navigation");
+
+ await promisePageNavigation({
+ uri: "http://mochi.test:8888/chrome/docshell/test/chrome/582176_xml.xml",
+ onGlobalCreation,
+ });
+ is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return content.document.body.textContent;
+ }), "xslt result",
+ "Transform performed successfully");
+ is(notificationCount, 3, "Should notify only once on XSLT navigation");
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/bug608669.xhtml b/docshell/test/chrome/bug608669.xhtml
new file mode 100644
index 0000000000..993f24051c
--- /dev/null
+++ b/docshell/test/chrome/bug608669.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 608669 - Blank page"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="notifyOpener();">
+ <description flex="1" value="This window is intentionally left blank"/>
+ <script type="application/javascript">
+ function notifyOpener() {
+ if (opener) {
+ opener.postMessage("load", "*");
+ }
+ }
+ </script>
+</window>
diff --git a/docshell/test/chrome/bug662200_window.xhtml b/docshell/test/chrome/bug662200_window.xhtml
new file mode 100644
index 0000000000..7c6f656f26
--- /dev/null
+++ b/docshell/test/chrome/bug662200_window.xhtml
@@ -0,0 +1,119 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="303267Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTest, 0);"
+ title="bug 662200 test">
+
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);
+
+ ////
+ // Bug 662200
+ //
+ async function runTest()
+ {
+ // Load the first test page
+ var navData = {
+ uri: getHttpUrl("662200a.html"),
+ eventsToListenFor: ["pageshow"],
+ expectedEvents: [ {type: "pageshow", title: "A"} ],
+ };
+ await promisePageNavigation(navData);
+
+ // Load the second test page.
+ navData = {
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ {type: "pagehide",
+ title: "A"},
+ {type: "pageshow",
+ title: "B"} ],
+ }
+ let clicked = promisePageEvents(navData);
+ SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ var link = content.document.getElementById("link");
+ var event = content.document.createEvent("MouseEvents");
+ event.initMouseEvent("click", true, true, content,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ link.dispatchEvent(event);
+ });
+ await clicked;
+
+ // Load the third test page.
+ navData = {
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ {type: "pagehide",
+ title: "B"},
+ {type: "pageshow",
+ title: "C"} ],
+ };
+ clicked = promisePageEvents(navData);
+ SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ var link = content.document.getElementById("link");
+ var event = content.document.createEvent("MouseEvents");
+ event.initMouseEvent("click", true, true, content,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ link.dispatchEvent(event);
+ });
+ await clicked;
+
+ // Go back.
+ navData = {
+ back: true,
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ {type: "pagehide",
+ title: "C"},
+ {type: "pageshow",
+ title: "B"} ],
+ };
+ await promisePageNavigation(navData);
+
+ // Reload.
+ navData = {
+ eventsToListenFor: ["pageshow", "pagehide"],
+ expectedEvents: [ {type: "pagehide",
+ title: "B"},
+ {type: "pageshow",
+ title: "B"} ],
+ };
+ // Asking the docshell harness to reload for us will call reload on
+ // nsDocShell which has different behavior than the reload on nsSHistory
+ // so we call reloadCurrentEntry() (which is equivalent to reload(0) and
+ // visible from JS) explicitly here.
+ let reloaded = promisePageEvents(navData);
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ TestWindow.getBrowser().browsingContext.sessionHistory.reloadCurrentEntry();
+ } else {
+ SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.legacySHistory.reloadCurrentEntry();
+ });
+ }
+ await reloaded;
+
+ // After this sequence of events, we should be able to go back and forward
+ is(TestWindow.getBrowser().canGoBack, true, "Should be able to go back!");
+ is(TestWindow.getBrowser().canGoForward, true, "Should be able to go forward!");
+ let requestedIndex;
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ requestedIndex = TestWindow.getBrowser().browsingContext.sessionHistory.requestedIndex;
+ } else {
+ requestedIndex = await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ return docShell.sessionHistory.legacySHistory.requestedIndex;
+ })
+ }
+ is(requestedIndex, -1, "Requested index should be cleared!");
+
+ Services.prefs.clearUserPref("browser.navigation.requireUserInteraction");
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" />
+</window>
diff --git a/docshell/test/chrome/bug690056_window.xhtml b/docshell/test/chrome/bug690056_window.xhtml
new file mode 100644
index 0000000000..15b19f730a
--- /dev/null
+++ b/docshell/test/chrome/bug690056_window.xhtml
@@ -0,0 +1,173 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="690056Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(nextTest, 0);"
+ title="bug 6500056 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ var tests = testIterator();
+
+ function nextTest() {
+ tests.next();
+ }
+
+ // Makes sure that we fire the visibilitychange events
+ function* testIterator() {
+ // Enable bfcache
+ enableBFCache(8);
+
+ TestWindow.getBrowser().docShellIsActive = true;
+
+ // Load something for a start
+ doPageNavigation({
+ uri: 'data:text/html,<title>initial load</title>',
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ // Now load a new page
+ doPageNavigation({
+ uri: 'data:text/html,<title>new load</title>',
+ eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ],
+ expectedEvents: [ { type: "pagehide",
+ title: "initial load",
+ persisted: true },
+ { type: "visibilitychange",
+ title: "initial load",
+ visibilityState: "hidden",
+ hidden: true },
+ // No visibilitychange events fired for initial pageload
+ { type: "pageshow",
+ title: "new load",
+ persisted: false }, // false on initial load
+ ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ // Now go back
+ doPageNavigation({
+ back: true,
+ eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ],
+ expectedEvents: [ { type: "pagehide",
+ title: "new load",
+ persisted: true },
+ { type: "visibilitychange",
+ title: "new load",
+ visibilityState: "hidden",
+ hidden: true },
+ { type: "visibilitychange",
+ title: "initial load",
+ visibilityState: "visible",
+ hidden: false },
+ { type: "pageshow",
+ title: "initial load",
+ persisted: true },
+ ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ // And forward
+ doPageNavigation({
+ forward: true,
+ eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ],
+ expectedEvents: [ { type: "pagehide",
+ title: "initial load",
+ persisted: true },
+ { type: "visibilitychange",
+ title: "initial load",
+ visibilityState: "hidden",
+ hidden: true },
+ { type: "visibilitychange",
+ title: "new load",
+ visibilityState: "visible",
+ hidden: false },
+ { type: "pageshow",
+ title: "new load",
+ persisted: true },
+ ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ waitForPageEvents({
+ eventsToListenFor: [ "visibilitychange" ],
+ expectedEvents: [ { type: "visibilitychange",
+ title: "new load",
+ visibilityState: "hidden",
+ hidden: true },
+ ],
+ onNavComplete: nextTest
+ });
+
+ // Now flip our docshell to not active
+ TestWindow.getBrowser().docShellIsActive = false;
+ yield undefined;
+
+ // And navigate back; there should be no visibility state transitions
+ doPageNavigation({
+ back: true,
+ eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ],
+ expectedEvents: [ { type: "pagehide",
+ title: "new load",
+ persisted: true },
+ { type: "pageshow",
+ title: "initial load",
+ persisted: true },
+ ],
+ unexpectedEvents: [ "visibilitychange" ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ waitForPageEvents({
+ eventsToListenFor: [ "visibilitychange" ],
+ expectedEvents: [ { type: "visibilitychange",
+ title: "initial load",
+ visibilityState: "visible",
+ hidden: false },
+ ],
+ onNavComplete: nextTest
+ });
+
+ // Now set the docshell active again
+ TestWindow.getBrowser().docShellIsActive = true;
+ yield undefined;
+
+ // And forward
+ doPageNavigation({
+ forward: true,
+ eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ],
+ expectedEvents: [ { type: "pagehide",
+ title: "initial load",
+ persisted: true },
+ { type: "visibilitychange",
+ title: "initial load",
+ visibilityState: "hidden",
+ hidden: true },
+ { type: "visibilitychange",
+ title: "new load",
+ visibilityState: "visible",
+ hidden: false },
+ { type: "pageshow",
+ title: "new load",
+ persisted: true },
+ ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" manualactiveness="true" />
+</window>
diff --git a/docshell/test/chrome/bug846906.html b/docshell/test/chrome/bug846906.html
new file mode 100644
index 0000000000..a289417ea8
--- /dev/null
+++ b/docshell/test/chrome/bug846906.html
@@ -0,0 +1,10 @@
+<html>
+ <head>
+ <title>
+ </title>
+ </head>
+ <body>
+ <div id="div1" style="width:1024px; height:768px; border:none;">
+ </div>
+ </body>
+</html>
diff --git a/docshell/test/chrome/bug89419.sjs b/docshell/test/chrome/bug89419.sjs
new file mode 100644
index 0000000000..7172690a9a
--- /dev/null
+++ b/docshell/test/chrome/bug89419.sjs
@@ -0,0 +1,12 @@
+function handleRequest(request, response) {
+ var redirectstate = "/docshell/test/chrome/bug89419.sjs";
+ response.setStatusLine("1.1", 302, "Found");
+ if (getState(redirectstate) == "") {
+ response.setHeader("Location", "red.png", false);
+ setState(redirectstate, "red");
+ } else {
+ response.setHeader("Location", "blue.png", false);
+ setState(redirectstate, "");
+ }
+ response.setHeader("Cache-Control", "no-cache", false);
+}
diff --git a/docshell/test/chrome/bug89419_window.xhtml b/docshell/test/chrome/bug89419_window.xhtml
new file mode 100644
index 0000000000..12b9dec650
--- /dev/null
+++ b/docshell/test/chrome/bug89419_window.xhtml
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="89419Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(runTests, 0);"
+ title="bug 89419 test">
+
+ <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+
+ <script type="application/javascript"><![CDATA[
+ ////
+ // A visited link should have the :visited style applied
+ // to it when displayed on a page which was fetched from
+ // the bfcache.
+ //
+ async function runTests() {
+ // Disable rcwn to make cache behavior deterministic.
+ var {SpecialPowers} = window.arguments[0];
+ await SpecialPowers.pushPrefEnv({"set":[["network.http.rcwn.enabled", false]]});
+
+ // Load a test page containing an image referring to the sjs that returns
+ // a different redirect every time it's loaded.
+ await new Promise(resolve => {
+ doPageNavigation({
+ uri: getHttpUrl("89419.html"),
+ onNavComplete: resolve,
+ preventBFCache: true,
+ });
+ })
+
+ var first = await snapshotWindow(TestWindow.getWindow());
+
+ await new Promise(resolve => {
+ doPageNavigation({
+ uri: "about:blank",
+ onNavComplete: resolve,
+ });
+ });
+
+ var second = await snapshotWindow(TestWindow.getWindow());
+ function snapshotsEqual(snap1, snap2) {
+ return compareSnapshots(snap1, snap2, true)[0];
+ }
+ ok(!snapshotsEqual(first, second), "about:blank should not be the same as the image web page");
+
+ await new Promise(resolve => {
+ doPageNavigation({
+ back: true,
+ onNavComplete: resolve,
+ });
+ });
+
+ var third = await snapshotWindow(TestWindow.getWindow());
+ ok(!snapshotsEqual(third, second), "going back should not be the same as about:blank");
+ ok(snapshotsEqual(first, third), "going back should be the same as the initial load");
+
+ // Tell the framework the test is finished.
+ finish();
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/docshell/test/chrome/bug909218.html b/docshell/test/chrome/bug909218.html
new file mode 100644
index 0000000000..a11fa6000d
--- /dev/null
+++ b/docshell/test/chrome/bug909218.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css">
+ <script src="bug909218.js"></script>
+</head>
+<body>
+ <img src="http://mochi.test:8888/tests/docshell/test/chrome/red.png">
+ <!-- an iframe so we can check these too get the correct flags -->
+ <iframe src="generic.html"/>
+</body>
+</html>
diff --git a/docshell/test/chrome/bug909218.js b/docshell/test/chrome/bug909218.js
new file mode 100644
index 0000000000..2222480cd3
--- /dev/null
+++ b/docshell/test/chrome/bug909218.js
@@ -0,0 +1,2 @@
+// This file exists just to ensure that we load it with the correct flags.
+dump("bug909218.js loaded\n");
diff --git a/docshell/test/chrome/bug92598_window.xhtml b/docshell/test/chrome/bug92598_window.xhtml
new file mode 100644
index 0000000000..bad91f3df6
--- /dev/null
+++ b/docshell/test/chrome/bug92598_window.xhtml
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="92598Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="onLoad();"
+ title="92598 test">
+
+ <script src="chrome://mochikit/content/chrome-harness.js" />
+ <script type="application/javascript" src="docshell_helpers.js" />
+ <script type="application/javascript"><![CDATA[
+ var gBrowser;
+ var gTestsIterator;
+
+ function onLoad() {
+ gBrowser = document.getElementById("content");
+ gTestsIterator = testsIterator();
+ nextTest();
+ }
+
+ function nextTest() {
+ gTestsIterator.next();
+ }
+
+ function* testsIterator() {
+ // Load a page with a no-cache header, followed by a simple page
+ // On pagehide, first page should report it is not being persisted
+ var test1DocURI = "http://mochi.test:8888/chrome/docshell/test/chrome/92598_nostore.html";
+
+ doPageNavigation({
+ uri: test1DocURI,
+ eventsToListenFor: ["load", "pageshow"],
+ expectedEvents: [ { type: "load",
+ title: "test1" },
+ { type: "pageshow",
+ title: "test1",
+ persisted: false } ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ var test2Doc = "data:text/html,<html><head><title>test2</title></head>" +
+ "<body>test2</body></html>";
+
+ doPageNavigation({
+ uri: test2Doc,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "test1",
+ persisted: false },
+ { type: "load",
+ title: "test2" },
+ { type: "pageshow",
+ title: "test2",
+ persisted: false } ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ // Now go back in history. First page should not have been cached.
+ // Check persisted property to confirm
+ doPageNavigation({
+ back: true,
+ eventsToListenFor: ["load", "pageshow", "pagehide"],
+ expectedEvents: [ { type: "pagehide",
+ title: "test2",
+ persisted: true },
+ { type: "load",
+ title: "test1" },
+ { type: "pageshow",
+ title: "test1",
+ persisted: false } ],
+ onNavComplete: nextTest
+ });
+ yield undefined;
+
+ finish();
+ }
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" />
+</window>
diff --git a/docshell/test/chrome/chrome.toml b/docshell/test/chrome/chrome.toml
new file mode 100644
index 0000000000..ee4d287247
--- /dev/null
+++ b/docshell/test/chrome/chrome.toml
@@ -0,0 +1,142 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = [
+ "662200a.html",
+ "662200b.html",
+ "662200c.html",
+ "89419.html",
+ "bug113934_window.xhtml",
+ "bug215405_window.xhtml",
+ "bug293235.html",
+ "bug293235_p2.html",
+ "bug293235_window.xhtml",
+ "bug294258_testcase.html",
+ "bug294258_window.xhtml",
+ "bug298622_window.xhtml",
+ "bug301397_1.html",
+ "bug301397_2.html",
+ "bug301397_3.html",
+ "bug301397_4.html",
+ "bug301397_window.xhtml",
+ "bug303267.html",
+ "bug303267_window.xhtml",
+ "bug311007_window.xhtml",
+ "bug321671_window.xhtml",
+ "bug360511_case1.html",
+ "bug360511_case2.html",
+ "bug360511_window.xhtml",
+ "bug364461_window.xhtml",
+ "bug396519_window.xhtml",
+ "bug396649_window.xhtml",
+ "bug449778_window.xhtml",
+ "bug449780_window.xhtml",
+ "bug454235-subframe.xhtml",
+ "bug608669.xhtml",
+ "bug662200_window.xhtml",
+ "bug690056_window.xhtml",
+ "bug846906.html",
+ "bug89419_window.xhtml",
+ "bug909218.html",
+ "bug909218.js",
+ "docshell_helpers.js",
+ "DocShellHelpers.sys.mjs",
+ "file_viewsource_forbidden_in_iframe.html",
+ "generic.html",
+ "mozFrameType_window.xhtml",
+ "test_docRedirect.sjs",
+]
+
+["test_allowContentRetargeting.html"]
+
+["test_bug89419.xhtml"]
+
+["test_bug92598.xhtml"]
+support-files = [
+ "92598_nostore.html",
+ "92598_nostore.html^headers^",
+ "bug92598_window.xhtml",
+]
+
+["test_bug112564.xhtml"]
+support-files = [
+ "bug112564_window.xhtml",
+ "112564_nocache.html",
+ "112564_nocache.html^headers^",
+]
+
+["test_bug113934.xhtml"]
+
+["test_bug215405.xhtml"]
+
+["test_bug293235.xhtml"]
+skip-if = ["true"] # bug 1393441
+
+["test_bug294258.xhtml"]
+
+["test_bug298622.xhtml"]
+
+["test_bug301397.xhtml"]
+
+["test_bug303267.xhtml"]
+
+["test_bug311007.xhtml"]
+
+["test_bug321671.xhtml"]
+skip-if = ["os == 'mac' && !debug"] # Bug 1784831
+
+["test_bug360511.xhtml"]
+skip-if = ["os == 'win' && processor == 'x86'"]
+
+["test_bug364461.xhtml"]
+
+["test_bug396519.xhtml"]
+
+["test_bug396649.xhtml"]
+
+["test_bug428288.html"]
+
+["test_bug449778.xhtml"]
+
+["test_bug449780.xhtml"]
+
+["test_bug453650.xhtml"]
+
+["test_bug454235.xhtml"]
+
+["test_bug456980.xhtml"]
+
+["test_bug565388.xhtml"]
+skip-if = ["true"] # Bug 1026815,Bug 1546159
+
+["test_bug582176.xhtml"]
+support-files = [
+ "582176_dummy.html",
+ "582176_xml.xml",
+ "582176_xslt.xsl",
+ "bug582176_window.xhtml",
+]
+
+["test_bug608669.xhtml"]
+
+["test_bug662200.xhtml"]
+
+["test_bug690056.xhtml"]
+
+["test_bug789773.xhtml"]
+
+["test_bug846906.xhtml"]
+
+["test_bug909218.html"]
+
+["test_docRedirect.xhtml"]
+
+["test_mozFrameType.xhtml"]
+
+["test_open_and_immediately_close_opener.html"]
+# This bug only manifests in the non-e10s window open codepath. The test
+# should be updated to make sure it still opens a new window in the parent
+# process if and when we start running chrome mochitests with e10s enabled.
+skip-if = ["e10s"]
+
+["test_viewsource_forbidden_in_iframe.xhtml"]
+skip-if = ["true"] # bug 1019315
diff --git a/docshell/test/chrome/docshell_helpers.js b/docshell/test/chrome/docshell_helpers.js
new file mode 100644
index 0000000000..7f16708087
--- /dev/null
+++ b/docshell/test/chrome/docshell_helpers.js
@@ -0,0 +1,759 @@
+if (!window.opener && window.arguments) {
+ window.opener = window.arguments[0];
+}
+/**
+ * Import common SimpleTest methods so that they're usable in this window.
+ */
+/* globals SimpleTest, is, isnot, ok, onerror, todo, todo_is, todo_isnot */
+var imports = [
+ "SimpleTest",
+ "is",
+ "isnot",
+ "ok",
+ "onerror",
+ "todo",
+ "todo_is",
+ "todo_isnot",
+];
+for (var name of imports) {
+ window[name] = window.opener.wrappedJSObject[name];
+}
+const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+);
+
+const ACTOR_MODULE_URI =
+ "chrome://mochitests/content/chrome/docshell/test/chrome/DocShellHelpers.sys.mjs";
+const { DocShellHelpersParent } = ChromeUtils.importESModule(ACTOR_MODULE_URI);
+// Some functions assume chrome-harness.js has been loaded.
+/* import-globals-from ../../../testing/mochitest/chrome-harness.js */
+
+/**
+ * Define global constants and variables.
+ */
+const NAV_NONE = 0;
+const NAV_BACK = 1;
+const NAV_FORWARD = 2;
+const NAV_GOTOINDEX = 3;
+const NAV_URI = 4;
+const NAV_RELOAD = 5;
+
+var gExpectedEvents; // an array of events which are expected to
+// be triggered by this navigation
+var gUnexpectedEvents; // an array of event names which are NOT expected
+// to be triggered by this navigation
+var gFinalEvent; // true if the last expected event has fired
+var gUrisNotInBFCache = []; // an array of uri's which shouldn't be stored
+// in the bfcache
+var gNavType = NAV_NONE; // defines the most recent navigation type
+// executed by doPageNavigation
+var gOrigMaxTotalViewers = undefined; // original value of max_total_viewers, // to be restored at end of test
+
+var gExtractedPath = null; // used to cache file path for extracting files from a .jar file
+
+/**
+ * The doPageNavigation() function performs page navigations asynchronously,
+ * listens for specified events, and compares actual events with a list of
+ * expected events. When all expected events have occurred, an optional
+ * callback can be notified. The parameter passed to this function is an
+ * object with the following properties:
+ *
+ * uri: if !undefined, the browser will navigate to this uri
+ *
+ * back: if true, the browser will execute goBack()
+ *
+ * forward: if true, the browser will execute goForward()
+ *
+ * gotoIndex: if a number, the browser will execute gotoIndex() with
+ * the number as index
+ *
+ * reload: if true, the browser will execute reload()
+ *
+ * eventsToListenFor: an array containing one or more of the following event
+ * types to listen for: "pageshow", "pagehide", "onload",
+ * "onunload". If this property is undefined, only a
+ * single "pageshow" events will be listened for. If this
+ * property is explicitly empty, [], then no events will
+ * be listened for.
+ *
+ * expectedEvents: an array of one or more expectedEvent objects,
+ * corresponding to the events which are expected to be
+ * fired for this navigation. Each object has the
+ * following properties:
+ *
+ * type: one of the event type strings
+ * title (optional): the title of the window the
+ * event belongs to
+ * persisted (optional): the event's expected
+ * .persisted attribute
+ *
+ * This function will verify that events with the
+ * specified properties are fired in the same order as
+ * specified in the array. If .title or .persisted
+ * properties for an expectedEvent are undefined, those
+ * properties will not be verified for that particular
+ * event.
+ *
+ * This property is ignored if eventsToListenFor is
+ * undefined or [].
+ *
+ * preventBFCache: if true, an RTCPeerConnection will be added to the loaded
+ * page to prevent it from being bfcached. This property
+ * has no effect when eventsToListenFor is [].
+ *
+ * onNavComplete: a callback which is notified after all expected events
+ * have occurred, or after a timeout has elapsed. This
+ * callback is not notified if eventsToListenFor is [].
+ * onGlobalCreation: a callback which is notified when a DOMWindow is created
+ * (implemented by observing
+ * "content-document-global-created")
+ *
+ * There must be an expectedEvent object for each event of the types in
+ * eventsToListenFor which is triggered by this navigation. For example, if
+ * eventsToListenFor = [ "pagehide", "pageshow" ], then expectedEvents
+ * must contain an object for each pagehide and pageshow event which occurs as
+ * a result of this navigation.
+ */
+// eslint-disable-next-line complexity
+function doPageNavigation(params) {
+ // Parse the parameters.
+ let back = params.back ? params.back : false;
+ let forward = params.forward ? params.forward : false;
+ let gotoIndex = params.gotoIndex ? params.gotoIndex : false;
+ let reload = params.reload ? params.reload : false;
+ let uri = params.uri ? params.uri : false;
+ let eventsToListenFor =
+ typeof params.eventsToListenFor != "undefined"
+ ? params.eventsToListenFor
+ : ["pageshow"];
+ gExpectedEvents =
+ typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length
+ ? undefined
+ : params.expectedEvents;
+ gUnexpectedEvents =
+ typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length
+ ? undefined
+ : params.unexpectedEvents;
+ let preventBFCache =
+ typeof [params.preventBFCache] == "undefined"
+ ? false
+ : params.preventBFCache;
+ let waitOnly =
+ typeof params.waitForEventsOnly == "boolean" && params.waitForEventsOnly;
+
+ // Do some sanity checking on arguments.
+ let navigation = ["back", "forward", "gotoIndex", "reload", "uri"].filter(k =>
+ params.hasOwnProperty(k)
+ );
+ if (navigation.length > 1) {
+ throw new Error(`Can't specify both ${navigation[0]} and ${navigation[1]}`);
+ } else if (!navigation.length && !waitOnly) {
+ throw new Error(
+ "Must specify back or forward or gotoIndex or reload or uri"
+ );
+ }
+ if (params.onNavComplete && !eventsToListenFor.length) {
+ throw new Error("Can't use onNavComplete when eventsToListenFor == []");
+ }
+ if (params.preventBFCache && !eventsToListenFor.length) {
+ throw new Error("Can't use preventBFCache when eventsToListenFor == []");
+ }
+ if (params.preventBFCache && waitOnly) {
+ throw new Error("Can't prevent bfcaching when only waiting for events");
+ }
+ if (waitOnly && typeof params.onNavComplete == "undefined") {
+ throw new Error(
+ "Must specify onNavComplete when specifying waitForEventsOnly"
+ );
+ }
+ if (waitOnly && navigation.length) {
+ throw new Error(
+ "Can't specify a navigation type when using waitForEventsOnly"
+ );
+ }
+ for (let anEventType of eventsToListenFor) {
+ let eventFound = false;
+ if (anEventType == "pageshow" && !gExpectedEvents) {
+ eventFound = true;
+ }
+ if (gExpectedEvents) {
+ for (let anExpectedEvent of gExpectedEvents) {
+ if (anExpectedEvent.type == anEventType) {
+ eventFound = true;
+ }
+ }
+ }
+ if (gUnexpectedEvents) {
+ for (let anExpectedEventType of gUnexpectedEvents) {
+ if (anExpectedEventType == anEventType) {
+ eventFound = true;
+ }
+ }
+ }
+ if (!eventFound) {
+ throw new Error(
+ `Event type ${anEventType} is specified in ` +
+ "eventsToListenFor, but not in expectedEvents"
+ );
+ }
+ }
+
+ // If the test explicitly sets .eventsToListenFor to [], don't wait for any
+ // events.
+ gFinalEvent = !eventsToListenFor.length;
+
+ // Add observers as needed.
+ let observers = new Map();
+ if (params.hasOwnProperty("onGlobalCreation")) {
+ observers.set("content-document-global-created", params.onGlobalCreation);
+ }
+
+ // Add an event listener for each type of event in the .eventsToListenFor
+ // property of the input parameters, and add an observer for all the topics
+ // in the observers map.
+ let cleanup;
+ let useActor = TestWindow.getBrowser().isRemoteBrowser;
+ if (useActor) {
+ ChromeUtils.registerWindowActor("DocShellHelpers", {
+ parent: {
+ esModuleURI: ACTOR_MODULE_URI,
+ },
+ child: {
+ esModuleURI: ACTOR_MODULE_URI,
+ events: {
+ pageshow: { createActor: true, capture: true },
+ pagehide: { createActor: true, capture: true },
+ load: { createActor: true, capture: true },
+ unload: { createActor: true, capture: true },
+ visibilitychange: { createActor: true, capture: true },
+ },
+ observers: observers.keys(),
+ },
+ allFrames: true,
+ });
+ DocShellHelpersParent.eventsToListenFor = eventsToListenFor;
+ DocShellHelpersParent.observers = observers;
+
+ cleanup = () => {
+ DocShellHelpersParent.eventsToListenFor = null;
+ DocShellHelpersParent.observers = null;
+ ChromeUtils.unregisterWindowActor("DocShellHelpers");
+ };
+ } else {
+ for (let eventType of eventsToListenFor) {
+ dump("TEST: registering a listener for " + eventType + " events\n");
+ TestWindow.getBrowser().addEventListener(
+ eventType,
+ pageEventListener,
+ true
+ );
+ }
+ if (observers.size > 0) {
+ let observer = (_, topic) => {
+ observers.get(topic).call();
+ };
+ for (let topic of observers.keys()) {
+ Services.obs.addObserver(observer, topic);
+ }
+
+ // We only need to do cleanup for the observer, the event listeners will
+ // go away with the window.
+ cleanup = () => {
+ for (let topic of observers.keys()) {
+ Services.obs.removeObserver(observer, topic);
+ }
+ };
+ }
+ }
+
+ if (cleanup) {
+ // Register a cleanup function on domwindowclosed, to avoid contaminating
+ // other tests if we bail out early because of an error.
+ Services.ww.registerNotification(function windowClosed(
+ subject,
+ topic,
+ data
+ ) {
+ if (topic == "domwindowclosed" && subject == window) {
+ Services.ww.unregisterNotification(windowClosed);
+ cleanup();
+ }
+ });
+ }
+
+ // Perform the specified navigation.
+ if (back) {
+ gNavType = NAV_BACK;
+ TestWindow.getBrowser().goBack();
+ } else if (forward) {
+ gNavType = NAV_FORWARD;
+ TestWindow.getBrowser().goForward();
+ } else if (typeof gotoIndex == "number") {
+ gNavType = NAV_GOTOINDEX;
+ TestWindow.getBrowser().gotoIndex(gotoIndex);
+ } else if (uri) {
+ gNavType = NAV_URI;
+ BrowserTestUtils.startLoadingURIString(TestWindow.getBrowser(), uri);
+ } else if (reload) {
+ gNavType = NAV_RELOAD;
+ TestWindow.getBrowser().reload();
+ } else if (waitOnly) {
+ gNavType = NAV_NONE;
+ } else {
+ throw new Error("No valid navigation type passed to doPageNavigation!");
+ }
+
+ // If we're listening for events and there is an .onNavComplete callback,
+ // wait for all events to occur, and then call doPageNavigation_complete().
+ if (eventsToListenFor.length && params.onNavComplete) {
+ waitForTrue(
+ function () {
+ return gFinalEvent;
+ },
+ function () {
+ doPageNavigation_complete(
+ eventsToListenFor,
+ params.onNavComplete,
+ preventBFCache,
+ useActor,
+ cleanup
+ );
+ }
+ );
+ } else if (cleanup) {
+ cleanup();
+ }
+}
+
+/**
+ * Finish doPageNavigation(), by removing event listeners, adding an unload
+ * handler if appropriate, and calling the onNavComplete callback. This
+ * function is called after all the expected events for this navigation have
+ * occurred.
+ */
+function doPageNavigation_complete(
+ eventsToListenFor,
+ onNavComplete,
+ preventBFCache,
+ useActor,
+ cleanup
+) {
+ if (useActor) {
+ if (preventBFCache) {
+ let actor =
+ TestWindow.getBrowser().browsingContext.currentWindowGlobal.getActor(
+ "DocShellHelpers"
+ );
+ actor.sendAsyncMessage("docshell_helpers:preventBFCache");
+ }
+ } else {
+ // Unregister our event listeners.
+ dump("TEST: removing event listeners\n");
+ for (let eventType of eventsToListenFor) {
+ TestWindow.getBrowser().removeEventListener(
+ eventType,
+ pageEventListener,
+ true
+ );
+ }
+
+ // If the .preventBFCache property was set, add an RTCPeerConnection to
+ // prevent the page from being bfcached.
+ if (preventBFCache) {
+ let win = TestWindow.getWindow();
+ win.blockBFCache = new win.RTCPeerConnection();
+ }
+ }
+
+ if (cleanup) {
+ cleanup();
+ }
+
+ let uri = TestWindow.getBrowser().currentURI.spec;
+ if (preventBFCache) {
+ // Save the current uri in an array of uri's which shouldn't be
+ // stored in the bfcache, for later verification.
+ if (!(uri in gUrisNotInBFCache)) {
+ gUrisNotInBFCache.push(uri);
+ }
+ } else if (gNavType == NAV_URI) {
+ // If we're navigating to a uri and .preventBFCache was not
+ // specified, splice it out of gUrisNotInBFCache if it's there.
+ gUrisNotInBFCache.forEach(function (element, index, array) {
+ if (element == uri) {
+ array.splice(index, 1);
+ }
+ }, this);
+ }
+
+ // Notify the callback now that we're done.
+ onNavComplete.call();
+}
+
+function promisePageNavigation(params) {
+ if (params.hasOwnProperty("onNavComplete")) {
+ throw new Error(
+ "Can't use a onNavComplete completion callback with promisePageNavigation."
+ );
+ }
+ return new Promise(resolve => {
+ params.onNavComplete = resolve;
+ doPageNavigation(params);
+ });
+}
+
+/**
+ * Allows a test to wait for page navigation events, and notify a
+ * callback when they've all been received. This works exactly the
+ * same as doPageNavigation(), except that no navigation is initiated.
+ */
+function waitForPageEvents(params) {
+ params.waitForEventsOnly = true;
+ doPageNavigation(params);
+}
+
+function promisePageEvents(params) {
+ if (params.hasOwnProperty("onNavComplete")) {
+ throw new Error(
+ "Can't use a onNavComplete completion callback with promisePageEvents."
+ );
+ }
+ return new Promise(resolve => {
+ params.waitForEventsOnly = true;
+ params.onNavComplete = resolve;
+ doPageNavigation(params);
+ });
+}
+
+/**
+ * The event listener which listens for expectedEvents.
+ */
+function pageEventListener(
+ event,
+ originalTargetIsHTMLDocument = HTMLDocument.isInstance(event.originalTarget)
+) {
+ try {
+ dump(
+ "TEST: eventListener received a " +
+ event.type +
+ " event for page " +
+ event.originalTarget.title +
+ ", persisted=" +
+ event.persisted +
+ "\n"
+ );
+ } catch (e) {
+ // Ignore any exception.
+ }
+
+ // If this page shouldn't be in the bfcache because it was previously
+ // loaded with .preventBFCache, make sure that its pageshow event
+ // has .persisted = false, even if the test doesn't explicitly test
+ // for .persisted.
+ if (
+ event.type == "pageshow" &&
+ (gNavType == NAV_BACK ||
+ gNavType == NAV_FORWARD ||
+ gNavType == NAV_GOTOINDEX)
+ ) {
+ let uri = TestWindow.getBrowser().currentURI.spec;
+ if (uri in gUrisNotInBFCache) {
+ ok(
+ !event.persisted,
+ "pageshow event has .persisted = false, even " +
+ "though it was loaded with .preventBFCache previously\n"
+ );
+ }
+ }
+
+ if (typeof gUnexpectedEvents != "undefined") {
+ is(
+ gUnexpectedEvents.indexOf(event.type),
+ -1,
+ "Should not get unexpected event " + event.type
+ );
+ }
+
+ // If no expected events were specified, mark the final event as having been
+ // triggered when a pageshow event is fired; this will allow
+ // doPageNavigation() to return.
+ if (typeof gExpectedEvents == "undefined" && event.type == "pageshow") {
+ waitForNextPaint(function () {
+ gFinalEvent = true;
+ });
+ return;
+ }
+
+ // If there are explicitly no expected events, but we receive one, it's an
+ // error.
+ if (!gExpectedEvents.length) {
+ ok(false, "Unexpected event (" + event.type + ") occurred");
+ return;
+ }
+
+ // Grab the next expected event, and compare its attributes against the
+ // actual event.
+ let expected = gExpectedEvents.shift();
+
+ is(
+ event.type,
+ expected.type,
+ "A " +
+ expected.type +
+ " event was expected, but a " +
+ event.type +
+ " event occurred"
+ );
+
+ if (typeof expected.title != "undefined") {
+ ok(
+ originalTargetIsHTMLDocument,
+ "originalTarget for last " + event.type + " event not an HTMLDocument"
+ );
+ is(
+ event.originalTarget.title,
+ expected.title,
+ "A " +
+ event.type +
+ " event was expected for page " +
+ expected.title +
+ ", but was fired for page " +
+ event.originalTarget.title
+ );
+ }
+
+ if (typeof expected.persisted != "undefined") {
+ is(
+ event.persisted,
+ expected.persisted,
+ "The persisted property of the " +
+ event.type +
+ " event on page " +
+ event.originalTarget.location +
+ " had an unexpected value"
+ );
+ }
+
+ if ("visibilityState" in expected) {
+ is(
+ event.originalTarget.visibilityState,
+ expected.visibilityState,
+ "The visibilityState property of the document on page " +
+ event.originalTarget.location +
+ " had an unexpected value"
+ );
+ }
+
+ if ("hidden" in expected) {
+ is(
+ event.originalTarget.hidden,
+ expected.hidden,
+ "The hidden property of the document on page " +
+ event.originalTarget.location +
+ " had an unexpected value"
+ );
+ }
+
+ // If we're out of expected events, let doPageNavigation() return.
+ if (!gExpectedEvents.length) {
+ waitForNextPaint(function () {
+ gFinalEvent = true;
+ });
+ }
+}
+
+DocShellHelpersParent.eventListener = pageEventListener;
+
+/**
+ * End a test.
+ */
+function finish() {
+ // Work around bug 467960.
+ let historyPurged;
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let history = TestWindow.getBrowser().browsingContext?.sessionHistory;
+ history.purgeHistory(history.count);
+ historyPurged = Promise.resolve();
+ } else {
+ historyPurged = SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
+ let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory
+ .legacySHistory;
+ history.purgeHistory(history.count);
+ });
+ }
+
+ // If the test changed the value of max_total_viewers via a call to
+ // enableBFCache(), then restore it now.
+ if (typeof gOrigMaxTotalViewers != "undefined") {
+ Services.prefs.setIntPref(
+ "browser.sessionhistory.max_total_viewers",
+ gOrigMaxTotalViewers
+ );
+ }
+
+ // Close the test window and signal the framework that the test is done.
+ let opener = window.opener;
+ let SimpleTest = opener.wrappedJSObject.SimpleTest;
+
+ // Wait for the window to be closed before finishing the test
+ Services.ww.registerNotification(function observer(subject, topic, data) {
+ if (topic == "domwindowclosed") {
+ Services.ww.unregisterNotification(observer);
+ SimpleTest.waitForFocus(SimpleTest.finish, opener);
+ }
+ });
+
+ historyPurged.then(_ => {
+ window.close();
+ });
+}
+
+/**
+ * Helper function which waits until another function returns true, or until a
+ * timeout occurs, and then notifies a callback.
+ *
+ * Parameters:
+ *
+ * fn: a function which is evaluated repeatedly, and when it turns true,
+ * the onWaitComplete callback is notified.
+ *
+ * onWaitComplete: a callback which will be notified when fn() returns
+ * true, or when a timeout occurs.
+ *
+ * timeout: a timeout, in seconds or ms, after which waitForTrue() will
+ * fail an assertion and then return, even if the fn function never
+ * returns true. If timeout is undefined, waitForTrue() will never
+ * time out.
+ */
+function waitForTrue(fn, onWaitComplete, timeout) {
+ promiseTrue(fn, timeout).then(() => {
+ onWaitComplete.call();
+ });
+}
+
+function promiseTrue(fn, timeout) {
+ if (typeof timeout != "undefined") {
+ // If timeoutWait is less than 500, assume it represents seconds, and
+ // convert to ms.
+ if (timeout < 500) {
+ timeout *= 1000;
+ }
+ }
+
+ // Loop until the test function returns true, or until a timeout occurs,
+ // if a timeout is defined.
+ let intervalid, timeoutid;
+ let condition = new Promise(resolve => {
+ intervalid = setInterval(async () => {
+ if (await fn.call()) {
+ resolve();
+ }
+ }, 20);
+ });
+ if (typeof timeout != "undefined") {
+ condition = Promise.race([
+ condition,
+ new Promise((_, reject) => {
+ timeoutid = setTimeout(() => {
+ reject();
+ }, timeout);
+ }),
+ ]);
+ }
+ return condition
+ .finally(() => {
+ clearInterval(intervalid);
+ })
+ .then(() => {
+ clearTimeout(timeoutid);
+ });
+}
+
+function waitForNextPaint(cb) {
+ requestAnimationFrame(_ => requestAnimationFrame(cb));
+}
+
+function promiseNextPaint() {
+ return new Promise(resolve => {
+ waitForNextPaint(resolve);
+ });
+}
+
+/**
+ * Enable or disable the bfcache.
+ *
+ * Parameters:
+ *
+ * enable: if true, set max_total_viewers to -1 (the default); if false, set
+ * to 0 (disabled), if a number, set it to that specific number
+ */
+function enableBFCache(enable) {
+ // If this is the first time the test called enableBFCache(),
+ // store the original value of max_total_viewers, so it can
+ // be restored at the end of the test.
+ if (typeof gOrigMaxTotalViewers == "undefined") {
+ gOrigMaxTotalViewers = Services.prefs.getIntPref(
+ "browser.sessionhistory.max_total_viewers"
+ );
+ }
+
+ if (typeof enable == "boolean") {
+ if (enable) {
+ Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", -1);
+ } else {
+ Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0);
+ }
+ } else if (typeof enable == "number") {
+ Services.prefs.setIntPref(
+ "browser.sessionhistory.max_total_viewers",
+ enable
+ );
+ }
+}
+
+/*
+ * get http root for local tests. Use a single extractJarToTmp instead of
+ * extracting for each test.
+ * Returns a file://path if we have a .jar file
+ */
+function getHttpRoot() {
+ var location = window.location.href;
+ location = getRootDirectory(location);
+ var jar = getJar(location);
+ if (jar != null) {
+ if (gExtractedPath == null) {
+ var resolved = extractJarToTmp(jar);
+ gExtractedPath = resolved.path;
+ }
+ } else {
+ return null;
+ }
+ return "file://" + gExtractedPath + "/";
+}
+
+/**
+ * Returns the full HTTP url for a file in the mochitest docshell test
+ * directory.
+ */
+function getHttpUrl(filename) {
+ var root = getHttpRoot();
+ if (root == null) {
+ root = "http://mochi.test:8888/chrome/docshell/test/chrome/";
+ }
+ return root + filename;
+}
+
+/**
+ * A convenience object with methods that return the current test window,
+ * browser, and document.
+ */
+var TestWindow = {};
+TestWindow.getWindow = function () {
+ return document.getElementById("content").contentWindow;
+};
+TestWindow.getBrowser = function () {
+ return document.getElementById("content");
+};
+TestWindow.getDocument = function () {
+ return document.getElementById("content").contentDocument;
+};
diff --git a/docshell/test/chrome/file_viewsource_forbidden_in_iframe.html b/docshell/test/chrome/file_viewsource_forbidden_in_iframe.html
new file mode 100644
index 0000000000..fdecbbdfe1
--- /dev/null
+++ b/docshell/test/chrome/file_viewsource_forbidden_in_iframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test ifranes for view-source forbidden in iframe tests</title>
+</head>
+<body>
+ <iframe id="testIframe"></iframe>
+ <iframe id="refIframe"></iframe>
+</body>
+</html>
diff --git a/docshell/test/chrome/gen_template.pl b/docshell/test/chrome/gen_template.pl
new file mode 100644
index 0000000000..109d6161cd
--- /dev/null
+++ b/docshell/test/chrome/gen_template.pl
@@ -0,0 +1,39 @@
+#!/usr/bin/perl
+
+# This script makes docshell test case templates. It takes one argument:
+#
+# -b: a bugnumber
+#
+# For example, this command:
+#
+# perl gen_template.pl -b 303267
+#
+# Writes test case template files test_bug303267.xhtml and bug303267_window.xhtml
+# to the current directory.
+
+use FindBin;
+use Getopt::Long;
+GetOptions("b=i"=> \$bug_number);
+
+$template = "$FindBin::RealBin/test.template.txt";
+
+open(IN,$template) or die("Failed to open input file for reading.");
+open(OUT, ">>test_bug" . $bug_number . ".xhtml") or die("Failed to open output file for appending.");
+while((defined(IN)) && ($line = <IN>)) {
+ $line =~ s/{BUGNUMBER}/$bug_number/g;
+ print OUT $line;
+}
+close(IN);
+close(OUT);
+
+$template = "$FindBin::RealBin/window.template.txt";
+
+open(IN,$template) or die("Failed to open input file for reading.");
+open(OUT, ">>bug" . $bug_number . "_window.xhtml") or die("Failed to open output file for appending.");
+while((defined(IN)) && ($line = <IN>)) {
+ $line =~ s/{BUGNUMBER}/$bug_number/g;
+ print OUT $line;
+}
+close(IN);
+close(OUT);
+
diff --git a/docshell/test/chrome/generic.html b/docshell/test/chrome/generic.html
new file mode 100644
index 0000000000..569a78c05a
--- /dev/null
+++ b/docshell/test/chrome/generic.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <title>
+ generic page
+ </title>
+ </head>
+<body>
+<div id="div1" style="height: 1000px; border: thin solid black;">
+ A generic page which can be used any time a test needs to load an arbitrary page via http.
+ </div>
+</body>
+</html>
diff --git a/docshell/test/chrome/mozFrameType_window.xhtml b/docshell/test/chrome/mozFrameType_window.xhtml
new file mode 100644
index 0000000000..e5d0126d22
--- /dev/null
+++ b/docshell/test/chrome/mozFrameType_window.xhtml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<window title="Test mozFrameType attribute"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTests();">
+
+ <html:iframe id="normalFrame"/>
+ <html:iframe id="typeContentFrame" mozframetype="content"/>
+
+ <script type="application/javascript"><![CDATA[
+ function runTests() {
+ let opener = window.arguments[0];
+ let SimpleTest = opener.SimpleTest;
+
+ function getDocShellType(frame) {
+ return frame.contentWindow.docShell.itemType;
+ }
+
+ var normalFrame = document.getElementById("normalFrame");
+ var typeContentFrame = document.getElementById("typeContentFrame");
+
+ SimpleTest.is(getDocShellType(normalFrame), Ci.nsIDocShellTreeItem.typeChrome,
+ "normal iframe in chrome document is typeChrome");
+ SimpleTest.is(getDocShellType(typeContentFrame), Ci.nsIDocShellTreeItem.typeContent,
+ "iframe with mozFrameType='content' in chrome document is typeContent");
+
+ SimpleTest.executeSoon(function () {
+ // First focus the parent window and then close this one.
+ SimpleTest.waitForFocus(function() {
+ let ww = SpecialPowers.Services.ww;
+ ww.registerNotification(function windowObs(subject, topic, data) {
+ if (topic == "domwindowclosed") {
+ ww.unregisterNotification(windowObs);
+
+ // Don't start the next test synchronously!
+ SimpleTest.executeSoon(function() {
+ SimpleTest.finish();
+ });
+ }
+ });
+
+ window.close();
+ }, opener);
+ });
+ }
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/red.png b/docshell/test/chrome/red.png
new file mode 100644
index 0000000000..aa9ce25263
--- /dev/null
+++ b/docshell/test/chrome/red.png
Binary files differ
diff --git a/docshell/test/chrome/test.template.txt b/docshell/test/chrome/test.template.txt
new file mode 100644
index 0000000000..b7dd5e5c23
--- /dev/null
+++ b/docshell/test/chrome/test.template.txt
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}.xul
+-->
+<window title="Mozilla Bug {BUGNUMBER}"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <title>Test for Bug {BUGNUMBER}</title>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}">
+ Mozilla Bug {BUGNUMBER}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug {BUGNUMBER} **/
+
+SimpleTest.waitForExplicitFinish();
+window.open("bug{BUGNUMBER}_window.xul", "bug{BUGNUMBER}",
+ "chrome,width=600,height=600");
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_allowContentRetargeting.html b/docshell/test/chrome/test_allowContentRetargeting.html
new file mode 100644
index 0000000000..b6b830138f
--- /dev/null
+++ b/docshell/test/chrome/test_allowContentRetargeting.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runNextTest);
+
+var TEST_URL = "http://mochi.test:8888/tests/docshell/test/chrome/allowContentRetargeting.sjs";
+
+function runNextTest() {
+ var test = tests.shift();
+ if (!test) {
+ SimpleTest.finish();
+ return;
+ }
+ test();
+}
+
+var tests = [
+
+ // Set allowContentRetargeting = false, load a downloadable URL, verify the
+ // downloadable stops loading.
+ function basic() {
+ var iframe = insertIframe();
+ iframe.contentWindow.docShell.allowContentRetargeting = false;
+ loadIframe(iframe);
+ },
+
+ // Set allowContentRetargeting = false on parent docshell, load a downloadable
+ // URL, verify the downloadable stops loading.
+ function inherit() {
+ var docshell = window.docShell;
+ docshell.allowContentRetargeting = false;
+ loadIframe(insertIframe());
+ },
+];
+
+function insertIframe() {
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ return iframe;
+}
+
+function loadIframe(iframe) {
+ iframe.setAttribute("src", TEST_URL);
+ iframe.contentWindow.docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ addProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+}
+
+var progressListener = {
+ onStateChange(webProgress, req, flags, status) {
+ if (!(flags & Ci.nsIWebProgressListener.STATE_STOP))
+ return;
+ is(Components.isSuccessCode(status), false,
+ "Downloadable should have failed to load");
+ document.querySelector("iframe").remove();
+ runNextTest();
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]),
+};
+
+ </script>
+</head>
+<body>
+<p id="display">
+</p>
+</body>
+</html>
diff --git a/docshell/test/chrome/test_bug112564.xhtml b/docshell/test/chrome/test_bug112564.xhtml
new file mode 100644
index 0000000000..83a087b6d9
--- /dev/null
+++ b/docshell/test/chrome/test_bug112564.xhtml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=112564
+-->
+<window title="Mozilla Bug 112564"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=112564">Mozilla Bug 112564</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 112564 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug112564_window.xhtml", "bug112564",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug113934.xhtml b/docshell/test/chrome/test_bug113934.xhtml
new file mode 100644
index 0000000000..99b8ae253c
--- /dev/null
+++ b/docshell/test/chrome/test_bug113934.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=113934
+-->
+<window title="Mozilla Bug 113934"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=113934"
+ target="_blank">Mozilla Bug 396519</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ window.openDialog("bug113934_window.xhtml?content", "bug113934",
+ "chrome,width=800,height=800,noopener", window);
+ });
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/test_bug215405.xhtml b/docshell/test/chrome/test_bug215405.xhtml
new file mode 100644
index 0000000000..e9511a6ed1
--- /dev/null
+++ b/docshell/test/chrome/test_bug215405.xhtml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=215405
+-->
+<window title="Mozilla Bug 215405"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=215405">Mozilla Bug 215405</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 215405 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug215405_window.xhtml", "bug215405",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug293235.xhtml b/docshell/test/chrome/test_bug293235.xhtml
new file mode 100644
index 0000000000..4696c37557
--- /dev/null
+++ b/docshell/test/chrome/test_bug293235.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=293235.xul
+-->
+<window title="Mozilla Bug 293235"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=293235">
+ Mozilla Bug 293235</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 293235 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug293235_window.xhtml", "bug293235",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug294258.xhtml b/docshell/test/chrome/test_bug294258.xhtml
new file mode 100644
index 0000000000..8f61a6c299
--- /dev/null
+++ b/docshell/test/chrome/test_bug294258.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=294258.xul
+-->
+<window title="Mozilla Bug 294258"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=294258">
+ Mozilla Bug 294258</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 294258 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug294258_window.xhtml", "bug294258",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug298622.xhtml b/docshell/test/chrome/test_bug298622.xhtml
new file mode 100644
index 0000000000..8a154b5145
--- /dev/null
+++ b/docshell/test/chrome/test_bug298622.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=298622.xul
+-->
+<window title="Mozilla Bug 298622"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=298622">
+ Mozilla Bug 298622</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 298622 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug298622_window.xhtml", "bug298622",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug301397.xhtml b/docshell/test/chrome/test_bug301397.xhtml
new file mode 100644
index 0000000000..3da88ecf53
--- /dev/null
+++ b/docshell/test/chrome/test_bug301397.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=301397.xul
+-->
+<window title="Mozilla Bug 301397"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=301397">
+ Mozilla Bug 301397</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 301397 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug301397_window.xhtml", "bug301397",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug303267.xhtml b/docshell/test/chrome/test_bug303267.xhtml
new file mode 100644
index 0000000000..03af56d344
--- /dev/null
+++ b/docshell/test/chrome/test_bug303267.xhtml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=303267.xul
+-->
+<window title="Mozilla Bug 303267"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=303267">Mozilla Bug 303267</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.expectAssertions(0, 1);
+
+/** Test for Bug 303267 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug303267_window.xhtml", "bug303267",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug311007.xhtml b/docshell/test/chrome/test_bug311007.xhtml
new file mode 100644
index 0000000000..4601c6ec20
--- /dev/null
+++ b/docshell/test/chrome/test_bug311007.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=311007.xul
+-->
+<window title="Mozilla Bug 311007"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=311007">
+ Mozilla Bug 311007</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+if (navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+/** Test for Bug 311007 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug311007_window.xhtml", "bug311007",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug321671.xhtml b/docshell/test/chrome/test_bug321671.xhtml
new file mode 100644
index 0000000000..7855db6f13
--- /dev/null
+++ b/docshell/test/chrome/test_bug321671.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=321671.xul
+-->
+<window title="Mozilla Bug 321671"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=321671">
+ Mozilla Bug 321671</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 321671 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug321671_window.xhtml", "bug321671",
+ "chrome,width=600,height=600,scrollbars,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug360511.xhtml b/docshell/test/chrome/test_bug360511.xhtml
new file mode 100644
index 0000000000..99b9d180f4
--- /dev/null
+++ b/docshell/test/chrome/test_bug360511.xhtml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=360511.xul
+-->
+<window title="Mozilla Bug 360511"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+ </script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=360511">
+ Mozilla Bug 360511</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 360511 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug360511_window.xhtml", "bug360511",
+ "chrome,scrollbars,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug364461.xhtml b/docshell/test/chrome/test_bug364461.xhtml
new file mode 100644
index 0000000000..9bdc016ae7
--- /dev/null
+++ b/docshell/test/chrome/test_bug364461.xhtml
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=364461
+-->
+<window title="Mozilla Bug 364461"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=364461">Mozilla Bug 364461</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 364461 */
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({
+ "set":[["security.data_uri.block_toplevel_data_uri_navigations", false]]
+}, runTests);
+
+function runTests() {
+ window.openDialog("bug364461_window.xhtml", "bug364461",
+ "chrome,width=600,height=600,noopener", window);
+}
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug396519.xhtml b/docshell/test/chrome/test_bug396519.xhtml
new file mode 100644
index 0000000000..7c99bf4a32
--- /dev/null
+++ b/docshell/test/chrome/test_bug396519.xhtml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=396519
+-->
+<window title="Mozilla Bug 396519"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=396519"
+ target="_blank">Mozilla Bug 396519</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+ /** Test for Bug 396519 */
+
+ SimpleTest.waitForExplicitFinish();
+ window.openDialog("bug396519_window.xhtml", "bug396519",
+ "chrome,width=600,height=600,noopener", window);
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/test_bug396649.xhtml b/docshell/test/chrome/test_bug396649.xhtml
new file mode 100644
index 0000000000..8ac05eee85
--- /dev/null
+++ b/docshell/test/chrome/test_bug396649.xhtml
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=396649.xul
+-->
+<window title="Mozilla Bug 396649"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src=
+ "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+ </script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=396649">
+ Mozilla Bug 396649</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 396649 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug396649_window.xhtml", "bug396649",
+ "chrome,width=600,height=600,scrollbars,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug428288.html b/docshell/test/chrome/test_bug428288.html
new file mode 100644
index 0000000000..4697aed515
--- /dev/null
+++ b/docshell/test/chrome/test_bug428288.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=428288
+-->
+<head>
+ <title>Test for Bug 428288</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428288">Mozilla Bug 428288</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <iframe name="target"></iframe>
+ <a id="crashy" target="target" href="about:blank">crash me</a>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 428288 */
+
+function makeClick() {
+ var event = document.createEvent("MouseEvents");
+ event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ document.getElementById("crashy").dispatchEvent(event);
+ return true;
+}
+
+ok(makeClick(), "Crashes if bug 428288 is present");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/docshell/test/chrome/test_bug449778.xhtml b/docshell/test/chrome/test_bug449778.xhtml
new file mode 100644
index 0000000000..67e17164ea
--- /dev/null
+++ b/docshell/test/chrome/test_bug449778.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=449778
+-->
+<window title="Mozilla Bug 449778"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=449778"
+ target="_blank">Mozilla Bug 396519</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ window.openDialog("bug449778_window.xhtml", "bug449778",
+ "chrome,width=800,height=800,noopener", window);
+ });
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/test_bug449780.xhtml b/docshell/test/chrome/test_bug449780.xhtml
new file mode 100644
index 0000000000..43ed3ce25d
--- /dev/null
+++ b/docshell/test/chrome/test_bug449780.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=449780
+-->
+<window title="Mozilla Bug 449780"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=449780"
+ target="_blank">Mozilla Bug 396519</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ window.openDialog("bug449780_window.xhtml", "bug449780",
+ "chrome,width=800,height=800,noopener", window);
+ });
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/test_bug453650.xhtml b/docshell/test/chrome/test_bug453650.xhtml
new file mode 100644
index 0000000000..2283e29206
--- /dev/null
+++ b/docshell/test/chrome/test_bug453650.xhtml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=453650
+-->
+<window title="Mozilla Bug 453650"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ /** Test for Bug 453650 */
+ SimpleTest.waitForExplicitFinish();
+
+ var iter = runTests();
+ nextTest();
+
+ function* runTests() {
+ var iframe = document.createXULElement("iframe");
+ iframe.style.width = "300px";
+ iframe.style.height = "300px";
+ iframe.setAttribute("src", "data:text/html,<h1 id='h'>hello</h1>");
+
+ document.documentElement.appendChild(iframe);
+ yield whenLoaded(iframe);
+ info("iframe loaded");
+
+ var h1 = iframe.contentDocument.getElementById("h");
+ let myCallback = function() { h1.style.width = "400px"; };
+ info("Calling waitForInterruptibleReflow");
+ yield waitForInterruptibleReflow(iframe.docShell, myCallback);
+ info("got past top-level waitForInterruptibleReflow");
+
+ myCallback = function() { h1.style.width = "300px"; };
+ info("Calling waitForReflow");
+ waitForReflow(iframe.docShell, myCallback);
+ info("got past top-level waitForReflow");
+ yield is(300, h1.offsetWidth, "h1 has correct width");
+
+ SimpleTest.finish();
+ }
+
+ function waitForInterruptibleReflow(docShell,
+ callbackThatShouldTriggerReflow) {
+ waitForReflow(docShell, callbackThatShouldTriggerReflow, true);
+ }
+
+ function waitForReflow(docShell, callbackThatShouldTriggerReflow,
+ interruptible = false) {
+ info("Entering waitForReflow");
+ function done() {
+ info("Entering done (inside of waitForReflow)");
+
+ docShell.removeWeakReflowObserver(observer);
+ SimpleTest.executeSoon(nextTest);
+ }
+
+ var observer = {
+ reflow (start, end) {
+ info("Entering observer.reflow");
+ if (interruptible) {
+ ok(false, "expected interruptible reflow");
+ } else {
+ ok(true, "observed uninterruptible reflow");
+ }
+
+ info("times: " + start + ", " + end);
+ ok(start <= end, "reflow start time lower than end time");
+ done();
+ },
+
+ reflowInterruptible (start, end) {
+ info("Entering observer.reflowInterruptible");
+ if (!interruptible) {
+ ok(false, "expected uninterruptible reflow");
+ } else {
+ ok(true, "observed interruptible reflow");
+ }
+
+ info("times: " + start + ", " + end);
+ ok(start <= end, "reflow start time lower than end time");
+ done();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIReflowObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ info("waitForReflow is adding a reflow observer");
+ docShell.addWeakReflowObserver(observer);
+ callbackThatShouldTriggerReflow();
+ }
+
+ function whenLoaded(iframe) {
+ info("entering whenLoaded");
+ iframe.addEventListener("load", function() {
+ SimpleTest.executeSoon(nextTest);
+ }, { once: true });
+ }
+
+ function nextTest() {
+ info("entering nextTest");
+ iter.next();
+ }
+
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=453650"
+ target="_blank">Mozilla Bug 453650</a>
+ </body>
+</window>
diff --git a/docshell/test/chrome/test_bug454235.xhtml b/docshell/test/chrome/test_bug454235.xhtml
new file mode 100644
index 0000000000..d64f701f8e
--- /dev/null
+++ b/docshell/test/chrome/test_bug454235.xhtml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=454235
+-->
+<window title="Mozilla Bug 454235"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=454235"
+ target="_blank">Mozilla Bug 454235</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+ /** Test for Bug 454235 */
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(doTest);
+
+function doTest() {
+ var hiddenBrowser = document.getElementById("hiddenBrowser");
+
+ hiddenBrowser.contentWindow.focus();
+ ok(!hiddenBrowser.contentDocument.hasFocus(), "hidden browser is unfocusable");
+
+ SimpleTest.finish();
+}
+
+
+
+ ]]></script>
+ <box flex="1" style="visibility: hidden; border:5px black solid">
+ <browser style="border:5px blue solid" id="hiddenBrowser" src="bug454235-subframe.xhtml"/>
+ </box>
+</window>
diff --git a/docshell/test/chrome/test_bug456980.xhtml b/docshell/test/chrome/test_bug456980.xhtml
new file mode 100644
index 0000000000..d1741209c6
--- /dev/null
+++ b/docshell/test/chrome/test_bug456980.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=456980
+-->
+<window title="Mozilla Bug 456980"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=456980"
+ target="_blank">Mozilla Bug 396519</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ window.openDialog("bug113934_window.xhtml?chrome", "bug456980",
+ "chrome,width=800,height=800,noopener", window);
+ });
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/test_bug565388.xhtml b/docshell/test/chrome/test_bug565388.xhtml
new file mode 100644
index 0000000000..4da580c117
--- /dev/null
+++ b/docshell/test/chrome/test_bug565388.xhtml
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=565388
+-->
+<window title="Mozilla Bug 565388"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ /** Test for Bug 565388 */
+ SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var progressListener = {
+ add(docShell, callback) {
+ this.callback = callback;
+ this.docShell = docShell;
+ docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+ },
+
+ finish() {
+ this.docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ removeProgressListener(this);
+ this.callback();
+ },
+
+ onStateChange (webProgress, req, flags, status) {
+ if (req.name.startsWith("data:application/vnd.mozilla.xul")) {
+ if (flags & Ci.nsIWebProgressListener.STATE_STOP)
+ this.finish();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ }
+
+ var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
+ .getService(Ci.nsIPrincipal);
+ var webNav = SpecialPowers.Services.appShell.createWindowlessBrowser(true);
+ var docShell = webNav.docShell;
+ docShell.createAboutBlankDocumentViewer(systemPrincipal, systemPrincipal);
+ var win = docShell.docViewer.DOMDocument.defaultView;
+
+ progressListener.add(docShell, function(){
+ is(win.document.documentURI, "data:application/xhtml+xml;charset=utf-8,<window/>");
+ webNav.close();
+ SimpleTest.finish();
+ });
+
+ win.location = "data:application/xhtml+xml;charset=utf-8,<window/>";
+}
+
+addLoadEvent(function onLoad() {
+ test();
+});
+
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=565388"
+ target="_blank">Mozilla Bug 565388</a>
+ </body>
+</window>
diff --git a/docshell/test/chrome/test_bug582176.xhtml b/docshell/test/chrome/test_bug582176.xhtml
new file mode 100644
index 0000000000..5f189ae87b
--- /dev/null
+++ b/docshell/test/chrome/test_bug582176.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=582176.xul
+-->
+<window title="Mozilla Bug 582176"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=582176">
+ Mozilla Bug 582176</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 582176 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug582176_window.xhtml", "bug582176",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug608669.xhtml b/docshell/test/chrome/test_bug608669.xhtml
new file mode 100644
index 0000000000..f6a62b0802
--- /dev/null
+++ b/docshell/test/chrome/test_bug608669.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=608669
+-->
+<window title="Mozilla Bug 608669"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=608669"
+ target="_blank">Mozilla Bug 608669</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+/** Test for Bug 608669 */
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(nextTest);
+
+let gen = doTest();
+
+function nextTest() {
+ gen.next();
+}
+
+let chromeWindow = window.browsingContext.topChromeWindow;
+
+function* doTest() {
+ var notificationCount = 0;
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ is(aTopic, "chrome-document-global-created",
+ "correct topic");
+ is(aData, "null",
+ "correct data");
+ notificationCount++;
+ }
+ };
+
+ var os = SpecialPowers.Services.obs;
+ os.addObserver(observer, "chrome-document-global-created");
+ os.addObserver(observer, "content-document-global-created");
+
+ is(notificationCount, 0, "initial count");
+
+ // create a new window
+ var testWin = chromeWindow.open("", "bug 608669", "chrome,width=600,height=600");
+ testWin.x = "y";
+ is(notificationCount, 1, "after created window");
+
+ // Try loading in the window
+ testWin.location = "bug608669.xhtml";
+ chromeWindow.onmessage = nextTest;
+ yield undefined;
+ is(notificationCount, 1, "after first load");
+ is(testWin.x, "y", "reused window");
+
+ // Try loading again in the window
+ testWin.location = "bug608669.xhtml?x";
+ chromeWindow.onmessage = nextTest;
+ yield undefined;
+ is(notificationCount, 2, "after second load");
+ is("x" in testWin, false, "didn't reuse window");
+
+ chromeWindow.onmessage = null;
+
+ testWin.close();
+
+ os.removeObserver(observer, "chrome-document-global-created");
+ os.removeObserver(observer, "content-document-global-created");
+ SimpleTest.finish();
+}
+
+ ]]></script>
+</window>
diff --git a/docshell/test/chrome/test_bug662200.xhtml b/docshell/test/chrome/test_bug662200.xhtml
new file mode 100644
index 0000000000..19a386ba97
--- /dev/null
+++ b/docshell/test/chrome/test_bug662200.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=662200.xul
+-->
+<window title="Mozilla Bug 662200"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=662200">
+ Mozilla Bug 662200</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 662200 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug662200_window.xhtml", "bug662200",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug690056.xhtml b/docshell/test/chrome/test_bug690056.xhtml
new file mode 100644
index 0000000000..5f7a2b9534
--- /dev/null
+++ b/docshell/test/chrome/test_bug690056.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=690056
+-->
+<window title="Mozilla Bug 690056"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=690056"
+ target="_blank">Mozilla Bug 690056</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /** Test for Bug 690056 */
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug690056_window.xhtml", "bug690056",
+ "chrome,width=600,height=600,noopener", window);
+ ]]>
+ </script>
+</window>
diff --git a/docshell/test/chrome/test_bug789773.xhtml b/docshell/test/chrome/test_bug789773.xhtml
new file mode 100644
index 0000000000..196d40e12d
--- /dev/null
+++ b/docshell/test/chrome/test_bug789773.xhtml
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=789773
+-->
+<window title="Mozilla Bug 789773"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=789773"
+ target="_blank">Mozilla Bug 789773</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /* Test for Bug 789773.
+ *
+ * See comment 50 for the situation we're testing against here.
+ *
+ * Note that the failure mode of this test is to hang, and hang the browser on quit.
+ * This is an unfortunate occurance, but that's why we're testing it.
+ */
+ SimpleTest.waitForExplicitFinish();
+
+ const {AppConstants} = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ var calledListenerForBrowserChromeURL = false;
+ var testProgressListener = {
+ START_DOC: Ci.nsIWebProgressListener.STATE_START | Ci.nsIWebProgressListener.STATE_IS_DOCUMENT,
+ onStateChange(wp, req, stateFlags, status) {
+ let browserChromeFileName = AppConstants.BROWSER_CHROME_URL.split("/").reverse()[0];
+ if (req.name.includes(browserChromeFileName)) {
+ wp.DOMWindow; // Force the lazy creation of a DOM window.
+ calledListenerForBrowserChromeURL = true;
+ }
+ if (req.name.includes("mozilla.html") && (stateFlags & Ci.nsIWebProgressListener.STATE_STOP))
+ finishTest();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ }
+
+ // Add our progress listener
+ var webProgress = Cc['@mozilla.org/docloaderservice;1'].getService(Ci.nsIWebProgress);
+ webProgress.addProgressListener(testProgressListener, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
+
+ // Open the window.
+ var popup = window.open("about:mozilla", "_blank", "width=640,height=400");
+
+ // Wait for the window to load.
+ function finishTest() {
+ webProgress.removeProgressListener(testProgressListener);
+ ok(true, "Loaded the popup window without spinning forever in the event loop!");
+ ok(calledListenerForBrowserChromeURL, "Should have called the progress listener for browser.xhtml");
+ popup.close();
+ SimpleTest.finish();
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/docshell/test/chrome/test_bug846906.xhtml b/docshell/test/chrome/test_bug846906.xhtml
new file mode 100644
index 0000000000..74bfdf7036
--- /dev/null
+++ b/docshell/test/chrome/test_bug846906.xhtml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=846906
+-->
+<window title="Mozilla Bug 846906"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ /** Test for Bug 846906 */
+ SimpleTest.waitForExplicitFinish();
+
+ var appShellService = SpecialPowers.Services.appShell;
+ ok(appShellService, "Should be able to get app shell service");
+
+ var windowlessBrowser = appShellService.createWindowlessBrowser();
+ ok(windowlessBrowser, "Should be able to create windowless browser");
+
+ ok(windowlessBrowser instanceof Ci.nsIWindowlessBrowser,
+ "Windowless browser should implement nsIWindowlessBrowser");
+
+ var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
+ ok(webNavigation, "Windowless browser should implement nsIWebNavigation");
+
+ var interfaceRequestor = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor);
+ ok(interfaceRequestor, "Should be able to query interface requestor interface");
+
+ var docShell = windowlessBrowser.docShell;
+ ok(docShell, "Should be able to get doc shell interface");
+
+ var document = webNavigation.document;
+ ok(document, "Should be able to get document");
+
+ var iframe = document.createXULElement("iframe");
+ ok(iframe, "Should be able to create iframe");
+
+ iframe.onload = function () {
+ ok(true, "Should receive initial onload event");
+
+ iframe.onload = function () {
+ ok(true, "Should receive onload event");
+
+ var contentDocument = iframe.contentDocument;
+ ok(contentDocument, "Should be able to get content document");
+
+ var div = contentDocument.getElementById("div1");
+ ok(div, "Should be able to get element by id");
+
+ var rect = div.getBoundingClientRect();
+ ok(rect, "Should be able to get bounding client rect");
+
+ // xxx: can we do better than hardcoding these values here?
+ is(rect.width, 1024);
+ is(rect.height, 768);
+
+ windowlessBrowser.close();
+
+ // Once the browser is closed, nsIWebNavigation and
+ // nsIInterfaceRequestor methods should no longer be accessible.
+ try {
+ windowlessBrowser.getInterface(Ci.nsIDocShell);
+ ok(false);
+ } catch (e) {
+ is(e.result, Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ try {
+ windowlessBrowser.document;
+ ok(false);
+ } catch (e) {
+ is(e.result, Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ SimpleTest.finish();
+ };
+ iframe.setAttribute("src", "http://mochi.test:8888/chrome/docshell/test/chrome/bug846906.html");
+ };
+ document.documentElement.appendChild(iframe);
+
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=846906"
+ target="_blank">Mozilla Bug 846906</a>
+ </body>
+</window>
diff --git a/docshell/test/chrome/test_bug89419.xhtml b/docshell/test/chrome/test_bug89419.xhtml
new file mode 100644
index 0000000000..848982180f
--- /dev/null
+++ b/docshell/test/chrome/test_bug89419.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=89419.xul
+-->
+<window title="Mozilla Bug 89419"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=89419">
+ Mozilla Bug 89419</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 89419 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug89419_window.xhtml", "bug89419",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_bug909218.html b/docshell/test/chrome/test_bug909218.html
new file mode 100644
index 0000000000..bcbcc176eb
--- /dev/null
+++ b/docshell/test/chrome/test_bug909218.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(test);
+
+// The default flags we will stick on the docShell - every request made by the
+// docShell should include those flags.
+const TEST_FLAGS = Ci.nsIRequest.LOAD_ANONYMOUS |
+ Ci.nsIRequest.LOAD_BYPASS_CACHE |
+ Ci.nsIRequest.INHIBIT_CACHING;
+
+var TEST_URL = "http://mochi.test:8888/chrome/docshell/test/chrome/bug909218.html";
+
+// These are the requests we expect to see loading TEST_URL into our iframe.
+
+// The test entry-point. The basic outline is:
+// * Create an iframe and set defaultLoadFlags on its docShell.
+// * Add a web progress listener to observe each request as the iframe is
+// loaded, and check that each request has the flags we specified.
+// * Load our test URL into the iframe and wait for the load to complete.
+function test() {
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ var docShell = iframe.contentWindow.docShell;
+ // Add our progress listener - when it notices the top-level document is
+ // complete, the test will end.
+ RequestWatcher.init(docShell, SimpleTest.finish);
+ // Set the flags we care about, then load our test URL.
+ docShell.defaultLoadFlags = TEST_FLAGS;
+ iframe.setAttribute("src", TEST_URL);
+}
+
+// an nsIWebProgressListener that checks all requests made by the docShell
+// have the flags we expect.
+var RequestWatcher = {
+ init(docShell, callback) {
+ this.callback = callback;
+ this.docShell = docShell;
+ docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+ // These are the requests we expect to see - initialize each to have a
+ // count of zero.
+ this.requestCounts = {};
+ for (var url of [
+ TEST_URL,
+ // content loaded by the above test html.
+ "http://mochi.test:8888/chrome/docshell/test/chrome/bug909218.js",
+ "http://mochi.test:8888/tests/SimpleTest/test.css",
+ "http://mochi.test:8888/tests/docshell/test/chrome/red.png",
+ // the content of an iframe in the test html.
+ "http://mochi.test:8888/chrome/docshell/test/chrome/generic.html",
+ ]) {
+ this.requestCounts[url] = 0;
+ }
+ },
+
+ // Finalize the test after we detect a completed load. We check we saw the
+ // correct requests and make a callback to exit.
+ finalize() {
+ ok(Object.keys(this.requestCounts).length, "we expected some requests");
+ for (var url in this.requestCounts) {
+ var count = this.requestCounts[url];
+ // As we are looking at all request states, we expect more than 1 for
+ // each URL - 0 or 1 would imply something went wrong - >1 just means
+ // multiple states for each request were recorded, which we don't care
+ // about (we do care they all have the correct flags though - but we
+ // do that in onStateChange)
+ ok(count > 1, url + " saw " + count + " requests");
+ }
+ this.docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ removeProgressListener(this);
+ this.callback();
+ },
+
+ onStateChange(webProgress, req, flags, status) {
+ // We are checking requests - if there isn't one, ignore it.
+ if (!req) {
+ return;
+ }
+ // We will usually see requests for 'about:document-onload-blocker' not
+ // have the flag, so we just ignore them.
+ // We also see, eg, resource://gre-resources/loading-image.png, so
+ // skip resource:// URLs too.
+ // We may also see, eg, chrome://global/skin/icons/chevron.svg, so
+ // skip chrome:// URLs too.
+ if (req.name.startsWith("about:") || req.name.startsWith("resource:") ||
+ req.name.startsWith("chrome:") || req.name.startsWith("documentchannel:")) {
+ return;
+ }
+ is(req.loadFlags & TEST_FLAGS, TEST_FLAGS, "request " + req.name + " has the expected flags");
+ this.requestCounts[req.name] += 1;
+ var stopFlags = Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ if (req.name == TEST_URL && (flags & stopFlags) == stopFlags) {
+ this.finalize();
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+</script>
+</head>
+</html>
diff --git a/docshell/test/chrome/test_bug92598.xhtml b/docshell/test/chrome/test_bug92598.xhtml
new file mode 100644
index 0000000000..1b89e3eeb3
--- /dev/null
+++ b/docshell/test/chrome/test_bug92598.xhtml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=92598
+-->
+<window title="Mozilla Bug 92598"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=92598">Mozilla Bug 92598</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 92598 */
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug92598_window.xhtml", "bug92598",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_docRedirect.sjs b/docshell/test/chrome/test_docRedirect.sjs
new file mode 100644
index 0000000000..4050eb06d7
--- /dev/null
+++ b/docshell/test/chrome/test_docRedirect.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ response.setHeader("Location", "http://example.org/");
+ response.write("Hello world!");
+}
diff --git a/docshell/test/chrome/test_docRedirect.xhtml b/docshell/test/chrome/test_docRedirect.xhtml
new file mode 100644
index 0000000000..56fdc1c5d2
--- /dev/null
+++ b/docshell/test/chrome/test_docRedirect.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1342989
+-->
+<window title="Mozilla Bug 1342989"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ const WEB_PROGRESS_LISTENER_FLAGS =
+ Object.keys(Ci.nsIWebProgressListener).filter(
+ propName => propName.startsWith("STATE_")
+ );
+
+ function bitFlagsToNames(flags, knownNames, intf) {
+ return knownNames.map( (F) => {
+ return (flags & intf[F]) ? F : undefined;
+ }).filter( (s) => !!s );
+ }
+
+ var progressListener = {
+ add(docShell, callback) {
+ this.callback = callback;
+ this.docShell = docShell;
+ docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL);
+ },
+
+ finish(success) {
+ this.docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ removeProgressListener(this);
+ this.callback(success);
+ },
+
+ onStateChange (webProgress, req, flags, status) {
+ if (!(flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT) &&
+ !(flags & Ci.nsIWebProgressListener.STATE_IS_REDIRECTED_DOCUMENT))
+ return;
+
+ var channel = req.QueryInterface(Ci.nsIChannel);
+
+ if (flags & Ci.nsIWebProgressListener.STATE_IS_REDIRECTED_DOCUMENT) {
+ SimpleTest.is(channel.URI.host, "example.org",
+ "Should be redirected to example.org (see test_docRedirect.sjs)");
+ this.finish(true);
+ }
+
+ // Fail in case we didn't receive document redirection event.
+ if (flags & Ci.nsIWebProgressListener.STATE_STOP)
+ this.finish(false);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ }
+
+ var webNav = SpecialPowers.Services.appShell.createWindowlessBrowser(true);
+ let docShell = webNav.docShell;
+ let system = Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal);
+ docShell.createAboutBlankDocumentViewer(system, system);
+
+ progressListener.add(docShell, function(success) {
+ webNav.close();
+ SimpleTest.is(success, true, "Received document redirect event");
+ SimpleTest.finish();
+ });
+
+ var win = docShell.docViewer.DOMDocument.defaultView;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ win.location = "http://example.com/chrome/docshell/test/chrome/test_docRedirect.sjs"
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1342989"
+ target="_blank">Mozilla Bug 1342989</a>
+ </body>
+</window>
diff --git a/docshell/test/chrome/test_mozFrameType.xhtml b/docshell/test/chrome/test_mozFrameType.xhtml
new file mode 100644
index 0000000000..f9740a761b
--- /dev/null
+++ b/docshell/test/chrome/test_mozFrameType.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=769771
+-->
+<window title="Test mozFrameType attribute"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+if (navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+/** Test for Bug 769771 */
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function () {
+ window.openDialog("mozFrameType_window.xhtml", "mozFrameType",
+ "chrome,width=600,height=600,noopener", window);
+});
+
+]]>
+</script>
+
+</window>
diff --git a/docshell/test/chrome/test_open_and_immediately_close_opener.html b/docshell/test/chrome/test_open_and_immediately_close_opener.html
new file mode 100644
index 0000000000..bb5f6f054d
--- /dev/null
+++ b/docshell/test/chrome/test_open_and_immediately_close_opener.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for Bug 1702678</title>
+ <meta charset="utf-8">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1702678">Mozilla Bug 1702678</a>
+
+<script type="application/javascript">
+"use strict";
+
+const HTML = `
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ // We need to queue a promise reaction job whilch will close the window
+ // during the nested event loop spun up by the window opening code.
+ Promise.resolve().then(() => {
+ window.close();
+ });
+ window.open("data:text/html,Hello");
+ <\/script>
+</head>
+</html>
+`;
+
+add_task(async function() {
+ // This bug only manifests when opening tabs in new windows.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.link.open_newwindow", 2]],
+ });
+
+ // Create a window in a new BrowsingContextGroup so that it will be the last
+ // window in the group when it closes, and the group will be destroyed.
+ window.open(`data:text/html,${encodeURIComponent(HTML)}`, "", "noopener");
+
+ // Make a few trips through the event loop to ensure we've had a chance to
+ // open and close the relevant windows.
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ ok(true, "We didn't crash");
+});
+</script>
+
+</body>
+</html>
+
diff --git a/docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml b/docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml
new file mode 100644
index 0000000000..0cc45c7821
--- /dev/null
+++ b/docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml
@@ -0,0 +1,159 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=624883
+-->
+<window title="Mozilla Bug 624883"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=624883"
+ target="_blank">Mozilla Bug 624883</a>
+ </body>
+
+ <!-- test code goes here -->
+ <iframe type="content" onload="startTest()" src="file_viewsource_forbidden_in_iframe.html"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ // We create a promise that will resolve with the error message
+ // on a network error page load and reject on any other load.
+ function createNetworkErrorMessagePromise(frame) {
+ return new Promise(function(resolve, reject) {
+
+ // Error pages do not fire "load" events, so use a progressListener.
+ var originalDocumentURI = frame.contentDocument.documentURI;
+ var progressListener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // Make sure nothing other than an error page is loaded.
+ if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) {
+ reject("location change was not to an error page");
+ }
+ },
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Wait until the documentURI changes (from about:blank) this should
+ // be the error page URI.
+ var documentURI = frame.contentDocument.documentURI;
+ if (documentURI == originalDocumentURI) {
+ return;
+ }
+
+ aWebProgress.removeProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_ALL);
+ var matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI);
+ if (!matchArray) {
+ reject("no network error message found in URI")
+ return;
+ }
+
+ var errorMsg = matchArray[1];
+ resolve(decodeURIComponent(errorMsg));
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
+ "nsISupportsWeakReference"])
+ };
+
+ frame.contentWindow.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .addProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION |
+ Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
+ });
+ }
+
+ function startTest() {
+ // Get a reference message that we know will be an unknown protocol message,
+ // so we can use it for comparisons in the test cases.
+ var refIframe = window[0].document.getElementById("refIframe");
+ var refErrorPromise = createNetworkErrorMessagePromise(refIframe);
+
+ refErrorPromise.then(
+ function(msg) {
+ window.refErrorMsg = msg;
+ var testIframe = window[0].document.getElementById("testIframe");
+
+ // Run test cases on load of "about:blank", so that the URI always changes
+ // and we can detect this in our Promise.
+ testIframe.onload = runNextTestCase;
+ testIframe.src = "about:blank";
+ },
+ function(reason) {
+ ok(false, "Could not get reference error message", reason);
+ SimpleTest.finish();
+ })
+ .catch(function(e) {
+ ok(false, "Unexpected exception thrown getting reference error message", e);
+ });
+
+ refIframe.src = "wibble://example.com";
+ }
+
+ function runTestCase(testCase) {
+ var testIframe = window[0].document.getElementById("testIframe");
+ var expectedErrorMsg = window.refErrorMsg.replace("wibble", testCase.expectedProtocolList);
+
+ var testErrorPromise = createNetworkErrorMessagePromise(testIframe);
+ testErrorPromise.then(
+ function(actualErrorMsg) {
+ is(actualErrorMsg, expectedErrorMsg, testCase.desc);
+ testIframe.src = "about:blank";
+ },
+ function(reason) {
+ ok(false, testCase.desc, reason);
+ testIframe.src = "about:blank";
+ })
+ .catch(function(e) {
+ ok(false, testCase.desc + " - unexpected exception thrown", e);
+ });
+
+ testIframe.src = testCase.protocols + "://example.com/!/";
+ }
+
+ var testCaseIndex = -1;
+ let testCases = [
+ {
+ desc: "Test 1: view-source should not be allowed in an iframe",
+ protocols: "view-source:http",
+ expectedProtocolList: "view-source, http"
+ },
+ {
+ desc: "Test 2: jar:view-source should not be allowed in an iframe",
+ protocols: "jar:view-source:http",
+ expectedProtocolList: "jar, view-source, http"
+ },
+ {
+ desc: "Test 3: if invalid protocol first should report before view-source",
+ protocols: "wibble:view-source:http",
+ // Nothing after the invalid protocol gets set as a proper nested URI,
+ // so the list stops there.
+ expectedProtocolList: "wibble"
+ },
+ {
+ desc: "Test 4: if view-source first should report before invalid protocol",
+ protocols: "view-source:wibble:http",
+ expectedProtocolList: "view-source, wibble"
+ }
+ ];
+
+ function runNextTestCase() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ runTestCase(testCases[testCaseIndex]);
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/docshell/test/chrome/window.template.txt b/docshell/test/chrome/window.template.txt
new file mode 100644
index 0000000000..4c520dc075
--- /dev/null
+++ b/docshell/test/chrome/window.template.txt
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="{BUGNUMBER}Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="setTimeout(nextTest,0);"
+ title="bug {BUGNUMBER} test">
+
+ <script type="application/javascript"
+ src="docshell_helpers.js">
+ </script>
+
+ <script type="application/javascript"><![CDATA[
+
+ // Define the generator-iterator for the tests.
+ var tests = testIterator();
+
+ ////
+ // Execute the next test in the generator function.
+ //
+ function nextTest() {
+ tests.next();
+ }
+
+ ////
+ // Generator function for test steps for bug {BUGNUMBER}:
+ // Description goes here.
+ //
+ function testIterator()
+ {
+ // Test steps go here. See bug303267_window.xhtml for an example.
+
+ // Tell the framework the test is finished. Include the final 'yield'
+ // statement to prevent a StopIteration exception from being thrown.
+ finish();
+ yield undefined;
+ }
+
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/docshell/test/iframesandbox/file_child_navigation_by_location.html b/docshell/test/iframesandbox/file_child_navigation_by_location.html
new file mode 100644
index 0000000000..7365bed81f
--- /dev/null
+++ b/docshell/test/iframesandbox/file_child_navigation_by_location.html
@@ -0,0 +1 @@
+<script>function onNav() { parent.parent.postMessage("childIframe", "*"); } window.onload = onNav; window.onhashchange = onNav;</script>
diff --git a/docshell/test/iframesandbox/file_marquee_event_handlers.html b/docshell/test/iframesandbox/file_marquee_event_handlers.html
new file mode 100644
index 0000000000..13ee31ddb7
--- /dev/null
+++ b/docshell/test/iframesandbox/file_marquee_event_handlers.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test marquee attribute event handlers in iframe sandbox</title>
+</head>
+<body>
+ <!-- Note that the width here is slightly longer than the contents, to make
+ sure we bounce and finish very quickly. -->
+ <marquee loop="2" width="145" behavior="alternate" truespeed scrolldelay="1"
+ onstart="parent.postMessage(window.name + ' marquee onstart', '*');"
+ onbounce="parent.postMessage(window.name + ' marquee onbounce', '*');"
+ onfinish="parent.postMessage(window.name + ' marquee onfinish', '*');">
+ Will bounce and finish
+ </marquee>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html
new file mode 100644
index 0000000000..ad24c0f242
--- /dev/null
+++ b/docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for other auxiliary navigation by location tests</title>
+<script>
+ function onNav() {
+ opener.postMessage(window.name, "*");
+ }
+
+ window.onload = onNav;
+ window.onhashchange = onNav;
+</script>
+</head>
+</html>
diff --git a/docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html
new file mode 100644
index 0000000000..978980df25
--- /dev/null
+++ b/docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for our auxiliary navigation by location tests</title>
+<script>
+ function onNav() {
+ opener.parent.postMessage(window.name, "*");
+ }
+
+ window.onload = onNav;
+ window.onhashchange = onNav;
+</script>
+</head>
+</html>
diff --git a/docshell/test/iframesandbox/file_parent_navigation_by_location.html b/docshell/test/iframesandbox/file_parent_navigation_by_location.html
new file mode 100644
index 0000000000..9a2e95fad0
--- /dev/null
+++ b/docshell/test/iframesandbox/file_parent_navigation_by_location.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for parent navigation by location tests</title>
+<script>
+ function onNav() {
+ parent.postMessage(window.name, "*");
+ }
+
+ window.onload = onNav;
+ window.onhashchange = onNav;
+</script>
+</head>
+<body>
+ <iframe name="childIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/file_sibling_navigation_by_location.html b/docshell/test/iframesandbox/file_sibling_navigation_by_location.html
new file mode 100644
index 0000000000..51a52bb8eb
--- /dev/null
+++ b/docshell/test/iframesandbox/file_sibling_navigation_by_location.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for sibling navigation by location tests</title>
+<script>
+ function onNav() {
+ parent.postMessage(window.name, "*");
+ }
+
+ window.onload = onNav;
+ window.onhashchange = onNav;
+</script>
+</head>
+</html>
diff --git a/docshell/test/iframesandbox/file_top_navigation_by_location.html b/docshell/test/iframesandbox/file_top_navigation_by_location.html
new file mode 100644
index 0000000000..194430f38c
--- /dev/null
+++ b/docshell/test/iframesandbox/file_top_navigation_by_location.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for top navigation by location tests</title>
+<script>
+ function onNav() {
+ opener.postMessage(window.name, "*");
+ }
+
+ window.onload = onNav;
+ window.onhashchange = onNav;
+</script>
+</head>
+<body>
+ <iframe name="if1" sandbox="allow-scripts allow-same-origin"></iframe>
+ <iframe name="if2" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe>
+ <iframe name="if3" sandbox="allow-scripts allow-top-navigation"></iframe>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html b/docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html
new file mode 100644
index 0000000000..9a26bc80db
--- /dev/null
+++ b/docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for top navigation by location tests</title>
+<script>
+ function onBlock() {
+ opener.postMessage({ name: window.name, blocked: true }, "*");
+ }
+
+ function onNav() {
+ opener.postMessage({ name: window.name, blocked: false }, "*");
+ }
+
+ function setOwnHref() {
+ // eslint-disable-next-line no-self-assign
+ location.href = location.href;
+ }
+
+ window.onload = onNav;
+</script>
+</head>
+<body>
+ <iframe name="if1" sandbox="allow-scripts allow-same-origin"></iframe>
+ <iframe name="if2" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/file_top_navigation_by_user_activation.html b/docshell/test/iframesandbox/file_top_navigation_by_user_activation.html
new file mode 100644
index 0000000000..8454f1fac4
--- /dev/null
+++ b/docshell/test/iframesandbox/file_top_navigation_by_user_activation.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test window for top navigation with user activation</title>
+<script>
+window.onload = () => {
+ opener.postMessage("READY", "*");
+};
+
+window.onhashchange = () => {
+ opener.postMessage("NAVIGATED", "*");
+};
+
+window.onmessage = (e) => {
+ if (e.data == "CLICK" || e.data == "SCRIPT") {
+ frames[0].postMessage([e.data, location.href + "#hash"], "*");
+ } else {
+ opener.postMessage(e.data, "*");
+ }
+};
+</script>
+</head>
+<body>
+ <iframe sandbox="allow-scripts allow-top-navigation-by-user-activation" src="http://example.org/tests/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html"></iframe>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html b/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html
new file mode 100644
index 0000000000..b775579f28
--- /dev/null
+++ b/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<title>Test window for top navigation with user activation</title>
+<script>
+function navigate(aURL) {
+ try {
+ top.location.href = aURL;
+ } catch (e) {
+ top.postMessage("BLOCKED", "*");
+ }
+}
+
+window.onmessage = (e) => {
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ let [command, url] = e.data;
+ if (command == "CLICK") {
+ let button = document.querySelector("button");
+ button.addEventListener("click", () => {
+ navigate(url);
+ }, { once: true });
+ synthesizeMouseAtCenter(button, {});
+ } else if (command == "SCRIPT") {
+ navigate(url);
+ }
+};
+</script>
+</head>
+<body><button>Click</button></body>
+</html>
diff --git a/docshell/test/iframesandbox/mochitest.toml b/docshell/test/iframesandbox/mochitest.toml
new file mode 100644
index 0000000000..a8bf4b1d72
--- /dev/null
+++ b/docshell/test/iframesandbox/mochitest.toml
@@ -0,0 +1,43 @@
+[DEFAULT]
+support-files = [
+ "file_child_navigation_by_location.html",
+ "file_marquee_event_handlers.html",
+ "file_other_auxiliary_navigation_by_location.html",
+ "file_our_auxiliary_navigation_by_location.html",
+ "file_parent_navigation_by_location.html",
+ "file_sibling_navigation_by_location.html",
+ "file_top_navigation_by_location.html",
+ "file_top_navigation_by_location_exotic.html",
+]
+
+["test_child_navigation_by_location.html"]
+
+["test_marquee_event_handlers.html"]
+skip-if = ["true"] # Bug 1455996
+
+["test_other_auxiliary_navigation_by_location.html"]
+tags = "openwindow"
+
+["test_our_auxiliary_navigation_by_location.html"]
+tags = "openwindow"
+
+["test_parent_navigation_by_location.html"]
+tags = "openwindow"
+
+["test_sibling_navigation_by_location.html"]
+tags = "openwindow"
+
+["test_top_navigation_by_location.html"]
+
+["test_top_navigation_by_location_exotic.html"]
+
+["test_top_navigation_by_user_activation.html"]
+support-files = [
+ "file_top_navigation_by_user_activation.html",
+ "file_top_navigation_by_user_activation_iframe.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
diff --git a/docshell/test/iframesandbox/test_child_navigation_by_location.html b/docshell/test/iframesandbox/test_child_navigation_by_location.html
new file mode 100644
index 0000000000..383320a02b
--- /dev/null
+++ b/docshell/test/iframesandbox/test_child_navigation_by_location.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox child navigation by location tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ var testDataUri = "file_child_navigation_by_location.html";
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data != "childIframe") {
+ ok(false, "event.data: got '" + event.data + "', expected 'childIframe'");
+ }
+ ok(!testCase.shouldBeBlocked, testCase.desc + " - child navigation was NOT blocked");
+ runNextTest();
+ };
+ try {
+ window.parentIframe.eval(testCase.script);
+ } catch (e) {
+ ok(testCase.shouldBeBlocked, testCase.desc + " - " + e.message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: cross origin child location.replace should NOT be blocked",
+ script: "window['crossOriginChildIframe'].location.replace(\"" + testDataUri + "\")",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 2: cross origin child location.assign should be blocked",
+ script: "window['crossOriginChildIframe'].location.assign(\"" + testDataUri + "\")",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 3: same origin child location.assign should NOT be blocked",
+ script: "window['sameOriginChildIframe'].location.assign(\"" + testDataUri + "\")",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 4: cross origin child location.href should NOT be blocked",
+ script: "window['crossOriginChildIframe'].location.href = \"" + testDataUri + "\"",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 5: cross origin child location.hash should be blocked",
+ script: "window['crossOriginChildIframe'].location.hash = 'wibble'",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 6: same origin child location.hash should NOT be blocked",
+ script: "window['sameOriginChildIframe'].location.hash = 'wibble'",
+ shouldBeBlocked: false,
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ addLoadEvent(runNextTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+
+<iframe name="parentIframe" sandbox="allow-scripts allow-same-origin" srcdoc="<iframe name='sameOriginChildIframe'></iframe><iframe name='crossOriginChildIframe' sandbox='allow-scripts'></iframe>"</iframe>
+
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_marquee_event_handlers.html b/docshell/test/iframesandbox/test_marquee_event_handlers.html
new file mode 100644
index 0000000000..80added8ab
--- /dev/null
+++ b/docshell/test/iframesandbox/test_marquee_event_handlers.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1277475
+html5 sandboxed iframe should not run marquee attribute event handlers without allow-scripts
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 1277475 - html5 sandboxed iframe should not run marquee attribute event handlers without allow-scripts</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1277475">Mozilla Bug 1277475</a>
+<p id="display"></p>
+<div id="content">Tests for Bug 1277475</div>
+
+<iframe id="if1" name="if1" src="file_marquee_event_handlers.html"
+ sandbox="allow-same-origin allow-forms allow-top-navigation allow-pointer-lock allow-orientation-lock allow-popups allow-modals allow-popups-to-escape-sandbox">
+</iframe>
+
+<iframe id="if2" name="if2" src="file_marquee_event_handlers.html"
+ sandbox="allow-scripts"></iframe>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ var expectedMessages = new Set();
+ var numberOfMessagesExpected = 4;
+ var unexpectedMessages = new Set();
+
+ window.onmessage = function(event) {
+ info(event.data + " message received");
+ if (event.data.startsWith("if2") || event.data == "if1 chaser") {
+ expectedMessages.add(event.data);
+ if (expectedMessages.size == numberOfMessagesExpected) {
+ checkRecievedMessages();
+ }
+ } else {
+ unexpectedMessages.add(event.data);
+ }
+ };
+
+ function checkRecievedMessages() {
+ // Check the expected messages explicitly as a cross-check.
+ ok(expectedMessages.has("if1 chaser"),
+ "if1 chaser message should have been received");
+ ok(expectedMessages.has("if2 marquee onstart"),
+ "if2 marquee onstart should have run in iframe sandbox with allow-scripts");
+ ok(expectedMessages.has("if2 marquee onbounce"),
+ "if2 marquee onbounce should have run in iframe sandbox with allow-scripts");
+ ok(expectedMessages.has("if2 marquee onfinish"),
+ "if2 marquee onfinish should have run in iframe sandbox with allow-scripts");
+
+ unexpectedMessages.forEach(
+ (v) => {
+ ok(false, v + " should NOT have run in iframe sandbox without allow-scripts");
+ }
+ );
+
+ SimpleTest.finish();
+ }
+
+ // If things are working properly the attribute event handlers won't run on
+ // the marquee in if1, so add our own capturing listeners on its window, so we
+ // know when they have fired. (These will run as we are not sandboxed.)
+ var if1FiredEvents = new Set();
+ var if1NumberOfEventsExpected = 3;
+ var if1Win = document.getElementById("if1").contentWindow;
+ if1Win.addEventListener("start", () => { checkMarqueeEvent("start"); }, true);
+ if1Win.addEventListener("bounce", () => { checkMarqueeEvent("bounce"); }, true);
+ if1Win.addEventListener("finish", () => { checkMarqueeEvent("finish"); }, true);
+
+ function checkMarqueeEvent(eventType) {
+ info("if1 event " + eventType + " fired");
+ if1FiredEvents.add(eventType);
+ if (if1FiredEvents.size == if1NumberOfEventsExpected) {
+ // Only send the chasing message after a tick of the event loop to allow
+ // event handlers on the marquee to process.
+ SimpleTest.executeSoon(sendChasingMessage);
+ }
+ }
+
+ function sendChasingMessage() {
+ // Add our own message listener to if1's window and echo back a chasing
+ // message to make sure that any messages from incorrectly run marquee
+ // attribute event handlers should have arrived before it.
+ if1Win.addEventListener("message",
+ (e) => { if1Win.parent.postMessage(e.data, "*"); });
+ if1Win.postMessage("if1 chaser", "*");
+ info("if1 chaser message sent");
+ }
+</script>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html
new file mode 100644
index 0000000000..3440878db7
--- /dev/null
+++ b/docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox other auxiliary navigation by location tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data != "otherWindow") {
+ ok(false, "event.data: got '" + event.data + "', expected 'otherWindow'");
+ }
+ ok(false, testCase.desc + " - auxiliary navigation was NOT blocked");
+ runNextTest();
+ };
+ try {
+ window.testIframe.eval(testCase.script);
+ } catch (e) {
+ ok(true, testCase.desc + " - " + e.message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: location.replace on auxiliary NOT opened by us should be blocked",
+ script: "parent.openedWindow.location.replace('file_other_auxiliary_navigation_by_location.html')",
+ },
+ {
+ desc: "Test 2: location.assign on auxiliary NOT opened by us should be blocked",
+ script: "parent.openedWindow.location.assign('file_other_auxiliary_navigation_by_location.html')",
+ },
+ {
+ desc: "Test 3: location.href on auxiliary NOT opened by us should be blocked",
+ script: "parent.openedWindow.location.href = 'file_other_auxiliary_navigation_by_location.html'",
+ },
+ {
+ desc: "Test 4: location.hash on auxiliary NOT opened by us should be blocked",
+ script: "parent.openedWindow.location.hash = 'wibble'",
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ window.openedWindow.close();
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ window.onmessage = runNextTest;
+
+ window.onload = function() {
+ window.openedWindow = window.open("file_other_auxiliary_navigation_by_location.html", "otherWindow");
+ };
+</script>
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+
+<iframe name="testIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation allow-popups"></iframe>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html
new file mode 100644
index 0000000000..1719f566a5
--- /dev/null
+++ b/docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox our auxiliary navigation by location tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data != "ourWindow") {
+ ok(false, "event.data: got '" + event.data + "', expected 'ourWindow'");
+ }
+ ok(!testCase.shouldBeBlocked, testCase.desc + " - auxiliary navigation was NOT blocked");
+ runNextTest();
+ };
+ try {
+ SpecialPowers.wrap(window.testIframe).eval(testCase.script);
+ } catch (e) {
+ ok(testCase.shouldBeBlocked, testCase.desc + " - " + SpecialPowers.wrap(e).message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: location.replace on auxiliary opened by us should NOT be blocked",
+ script: "openedWindow.location.replace('file_our_auxiliary_navigation_by_location.html')",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 2: location.assign on auxiliary opened by us should be blocked without allow-same-origin",
+ script: "openedWindow.location.assign('file_our_auxiliary_navigation_by_location.html')",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 3: location.href on auxiliary opened by us should NOT be blocked",
+ script: "openedWindow.location.href = 'file_our_auxiliary_navigation_by_location.html'",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 4: location.hash on auxiliary opened by us should be blocked without allow-same-origin",
+ script: "openedWindow.location.hash = 'wibble'",
+ shouldBeBlocked: true,
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ SpecialPowers.wrap(window.testIframe).eval("openedWindow.close()");
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ window.onmessage = runNextTest;
+
+ window.onload = function() {
+ SpecialPowers.wrap(window.testIframe).eval("var openedWindow = window.open('file_our_auxiliary_navigation_by_location.html', 'ourWindow')");
+ };
+</script>
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+
+<iframe name="testIframe" sandbox="allow-scripts allow-popups"></iframe>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_parent_navigation_by_location.html b/docshell/test/iframesandbox/test_parent_navigation_by_location.html
new file mode 100644
index 0000000000..ac6977a3f3
--- /dev/null
+++ b/docshell/test/iframesandbox/test_parent_navigation_by_location.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox parent navigation by location tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data != "parentIframe") {
+ ok(false, "event.data: got '" + event.data + "', expected 'parentIframe'");
+ }
+ ok(false, testCase.desc + " - parent navigation was NOT blocked");
+ runNextTest();
+ };
+ try {
+ window.parentIframe.childIframe.eval(testCase.script);
+ } catch (e) {
+ ok(true, testCase.desc + " - " + e.message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: parent.location.replace should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent.location.replace('file_parent_navigation_by_location.html')",
+ },
+ {
+ desc: "Test 2: parent.location.assign should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent.location.assign('file_parent_navigation_by_location.html')",
+ },
+ {
+ desc: "Test 3: parent.location.href should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent.location.href = 'file_parent_navigation_by_location.html'",
+ },
+ {
+ desc: "Test 4: parent.location.hash should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent.location.hash = 'wibble'",
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ window.onmessage = runNextTest;
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+
+<iframe name="parentIframe" src="file_parent_navigation_by_location.html"></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_sibling_navigation_by_location.html b/docshell/test/iframesandbox/test_sibling_navigation_by_location.html
new file mode 100644
index 0000000000..d7508d5748
--- /dev/null
+++ b/docshell/test/iframesandbox/test_sibling_navigation_by_location.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox sibling navigation by location tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data != "siblingIframe") {
+ ok(false, "event.data: got '" + event.data + "', expected 'siblingIframe'");
+ }
+
+ ok(false, testCase.desc + " - sibling navigation was NOT blocked");
+ runNextTest();
+ };
+
+ try {
+ window.testIframe.eval(testCase.script);
+ } catch (e) {
+ ok(true, testCase.desc + " - " + e.message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: sibling location.replace should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent['siblingIframe'].location.replace('file_sibling_navigation_by_location.html')",
+ },
+ {
+ desc: "Test 2: sibling location.assign should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent['siblingIframe'].location.assign('file_sibling_navigation_by_location.html')",
+ },
+ {
+ desc: "Test 3: sibling location.href should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent['siblingIframe'].location.href = 'file_sibling_navigation_by_location.html'",
+ },
+ {
+ desc: "Test 4: sibling location.hash should be blocked even when sandboxed with allow-same-origin allow-top-navigation",
+ script: "parent['siblingIframe'].location.hash = 'wibble'",
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ window.onmessage = runNextTest;
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+
+<iframe name="testIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe>
+<iframe name="siblingIframe" src="file_sibling_navigation_by_location.html"></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_top_navigation_by_location.html b/docshell/test/iframesandbox/test_top_navigation_by_location.html
new file mode 100644
index 0000000000..248f854bbf
--- /dev/null
+++ b/docshell/test/iframesandbox/test_top_navigation_by_location.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox top navigation by location tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ var testWin;
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data != "newTop") {
+ ok(false, "event.data: got '" + event.data + "', expected 'newTop'");
+ }
+ ok(!testCase.shouldBeBlocked, testCase.desc + " - top navigation was NOT blocked");
+ runNextTest();
+ };
+ try {
+ SpecialPowers.wrap(testWin[testCase.iframeName]).eval(testCase.script);
+ } catch (e) {
+ ok(testCase.shouldBeBlocked, testCase.desc + " - " + SpecialPowers.wrap(e).message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: top.location.replace should be blocked when sandboxed without allow-top-navigation",
+ script: "top.location.replace('file_top_navigation_by_location.html')",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 2: top.location.assign should be blocked when sandboxed without allow-top-navigation",
+ script: "top.location.assign('file_top_navigation_by_location.html')",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 3: top.location.href should be blocked when sandboxed without allow-top-navigation",
+ script: "top.location.href = 'file_top_navigation_by_location.html'",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 4: top.location.pathname should be blocked when sandboxed without allow-top-navigation",
+ script: "top.location.pathname = top.location.pathname",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 5: top.location should be blocked when sandboxed without allow-top-navigation",
+ script: "top.location = 'file_top_navigation_by_location.html'",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 6: top.location.hash should be blocked when sandboxed without allow-top-navigation",
+ script: "top.location.hash = 'wibble'",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 7: top.location.replace should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation",
+ script: "top.location.replace('file_top_navigation_by_location.html')",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 8: top.location.assign should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation",
+ script: "top.location.assign('file_top_navigation_by_location.html')",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 9: top.location.href should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation",
+ script: "top.location.href = 'file_top_navigation_by_location.html'",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 10: top.location.pathname should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation",
+ script: "top.location.pathname = top.location.pathname",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 11: top.location should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation",
+ script: "top.location = 'file_top_navigation_by_location.html'",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 12: top.location.hash should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation",
+ script: "top.location.hash = 'wibble'",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 13: top.location.replace should NOT be blocked when sandboxed with allow-top-navigation, but without allow-same-origin",
+ script: "top.location.replace('file_top_navigation_by_location.html')",
+ iframeName: "if3",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 14: top.location.assign should be blocked when sandboxed with allow-top-navigation, but without allow-same-origin",
+ script: "top.location.assign('file_top_navigation_by_location.html')",
+ iframeName: "if3",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 15: top.location.href should NOT be blocked when sandboxed with allow-top-navigation, but without allow-same-origin",
+ script: "top.location.href = 'file_top_navigation_by_location.html'",
+ iframeName: "if3",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 16: top.location.pathname should be blocked when sandboxed with allow-top-navigation, but without allow-same-origin",
+ script: "top.location.pathname = top.location.pathname",
+ iframeName: "if3",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 17: top.location should NOT be blocked when sandboxed with allow-top-navigation, but without allow-same-origin",
+ script: "top.location = 'file_top_navigation_by_location.html'",
+ iframeName: "if3",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 18: top.location.hash should be blocked when sandboxed with allow-top-navigation, but without allow-same-origin",
+ script: "top.location.hash = 'wibble'",
+ iframeName: "if3",
+ shouldBeBlocked: true,
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ testWin.close();
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ window.onmessage = runNextTest;
+ testWin = window.open("file_top_navigation_by_location.html", "newTop");
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html b/docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html
new file mode 100644
index 0000000000..11b7c78699
--- /dev/null
+++ b/docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html
@@ -0,0 +1,204 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=785310
+html5 sandboxed iframe should not be able to perform top navigation with scripts allowed
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 785310 - iframe sandbox top navigation by location via exotic means tests</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ var testWin;
+
+ function runScriptNavigationTest(testCase) {
+ window.onmessage = function(event) {
+ if (event.data.name != "newWindow") {
+ ok(false, "event.data.name: got '" + event.data.name + "', expected 'newWindow'");
+ }
+ var diag = "top navigation was " + (event.data.blocked ? "" : "NOT ") + "blocked";
+ ok((testCase.shouldBeBlocked == event.data.blocked), testCase.desc + " - " + diag);
+ runNextTest();
+ };
+ try {
+ testWin[testCase.iframeName].eval(testCase.script);
+ } catch (e) {
+ ok(testCase.shouldBeBlocked, testCase.desc + " - " + e.message);
+ runNextTest();
+ }
+ }
+
+ var testCaseIndex = -1;
+ var testCases = [
+ {
+ desc: "Test 1: location.replace.call(top.location, ...) should be blocked when sandboxed without allow-top-navigation",
+ script: "location.replace.call(top.location, 'file_top_navigation_by_location_exotic.html')",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 2: location.replace.bind(top.location, ...) should be blocked when sandboxed without allow-top-navigation",
+ script: "location.replace.bind(top.location, 'file_top_navigation_by_location_exotic.html')()",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 3: Function.bind.call(location.replace, top.location, ...) should be blocked when sandboxed without allow-top-navigation",
+ script: "Function.bind.call(location.replace, top.location, 'file_top_navigation_by_location_exotic.html')()",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 4: location.replace.call(top.location, ...) should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "location.replace.call(top.location, 'file_top_navigation_by_location_exotic.html')",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 5: location.replace.bind(top.location, ...) should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "location.replace.bind(top.location, 'file_top_navigation_by_location_exotic.html')()",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 6: Function.bind.call(location.replace, top.location, ...) should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "Function.bind.call(location.replace, top.location, 'file_top_navigation_by_location_exotic.html')()",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 7: top.location.href, via setTimeout, should be blocked when sandboxed without allow-top-navigation",
+ script: "setTimeout(function() { try { top.location.href = 'file_top_navigation_by_location_exotic.html' } catch (e) { top.onBlock() } }, 0)",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 8: top.location.href, via setTimeout, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "setTimeout(function() { try { top.location.href = 'file_top_navigation_by_location_exotic.html' } catch(e) { top.onBlock() } }, 0)",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 9: top.location.href, via eval, should be blocked when sandboxed without allow-top-navigation",
+ script: "eval('top.location.href = \"file_top_navigation_by_location_exotic.html\"')",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 10: top.location.href, via eval, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "eval('top.location.href = \"file_top_navigation_by_location_exotic.html\"')",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 11: top.location.href, via anonymous function, should be blocked when sandboxed without allow-top-navigation",
+ script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' })()",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 12: top.location.href, via anonymous function, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' })()",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 13: top.location.href, via function inserted in top, should be blocked when sandboxed without allow-top-navigation",
+ script: "top.doTest = function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }; top.doTest();",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 14: top.location.href, via function inserted in top, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "top.doTest = function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }; top.doTest();",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 15: top.location.href, via function inserted in us by top, should NOT be blocked when sandboxed without allow-top-navigation",
+ script: "top.eval('window[\"if1\"].doTest = function() { top.location.href = \"file_top_navigation_by_location_exotic.html\" };'), doTest();",
+ iframeName: "if1",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 16: top.location.href, via function inserted in top, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "top.eval('window[\"if2\"].doTest = function() { top.location.href = \"file_top_navigation_by_location_exotic.html\" };'), doTest();",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 17: top.location.href, via function in top, should NOT be blocked when sandboxed without allow-top-navigation",
+ script: "top.setOwnHref()",
+ iframeName: "if1",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 18: top.location.href, via function in top, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "top.setOwnHref()",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 19: top.location.href, via eval in top, should NOT be blocked when sandboxed without allow-top-navigation",
+ script: "top.eval('location.href = \"file_top_navigation_by_location_exotic.html\"')",
+ iframeName: "if1",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 20: top.location.href, via eval in top, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "top.eval('location.href = \"file_top_navigation_by_location_exotic.html\"')",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 21: top.location.href, via eval in top calling us, should be blocked when sandboxed without allow-top-navigation",
+ script: "function doTest() { top.location.href = 'file_top_navigation_by_location_exotic.html' } top.eval('window[\"if1\"].doTest()');",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 22: top.location.href, via eval in top calling us, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "function doTest() { top.location.href = 'file_top_navigation_by_location_exotic.html' } top.eval('window[\"if2\"].doTest()');",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ {
+ desc: "Test 23: top.location.href, via function bound to top, should be blocked when sandboxed without allow-top-navigation",
+ script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }).bind(top)();",
+ iframeName: "if1",
+ shouldBeBlocked: true,
+ },
+ {
+ desc: "Test 24: top.location.href, via function bound to top, should NOT be blocked when sandboxed with allow-top-navigation",
+ script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }).bind(top)();",
+ iframeName: "if2",
+ shouldBeBlocked: false,
+ },
+ ];
+
+ function runNextTest() {
+ ++testCaseIndex;
+ if (testCaseIndex == testCases.length) {
+ testWin.close();
+ SimpleTest.finish();
+ return;
+ }
+
+ runScriptNavigationTest(testCases[testCaseIndex]);
+ }
+
+ window.onmessage = runNextTest;
+ testWin = window.open("file_top_navigation_by_location_exotic.html", "newWindow");
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 785310
+</div>
+</body>
+</html>
diff --git a/docshell/test/iframesandbox/test_top_navigation_by_user_activation.html b/docshell/test/iframesandbox/test_top_navigation_by_user_activation.html
new file mode 100644
index 0000000000..b462c54d7b
--- /dev/null
+++ b/docshell/test/iframesandbox/test_top_navigation_by_user_activation.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1744321
+-->
+<head>
+<meta charset="utf-8">
+<title>Iframe sandbox top navigation by user activation</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+function waitForMessage(aCallback) {
+ return new Promise((aResolve) => {
+ window.addEventListener("message", function listener(aEvent) {
+ aCallback(aEvent);
+ aResolve();
+ }, { once: true });
+ });
+}
+
+[
+ {
+ desc: "A same-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' cannot navigate its top level page, if the navigation is not triggered by a user gesture",
+ sameOrigin: true,
+ userGesture: false,
+ },
+ {
+ desc: "A same-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' can navigate its top level page, if the navigation is triggered by a user gesture",
+ sameOrigin: true,
+ userGesture: true,
+ },
+ {
+ desc: "A cross-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' cannot navigate its top level page, if the navigation is not triggered by a user gesture",
+ sameOrigin: false,
+ userGesture: false,
+ },
+ {
+ desc: "A cross-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' can navigate its top level page, if the navigation is triggered by a user gesture",
+ sameOrigin: false,
+ userGesture: true,
+ },
+].forEach(({desc, sameOrigin, userGesture}) => {
+ add_task(async function() {
+ info(`Test: ${desc}`);
+
+ let url = "file_top_navigation_by_user_activation.html";
+ if (sameOrigin) {
+ url = `${location.origin}/tests/docshell/test/iframesandbox/${url}`;
+ }
+
+ let promise = waitForMessage((e) => {
+ is(e.data, "READY", "Ready for test");
+ });
+ let testWin = window.open(url);
+ await promise;
+
+ promise = waitForMessage((e) => {
+ is(e.data, userGesture ? "NAVIGATED" : "BLOCKED", "Check the result");
+ });
+ testWin.postMessage(userGesture ? "CLICK" : "SCRIPT", "*");
+ await promise;
+ testWin.close();
+ });
+});
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1744321">Mozilla Bug 1744321</a>
+<p id="display"></p>
+<div id="content">
+Tests for Bug 1744321
+</div>
+</body>
+</html>
diff --git a/docshell/test/mochitest/bug1422334_redirect.html b/docshell/test/mochitest/bug1422334_redirect.html
new file mode 100644
index 0000000000..eec7fda2c7
--- /dev/null
+++ b/docshell/test/mochitest/bug1422334_redirect.html
@@ -0,0 +1,3 @@
+<html>
+ <body>You should never see this</body>
+</html>
diff --git a/docshell/test/mochitest/bug1422334_redirect.html^headers^ b/docshell/test/mochitest/bug1422334_redirect.html^headers^
new file mode 100644
index 0000000000..fbf2d1b745
--- /dev/null
+++ b/docshell/test/mochitest/bug1422334_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Moved Temporarily
+Location: ../navigation/blank.html?x=y
diff --git a/docshell/test/mochitest/bug404548-subframe.html b/docshell/test/mochitest/bug404548-subframe.html
new file mode 100644
index 0000000000..9a248b40b3
--- /dev/null
+++ b/docshell/test/mochitest/bug404548-subframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body onload="setTimeout(function() { window.location = 'bug404548-subframe_window.html'; }, 10)">
+<iframe srcdoc="<body onpagehide='var p = window.parent.opener; var e = window.frameElement; e.parentNode.removeChild(e); if (e.parentNode == null && e.contentWindow == null) { p.firstRemoved = true; }'>">
+</iframe>
+<iframe srcdoc="<body onpagehide='window.parent.opener.secondHidden = true;'>">
+</iframe>
diff --git a/docshell/test/mochitest/bug404548-subframe_window.html b/docshell/test/mochitest/bug404548-subframe_window.html
new file mode 100644
index 0000000000..82ea73ea83
--- /dev/null
+++ b/docshell/test/mochitest/bug404548-subframe_window.html
@@ -0,0 +1 @@
+<body onload='window.opener.finishTest()'>
diff --git a/docshell/test/mochitest/bug413310-post.sjs b/docshell/test/mochitest/bug413310-post.sjs
new file mode 100644
index 0000000000..f87937ab56
--- /dev/null
+++ b/docshell/test/mochitest/bug413310-post.sjs
@@ -0,0 +1,10 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html");
+ response.write(
+ "<body onload='window.parent.onloadCount++'>" +
+ request.method +
+ " " +
+ Date.now() +
+ "</body>"
+ );
+}
diff --git a/docshell/test/mochitest/bug413310-subframe.html b/docshell/test/mochitest/bug413310-subframe.html
new file mode 100644
index 0000000000..bcff1886fd
--- /dev/null
+++ b/docshell/test/mochitest/bug413310-subframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <body onload="window.parent.onloadCount++">
+ <form action="bug413310-post.sjs" method="POST">
+ </form>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/bug529119-window.html b/docshell/test/mochitest/bug529119-window.html
new file mode 100644
index 0000000000..f1908835a7
--- /dev/null
+++ b/docshell/test/mochitest/bug529119-window.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test bug 529119, sub-window</title>
+<body onload="window.opener.windowLoaded();">
+</body>
+</html>
diff --git a/docshell/test/mochitest/bug530396-noref.sjs b/docshell/test/mochitest/bug530396-noref.sjs
new file mode 100644
index 0000000000..6a65882160
--- /dev/null
+++ b/docshell/test/mochitest/bug530396-noref.sjs
@@ -0,0 +1,22 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Cache-Control", "no-cache");
+ response.write("<body onload='");
+
+ if (!request.hasHeader("Referer")) {
+ response.write("window.parent.onloadCount++;");
+ }
+
+ if (request.queryString == "newwindow") {
+ response.write(
+ "if (window.opener) { window.opener.parent.onloadCount++; window.opener.parent.doNextStep(); }"
+ );
+ response.write("if (!window.opener) window.close();");
+ response.write("'>");
+ } else {
+ response.write("window.parent.doNextStep();'>");
+ }
+
+ response.write(request.method + " " + Date.now());
+ response.write("</body>");
+}
diff --git a/docshell/test/mochitest/bug530396-subframe.html b/docshell/test/mochitest/bug530396-subframe.html
new file mode 100644
index 0000000000..be81b9f144
--- /dev/null
+++ b/docshell/test/mochitest/bug530396-subframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <body onload="window.parent.onloadCount++">
+ <a href="bug530396-noref.sjs" rel="noreferrer foo" id="target1">bug530396-noref.sjs</a>
+ <a href="bug530396-noref.sjs?newwindow" rel="nofollow noreferrer" id="target2" target="newwindow">bug530396-noref.sjs with new window</a>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/bug570341_recordevents.html b/docshell/test/mochitest/bug570341_recordevents.html
new file mode 100644
index 0000000000..45b04866ec
--- /dev/null
+++ b/docshell/test/mochitest/bug570341_recordevents.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+ var start = Date.now();
+ window._testing_js_start = Date.now();
+ window["_testing_js_after_" + document.readyState] = start;
+ document.addEventListener("DOMContentLoaded",
+ function() {
+ window._testing_evt_DOMContentLoaded = Date.now();
+ }, true);
+ document.addEventListener("readystatechange", function() {
+ window["_testing_evt_DOM_" + document.readyState] = Date.now();
+ }, true);
+ function recordLoad() {
+ window._testing_evt_load = Date.now();
+ }
+</script>
+</head>
+<body onload="recordLoad()">This document collects time
+for events related to the page load progress.</body>
+</html>
diff --git a/docshell/test/mochitest/bug668513_redirect.html b/docshell/test/mochitest/bug668513_redirect.html
new file mode 100644
index 0000000000..1b8f66c631
--- /dev/null
+++ b/docshell/test/mochitest/bug668513_redirect.html
@@ -0,0 +1 @@
+<html><body>This document is redirected to a blank document.</body></html>
diff --git a/docshell/test/mochitest/bug668513_redirect.html^headers^ b/docshell/test/mochitest/bug668513_redirect.html^headers^
new file mode 100644
index 0000000000..0e785833c6
--- /dev/null
+++ b/docshell/test/mochitest/bug668513_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Moved Temporarily
+Location: navigation/blank.html
diff --git a/docshell/test/mochitest/bug691547_frame.html b/docshell/test/mochitest/bug691547_frame.html
new file mode 100644
index 0000000000..00172f7119
--- /dev/null
+++ b/docshell/test/mochitest/bug691547_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=691547
+-->
+<head>
+ <title>Test for Bug 691547</title>
+</head>
+<body>
+<iframe style="width:95%"></iframe>
+</body>
+</html>
diff --git a/docshell/test/mochitest/clicker.html b/docshell/test/mochitest/clicker.html
new file mode 100644
index 0000000000..b655e27ea5
--- /dev/null
+++ b/docshell/test/mochitest/clicker.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+ "use strict";
+ let target = window.opener ? window.opener : window.parent;
+
+ onmessage = ({data}) => target.postMessage({}, "*");
+</script>
diff --git a/docshell/test/mochitest/double_submit.sjs b/docshell/test/mochitest/double_submit.sjs
new file mode 100644
index 0000000000..4ca088173c
--- /dev/null
+++ b/docshell/test/mochitest/double_submit.sjs
@@ -0,0 +1,78 @@
+"use strict";
+
+let self = this;
+
+let { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function log(str) {
+ // dump(`LOG: ${str}\n`);
+}
+
+function readStream(inputStream) {
+ let available = 0;
+ let result = [];
+ while ((available = inputStream.available()) > 0) {
+ result.push(inputStream.readBytes(available));
+ }
+
+ return result.join("");
+}
+
+function now() {
+ return Date.now();
+}
+
+async function handleRequest(request, response) {
+ log("Get query parameters");
+ let params = new URLSearchParams(request.queryString);
+
+ let start = now();
+ let delay = parseInt(params.get("delay")) || 0;
+ log(`Delay for ${delay}`);
+
+ let message = "good";
+ if (request.method !== "POST") {
+ message = "bad";
+ } else {
+ log("Read POST body");
+ let body = new URLSearchParams(
+ readStream(new BinaryInputStream(request.bodyInputStream))
+ );
+ message = body.get("token") || "bad";
+ log(`The result was ${message}`);
+ }
+
+ let body = `<!doctype html>
+ <script>
+ "use strict";
+ let target = (opener || parent);
+ target.postMessage(${JSON.stringify(message)}, '*');
+ </script>`;
+
+ // Sieze power from the response to allow manually transmitting data at any
+ // rate we want, so we can delay transmitting headers.
+ response.seizePower();
+
+ log(`Writing HTTP status line at ${now() - start}`);
+ response.write("HTTP/1.1 200 OK\r\n");
+
+ await new Promise(resolve => setTimeout(() => resolve(), delay));
+
+ log(`Delay completed at ${now() - start}`);
+ response.write("Content-Type: text/html\r\n");
+ response.write(`Content-Length: ${body.length}\r\n`);
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+
+ log("Finished");
+}
diff --git a/docshell/test/mochitest/dummy_page.html b/docshell/test/mochitest/dummy_page.html
new file mode 100644
index 0000000000..59bf2a5f8f
--- /dev/null
+++ b/docshell/test/mochitest/dummy_page.html
@@ -0,0 +1,6 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ just a dummy html file
+ </body>
+</html>
diff --git a/docshell/test/mochitest/file_anchor_scroll_after_document_open.html b/docshell/test/mochitest/file_anchor_scroll_after_document_open.html
new file mode 100644
index 0000000000..7903380eac
--- /dev/null
+++ b/docshell/test/mochitest/file_anchor_scroll_after_document_open.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script>
+ if (location.hash == "#target") {
+ parent.postMessage("haveHash", "*");
+ } else {
+ document.addEventListener("DOMContentLoaded", function() {
+ document.open();
+ document.write("<!DOCTYPE html><html style='height: 100%'><body style='height: 100%'><div style='height: 200%'></div><div id='target'></div></body></html>");
+ document.close();
+ // Notify parent via postMessage, since otherwise exceptions will not get
+ // caught by its onerror handler.
+ parent.postMessage("doTest", "*");
+ });
+ }
+</script>
diff --git a/docshell/test/mochitest/file_bfcache_plus_hash_1.html b/docshell/test/mochitest/file_bfcache_plus_hash_1.html
new file mode 100644
index 0000000000..199f6003e0
--- /dev/null
+++ b/docshell/test/mochitest/file_bfcache_plus_hash_1.html
@@ -0,0 +1,24 @@
+<html><body>
+ Popup 1
+ <script type="application/javascript">
+ var bc = new BroadcastChannel("bug646641_1");
+ window.onload = () => {
+ bc.postMessage({ message: "childLoad", num: 1 })
+ }
+
+ window.onpageshow = () => {
+ bc.postMessage({ message: "childPageshow", num: 1 })
+ }
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ if (msg == "pushState") {
+ history.pushState("", "", "");
+ location = "file_bfcache_plus_hash_2.html";
+ } else if (msg == "close") {
+ bc.postMessage({ message: "closed" });
+ bc.close();
+ window.close();
+ }
+ }
+ </script>
+</body></html>
diff --git a/docshell/test/mochitest/file_bfcache_plus_hash_2.html b/docshell/test/mochitest/file_bfcache_plus_hash_2.html
new file mode 100644
index 0000000000..c27d4eaa3b
--- /dev/null
+++ b/docshell/test/mochitest/file_bfcache_plus_hash_2.html
@@ -0,0 +1,17 @@
+<html><body>
+ Popup 2
+ <script type="application/javascript">
+ var bc = new BroadcastChannel("bug646641_2");
+ window.onload = () => {
+ bc.postMessage({ message: "childLoad", num: 2 })
+ requestAnimationFrame(() => bc.postMessage({message: "childPageshow", num: 2}));
+ }
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ if (msg == "go-2") {
+ history.go(-2);
+ bc.close();
+ }
+ }
+ </script>
+</body></html>
diff --git a/docshell/test/mochitest/file_bug1121701_1.html b/docshell/test/mochitest/file_bug1121701_1.html
new file mode 100644
index 0000000000..3701bf2cad
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1121701_1.html
@@ -0,0 +1,27 @@
+<script>
+ var bc = new BroadcastChannel("file_bug1121701_1");
+ var pageHideAsserts = undefined;
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "setInnerHTML") {
+ document.body.innerHTML = "modified";
+ window.onpagehide = function(event) {
+ window.onpagehide = null;
+ pageHideAsserts = {};
+ pageHideAsserts.persisted = event.persisted;
+ pageHideAsserts.innerHTML = window.document.body.innerHTML;
+ };
+ window.location.href = msg.testUrl2;
+ } else if (command == "close") {
+ bc.postMessage({command: "closed"});
+ bc.close();
+ window.close();
+ }
+ }
+ window.onpageshow = function(e) {
+ var msg = {command: "child1PageShow", persisted: e.persisted, pageHideAsserts};
+ msg.innerHTML = window.document.body.innerHTML;
+ bc.postMessage(msg);
+ };
+</script>
diff --git a/docshell/test/mochitest/file_bug1121701_2.html b/docshell/test/mochitest/file_bug1121701_2.html
new file mode 100644
index 0000000000..6cec88cd5d
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1121701_2.html
@@ -0,0 +1,23 @@
+<script>
+ var bc = new BroadcastChannel("file_bug1121701_2");
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "setInnerHTML") {
+ window.document.body.innerHTML = "<img>";
+ window.onmessage = function() {
+ bc.postMessage({command: "onmessage"});
+ window.document.body.firstChild.src = msg.location;
+ bc.close();
+ };
+ window.onbeforeunload = function() {
+ window.postMessage("foo", "*");
+ };
+
+ history.back();
+ }
+ }
+ window.onpageshow = function(e) {
+ bc.postMessage({command: "child2PageShow", persisted: e.persisted});
+ };
+</script>
diff --git a/docshell/test/mochitest/file_bug1151421.html b/docshell/test/mochitest/file_bug1151421.html
new file mode 100644
index 0000000000..7bb8c8f363
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1151421.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<style>
+body, html {
+ height: 100%;
+}
+.spacer {
+ height: 80%;
+}
+</style>
+</head>
+<body onload='(parent || opener).childLoad()'>
+
+<div class="spacer"></div>
+<div id="content">content</div>
+<div class="spacer"></div>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1186774.html b/docshell/test/mochitest/file_bug1186774.html
new file mode 100644
index 0000000000..9af95b09bd
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1186774.html
@@ -0,0 +1 @@
+<div style='height: 9000px;'></div>
diff --git a/docshell/test/mochitest/file_bug1450164.html b/docshell/test/mochitest/file_bug1450164.html
new file mode 100644
index 0000000000..55e32ce93d
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1450164.html
@@ -0,0 +1,16 @@
+<html>
+ <head>
+ <script>
+ function go() {
+ var a = window.history.state;
+ window.history.replaceState(a, "", "1");
+ var ok = opener.ok;
+ var SimpleTest = opener.SimpleTest;
+ ok("Addition of history in unload did not crash browser");
+ SimpleTest.finish();
+ }
+ </script>
+ </head>
+ <body onunload="go()">
+ </body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1729662.html b/docshell/test/mochitest/file_bug1729662.html
new file mode 100644
index 0000000000..f5710e1c49
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1729662.html
@@ -0,0 +1,8 @@
+<script>
+addEventListener("load", () => {
+ (new BroadcastChannel("bug1729662")).postMessage("load");
+ history.pushState(1, null, location.href);
+ history.back();
+ history.forward();
+});
+</script>
diff --git a/docshell/test/mochitest/file_bug1740516_1.html b/docshell/test/mochitest/file_bug1740516_1.html
new file mode 100644
index 0000000000..ac8ca71d93
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1740516_1.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ window.addEventListener("pageshow", ({ persisted }) => {
+ let bc = new BroadcastChannel("bug1740516_1");
+ bc.addEventListener("message", ({ data }) => {
+ bc.close();
+ switch (data) {
+ case "block_bfcache_and_navigate":
+ window.blockBFCache = new RTCPeerConnection();
+ // Fall through
+ case "navigate":
+ document.location = "file_bug1740516_2.html";
+ break;
+ case "close":
+ window.close();
+ break;
+ }
+ });
+ bc.postMessage(persisted);
+ });
+ </script>
+</head>
+<body>
+ <iframe src="file_bug1740516_1_inner.html"></iframe>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1740516_1_inner.html b/docshell/test/mochitest/file_bug1740516_1_inner.html
new file mode 100644
index 0000000000..159c6bde5a
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1740516_1_inner.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ window.addEventListener("pageshow", ({ persisted }) => {
+ let bc = new BroadcastChannel("bug1740516_1_inner");
+ bc.postMessage(persisted);
+ bc.close();
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1740516_2.html b/docshell/test/mochitest/file_bug1740516_2.html
new file mode 100644
index 0000000000..2dc714feef
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1740516_2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ addEventListener("pageshow", () => { history.back(); });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1741132.html b/docshell/test/mochitest/file_bug1741132.html
new file mode 100644
index 0000000000..d863b9f015
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1741132.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ window.addEventListener("pageshow", ({ persisted }) => {
+ let bc = new BroadcastChannel("bug1741132");
+ bc.addEventListener("message", ({ data: { cmd, arg } }) => {
+ bc.close();
+ switch (cmd) {
+ case "load":
+ document.location = arg;
+ break;
+ case "go":
+ window.blockBFCache = new RTCPeerConnection();
+ history.go(arg);
+ break;
+ case "close":
+ window.close();
+ break;
+ }
+ });
+ bc.postMessage(persisted);
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1742865.sjs b/docshell/test/mochitest/file_bug1742865.sjs
new file mode 100644
index 0000000000..d97526f281
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1742865.sjs
@@ -0,0 +1,75 @@
+function handleRequest(request, response) {
+ if (request.queryString == "reset") {
+ setState("index", "0");
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.write("Reset");
+ return;
+ }
+
+ let refresh = "";
+ let index = Number(getState("index"));
+ // index == 0 First load, returns first meta refresh
+ // index == 1 Second load, caused by first meta refresh, returns second meta refresh
+ // index == 2 Third load, caused by second meta refresh, doesn't return a meta refresh
+ let query = new URLSearchParams(request.queryString);
+ if (index < 2) {
+ refresh = query.get("seconds");
+ if (query.get("crossOrigin") == "true") {
+ const hosts = ["example.org", "example.com"];
+
+ let url = `${request.scheme}://${hosts[index]}${request.path}?${request.queryString}`;
+ refresh += `; url=${url}`;
+ }
+ refresh = `<meta http-equiv="Refresh" content="${refresh}">`;
+ }
+ // We want to scroll for the first load, and check that the meta refreshes keep the same
+ // scroll position.
+ let scroll = index == 0 ? `scrollTo(0, ${query.get("scrollTo")});` : "";
+
+ setState("index", String(index + 1));
+
+ response.write(
+ `<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="no-cache">
+ ${refresh}
+ <script>
+ window.addEventListener("pageshow", () => {
+ ${scroll}
+ window.top.opener.postMessage({
+ commandType: "pageShow",
+ commandData: {
+ inputValue: document.getElementById("input").value,
+ scrollPosition: window.scrollY,
+ },
+ }, "*");
+ });
+ window.addEventListener("message", ({ data }) => {
+ if (data == "changeInputValue") {
+ document.getElementById("input").value = "1234";
+ window.top.opener.postMessage({
+ commandType: "onChangedInputValue",
+ commandData: {
+ historyLength: history.length,
+ inputValue: document.getElementById("input").value,
+ },
+ }, "*");
+ } else if (data == "loadNext") {
+ location.href += "&loadnext=1";
+ } else if (data == "back") {
+ history.back();
+ }
+ });
+ </script>
+</head>
+<body>
+<input type="text" id="input" value="initial"></input>
+<div style='height: 9000px;'></div>
+<p>
+</p>
+</body>
+</html>`
+ );
+}
diff --git a/docshell/test/mochitest/file_bug1742865_outer.sjs b/docshell/test/mochitest/file_bug1742865_outer.sjs
new file mode 100644
index 0000000000..f5e689a526
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1742865_outer.sjs
@@ -0,0 +1,23 @@
+function handleRequest(request, response) {
+ response.write(
+ `<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ window.addEventListener("message", ({ data }) => {
+ if (data == "loadNext") {
+ location.href += "&loadnext=1";
+ return;
+ }
+ // Forward other messages to the frame.
+ document.getElementById("frame").contentWindow.postMessage(data, "*");
+ });
+ </script>
+</head>
+<body>
+ <iframe src="file_bug1742865.sjs?${request.queryString}" id="frame"></iframe>
+</body>
+</html>`
+ );
+}
diff --git a/docshell/test/mochitest/file_bug1743353.html b/docshell/test/mochitest/file_bug1743353.html
new file mode 100644
index 0000000000..c08f8d143f
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1743353.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ window.addEventListener("pageshow", () => {
+ let bc = new BroadcastChannel("bug1743353");
+ bc.addEventListener("message", ({ data: cmd }) => {
+ switch (cmd) {
+ case "load":
+ bc.close();
+ document.location += "?1";
+ break;
+ case "back":
+ window.blockBFCache = new RTCPeerConnection();
+ window.addEventListener("pagehide", () => {
+ bc.postMessage("pagehide");
+ });
+ window.addEventListener("unload", () => {
+ bc.postMessage("unload");
+ bc.close();
+ });
+ history.back();
+ break;
+ case "close":
+ bc.close();
+ window.close();
+ break;
+ }
+ });
+ bc.postMessage("pageshow");
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1747033.sjs b/docshell/test/mochitest/file_bug1747033.sjs
new file mode 100644
index 0000000000..14401101b2
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1747033.sjs
@@ -0,0 +1,110 @@
+"use strict";
+
+const BOUNDARY = "BOUNDARY";
+
+// waitForPageShow should be false if this is for multipart/x-mixed-replace
+// and it's not the last part, Gecko doesn't fire pageshow for those parts.
+function documentString(waitForPageShow = true) {
+ return `<html>
+ <head>
+ <script>
+ let bc = new BroadcastChannel("bug1747033");
+ bc.addEventListener("message", ({ data: { cmd, arg = undefined } }) => {
+ switch (cmd) {
+ case "load":
+ location.href = arg;
+ break;
+ case "replaceState":
+ history.replaceState({}, "Replaced state", arg);
+ bc.postMessage({ "historyLength": history.length, "location": location.href });
+ break;
+ case "back":
+ history.back();
+ break;
+ case "close":
+ close();
+ break;
+ }
+ });
+
+ function reply() {
+ bc.postMessage({ "historyLength": history.length, "location": location.href });
+ }
+
+ ${waitForPageShow ? `addEventListener("pageshow", reply);` : "reply();"}
+ </script>
+ </head>
+ <body></body>
+</html>
+`;
+}
+
+function boundary(last = false) {
+ let b = `--${BOUNDARY}`;
+ if (last) {
+ b += "--";
+ }
+ return b + "\n";
+}
+
+function sendMultipart(response, last = false) {
+ setState("sendMore", "");
+
+ response.write(`Content-Type: text/html
+
+${documentString(last)}
+`);
+ response.write(boundary(last));
+}
+
+function shouldSendMore() {
+ return new Promise(resolve => {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ () => {
+ let sendMore = getState("sendMore");
+ if (sendMore !== "") {
+ timer.cancel();
+ resolve(sendMore);
+ }
+ },
+ 100,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ });
+}
+
+async function handleRequest(request, response) {
+ if (request.queryString == "") {
+ // This is for non-multipart/x-mixed-replace loads.
+ response.write(documentString());
+ return;
+ }
+
+ if (request.queryString == "sendNextPart") {
+ setState("sendMore", "next");
+ return;
+ }
+
+ if (request.queryString == "sendLastPart") {
+ setState("sendMore", "last");
+ return;
+ }
+
+ response.processAsync();
+
+ response.setHeader(
+ "Content-Type",
+ `multipart/x-mixed-replace; boundary=${BOUNDARY}`,
+ false
+ );
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ response.write(boundary());
+ sendMultipart(response);
+ while ((await shouldSendMore("sendMore")) !== "last") {
+ sendMultipart(response);
+ }
+ sendMultipart(response, true);
+ response.finish();
+}
diff --git a/docshell/test/mochitest/file_bug1773192_1.html b/docshell/test/mochitest/file_bug1773192_1.html
new file mode 100644
index 0000000000..42d5b3fced
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1773192_1.html
@@ -0,0 +1,13 @@
+<script>
+ let bc = new BroadcastChannel("bug1743353");
+ bc.addEventListener("message", ({ data }) => {
+ if (data == "next") {
+ location = "file_bug1773192_2.html";
+ } else if (data == "close") {
+ window.close();
+ }
+ });
+ window.addEventListener("pageshow", () => {
+ bc.postMessage({ location: location.href, referrer: document.referrer });
+ });
+</script>
diff --git a/docshell/test/mochitest/file_bug1773192_2.html b/docshell/test/mochitest/file_bug1773192_2.html
new file mode 100644
index 0000000000..c310ec66f5
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1773192_2.html
@@ -0,0 +1,19 @@
+<html>
+ <head>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store">
+ <meta name="referrer" content="same-origin">
+ </head>
+ <body>
+ <form method="POST" action="file_bug1773192_3.sjs"></form>
+ <script>
+ history.replaceState({}, "", document.referrer);
+ setTimeout(() => {
+ // The test relies on this page not going into the BFCache, so that
+ // when we come back to it we load the URL from the replaceState
+ // instead.
+ window.blockBFCache = new RTCPeerConnection();
+ document.forms[0].submit();
+ }, 0);
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/file_bug1773192_3.sjs b/docshell/test/mochitest/file_bug1773192_3.sjs
new file mode 100644
index 0000000000..ce889c7035
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1773192_3.sjs
@@ -0,0 +1,3 @@
+function handleRequest(request, response) {
+ response.write("<script>history.back();</script>");
+}
diff --git a/docshell/test/mochitest/file_bug1850335_1.html b/docshell/test/mochitest/file_bug1850335_1.html
new file mode 100644
index 0000000000..f317d2197a
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1850335_1.html
@@ -0,0 +1,23 @@
+<script>
+addEventListener("load", ({ persisted }) => {
+ document.getElementById("input1").value = "";
+});
+addEventListener("pageshow", ({ persisted }) => {
+ let bc = new BroadcastChannel("bug1850335");
+
+ bc.addEventListener("message", ({ data: { cmd, arg } }) => {
+ if (cmd == "setValue") {
+ document.getElementById("input1").value = arg;
+ bc.postMessage({ value: document.getElementById("input1").value });
+ } else if (cmd == "load") {
+ bc.close();
+ location = arg;
+ } else if (cmd == "close") {
+ close();
+ }
+ });
+
+ bc.postMessage({ persisted, value: document.getElementById("input1").value });
+});
+</script>
+<input type="text" id="input1"></input>
diff --git a/docshell/test/mochitest/file_bug1850335_2.html b/docshell/test/mochitest/file_bug1850335_2.html
new file mode 100644
index 0000000000..e2c55a6a04
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1850335_2.html
@@ -0,0 +1,18 @@
+<script>
+addEventListener("pageshow", ({ persisted }) => {
+ window.blockBFCache = new RTCPeerConnection();
+
+ let bc = new BroadcastChannel("bug1850335");
+
+ bc.addEventListener("message", ({ data: { cmd, arg } }) => {
+ bc.close();
+ if (cmd == "load") {
+ location = arg;
+ } else if (cmd == "back") {
+ history.back();
+ }
+ }, { once: true });
+
+ bc.postMessage({ persisted });
+});
+</script>
diff --git a/docshell/test/mochitest/file_bug1850335_3.html b/docshell/test/mochitest/file_bug1850335_3.html
new file mode 100644
index 0000000000..bd9ef0af7e
--- /dev/null
+++ b/docshell/test/mochitest/file_bug1850335_3.html
@@ -0,0 +1,7 @@
+<script>
+addEventListener("pageshow", () => {
+ setTimeout(() => {
+ history.back();
+ }, 0);
+});
+</script>
diff --git a/docshell/test/mochitest/file_bug385434_1.html b/docshell/test/mochitest/file_bug385434_1.html
new file mode 100644
index 0000000000..5c951f1fa6
--- /dev/null
+++ b/docshell/test/mochitest/file_bug385434_1.html
@@ -0,0 +1,29 @@
+<!--
+Inner frame for test of bug 385434.
+https://bugzilla.mozilla.org/show_bug.cgi?id=385434
+-->
+<html>
+<head>
+ <script type="application/javascript">
+ function hashchange() {
+ parent.onIframeHashchange();
+ }
+
+ function load() {
+ parent.onIframeLoad();
+ }
+
+ function scroll() {
+ parent.onIframeScroll();
+ }
+ </script>
+</head>
+
+<body onscroll="scroll()" onload="load()" onhashchange="hashchange()">
+<a href="#link1" id="link1">link1</a>
+<!-- Our parent loads us in an iframe with height 100px, so this spacer ensures
+ that switching between #link1 and #link2 causes us to scroll -->
+<div style="height:200px;"></div>
+<a href="#link2" id="link2">link2</a>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug385434_2.html b/docshell/test/mochitest/file_bug385434_2.html
new file mode 100644
index 0000000000..4aa5ef82b8
--- /dev/null
+++ b/docshell/test/mochitest/file_bug385434_2.html
@@ -0,0 +1,26 @@
+<!--
+Inner frame for test of bug 385434.
+https://bugzilla.mozilla.org/show_bug.cgi?id=385434
+-->
+<html>
+<head>
+ <script type="application/javascript">
+ function hashchange(e) {
+ // pass the event back to the parent so it can check its properties.
+ parent.gSampleEvent = e;
+
+ parent.statusMsg("Hashchange in 2.");
+ parent.onIframeHashchange();
+ }
+
+ function load() {
+ parent.statusMsg("Loading 2.");
+ parent.onIframeLoad();
+ }
+ </script>
+</head>
+
+<frameset onload="load()" onhashchange="hashchange(event)">
+ <frame src="about:blank" />
+</frameset>
+</html>
diff --git a/docshell/test/mochitest/file_bug385434_3.html b/docshell/test/mochitest/file_bug385434_3.html
new file mode 100644
index 0000000000..34dd68ef45
--- /dev/null
+++ b/docshell/test/mochitest/file_bug385434_3.html
@@ -0,0 +1,22 @@
+<!--
+Inner frame for test of bug 385434.
+https://bugzilla.mozilla.org/show_bug.cgi?id=385434
+-->
+<html>
+<head>
+ <script type="application/javascript">
+ // Notify our parent if we have a hashchange and once we're done loading.
+ window.addEventListener("hashchange", parent.onIframeHashchange);
+
+ window.addEventListener("DOMContentLoaded", function() {
+ // This also should trigger a hashchange, becuase the readystate is
+ // "interactive", not "complete" during DOMContentLoaded.
+ window.location.hash = "2";
+ });
+
+ </script>
+</head>
+
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug475636.sjs b/docshell/test/mochitest/file_bug475636.sjs
new file mode 100644
index 0000000000..cba9a968d6
--- /dev/null
+++ b/docshell/test/mochitest/file_bug475636.sjs
@@ -0,0 +1,97 @@
+let jsURL =
+ "javascript:" +
+ escape(
+ 'window.parent.postMessage("JS uri ran", "*");\
+return \'\
+<script>\
+window.parent.postMessage("Able to access private: " +\
+ window.parent.private, "*");\
+</script>\''
+ );
+let dataURL =
+ "data:text/html," +
+ escape(
+ '<!DOCTYPE HTML>\
+<script>\
+try {\
+ window.parent.postMessage("Able to access private: " +\
+ window.parent.private, "*");\
+}\
+catch (e) {\
+ window.parent.postMessage("pass", "*");\
+}\
+</script>'
+ );
+
+let tests = [
+ // Plain document should work as normal
+ '<!DOCTYPE HTML>\
+<script>\
+try {\
+ window.parent.private;\
+ window.parent.postMessage("pass", "*");\
+}\
+catch (e) {\
+ window.parent.postMessage("Unble to access private", "*");\
+}\
+</script>',
+
+ // refresh to plain doc
+ { refresh: "file_bug475636.sjs?1", doc: "<!DOCTYPE HTML>" },
+
+ // meta-refresh to plain doc
+ '<!DOCTYPE HTML>\
+<head>\
+ <meta http-equiv="refresh" content="0; url=file_bug475636.sjs?1">\
+</head>',
+
+ // refresh to data url
+ { refresh: dataURL, doc: "<!DOCTYPE HTML>" },
+
+ // meta-refresh to data url
+ '<!DOCTYPE HTML>\
+<head>\
+ <meta http-equiv="refresh" content="0; url=' +
+ dataURL +
+ '">\
+</head>',
+
+ // refresh to js url should not be followed
+ {
+ refresh: jsURL,
+ doc: '<!DOCTYPE HTML>\
+<script>\
+setTimeout(function() {\
+ window.parent.postMessage("pass", "*");\
+}, 2000);\
+</script>',
+ },
+
+ // meta refresh to js url should not be followed
+ '<!DOCTYPE HTML>\
+<head>\
+ <meta http-equiv="refresh" content="0; url=' +
+ jsURL +
+ '">\
+</head>\
+<script>\
+setTimeout(function() {\
+ window.parent.postMessage("pass", "*");\
+}, 2000);\
+</script>',
+];
+
+function handleRequest(request, response) {
+ dump("@@@@@@@@@hi there: " + request.queryString + "\n");
+ let test = tests[parseInt(request.queryString, 10) - 1];
+ response.setHeader("Content-Type", "text/html");
+
+ if (!test) {
+ response.write('<script>parent.postMessage("done", "*");</script>');
+ } else if (typeof test == "string") {
+ response.write(test);
+ } else if (test.refresh) {
+ response.setHeader("Refresh", "0; url=" + test.refresh);
+ response.write(test.doc);
+ }
+}
diff --git a/docshell/test/mochitest/file_bug509055.html b/docshell/test/mochitest/file_bug509055.html
new file mode 100644
index 0000000000..ac30876bbf
--- /dev/null
+++ b/docshell/test/mochitest/file_bug509055.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test inner frame for bug 509055</title>
+</head>
+<body onhashchange="hashchangeCallback()">
+ file_bug509055.html
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug511449.html b/docshell/test/mochitest/file_bug511449.html
new file mode 100644
index 0000000000..637732dbbf
--- /dev/null
+++ b/docshell/test/mochitest/file_bug511449.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<title>Used in test for bug 511449</title>
+<input type="text" id="input">
+<script type="text/javascript">
+ document.getElementById("input").focus();
+</script>
diff --git a/docshell/test/mochitest/file_bug540462.html b/docshell/test/mochitest/file_bug540462.html
new file mode 100644
index 0000000000..ab8c07eba5
--- /dev/null
+++ b/docshell/test/mochitest/file_bug540462.html
@@ -0,0 +1,25 @@
+<html>
+ <head>
+ <script>
+ // <!--
+ function test() {
+ document.open();
+ document.write(
+ `<html>
+ <body onload='opener.documentWriteLoad(); rel();'>
+ <a href='foo.html'>foo</a>
+ <script>
+ function rel() { setTimeout('location.reload()', 0); }
+ <\/script>
+ </body>
+ </html>`
+ );
+ document.close();
+ }
+ // -->
+ </script>
+ </head>
+ <body onload="setTimeout('test()', 0)">
+ Test for bug 540462
+ </body>
+</html>
diff --git a/docshell/test/mochitest/file_bug580069_1.html b/docshell/test/mochitest/file_bug580069_1.html
new file mode 100644
index 0000000000..7ab4610334
--- /dev/null
+++ b/docshell/test/mochitest/file_bug580069_1.html
@@ -0,0 +1,8 @@
+<html>
+<body onload='parent.page1Load();'>
+file_bug580069_1.html
+
+<form id='form' action='file_bug580069_2.sjs' method='POST'></form>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug580069_2.sjs b/docshell/test/mochitest/file_bug580069_2.sjs
new file mode 100644
index 0000000000..ff03c74e68
--- /dev/null
+++ b/docshell/test/mochitest/file_bug580069_2.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(
+ "<html><body onload='parent.page2Load(\"" +
+ request.method +
+ "\")'>file_bug580069_2.sjs</body></html>"
+ );
+}
diff --git a/docshell/test/mochitest/file_bug590573_1.html b/docshell/test/mochitest/file_bug590573_1.html
new file mode 100644
index 0000000000..b859ca7469
--- /dev/null
+++ b/docshell/test/mochitest/file_bug590573_1.html
@@ -0,0 +1,7 @@
+<html>
+<body onload='opener.page1Load();' onpageshow='opener.page1PageShow();'>
+
+<div style='height:10000px' id='div1'>This is a very tall div.</div>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug590573_2.html b/docshell/test/mochitest/file_bug590573_2.html
new file mode 100644
index 0000000000..5f9ca22be4
--- /dev/null
+++ b/docshell/test/mochitest/file_bug590573_2.html
@@ -0,0 +1,8 @@
+<html>
+<body onpopstate='opener.page2Popstate();' onload='opener.page2Load();'
+ onpageshow='opener.page2PageShow();'>
+
+<div style='height:300%' id='div2'>The second page also has a big div.</div>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug598895_1.html b/docshell/test/mochitest/file_bug598895_1.html
new file mode 100644
index 0000000000..d21f2b4a5d
--- /dev/null
+++ b/docshell/test/mochitest/file_bug598895_1.html
@@ -0,0 +1 @@
+<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body>Should show</body>
diff --git a/docshell/test/mochitest/file_bug598895_2.html b/docshell/test/mochitest/file_bug598895_2.html
new file mode 100644
index 0000000000..680c9bf22b
--- /dev/null
+++ b/docshell/test/mochitest/file_bug598895_2.html
@@ -0,0 +1 @@
+<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body></body>
diff --git a/docshell/test/mochitest/file_bug634834.html b/docshell/test/mochitest/file_bug634834.html
new file mode 100644
index 0000000000..3ff0897451
--- /dev/null
+++ b/docshell/test/mochitest/file_bug634834.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+Nothing to see here; just an empty page.
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug637644_1.html b/docshell/test/mochitest/file_bug637644_1.html
new file mode 100644
index 0000000000..d21f2b4a5d
--- /dev/null
+++ b/docshell/test/mochitest/file_bug637644_1.html
@@ -0,0 +1 @@
+<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body>Should show</body>
diff --git a/docshell/test/mochitest/file_bug637644_2.html b/docshell/test/mochitest/file_bug637644_2.html
new file mode 100644
index 0000000000..680c9bf22b
--- /dev/null
+++ b/docshell/test/mochitest/file_bug637644_2.html
@@ -0,0 +1 @@
+<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body></body>
diff --git a/docshell/test/mochitest/file_bug640387.html b/docshell/test/mochitest/file_bug640387.html
new file mode 100644
index 0000000000..3a939fb41e
--- /dev/null
+++ b/docshell/test/mochitest/file_bug640387.html
@@ -0,0 +1,26 @@
+<html>
+<body onhashchange='hashchange()' onload='load()' onpopstate='popstate()'>
+
+<script>
+function hashchange() {
+ var f = (opener || parent).childHashchange;
+ if (f)
+ f();
+}
+
+function load() {
+ var f = (opener || parent).childLoad;
+ if (f)
+ f();
+}
+
+function popstate() {
+ var f = (opener || parent).childPopstate;
+ if (f)
+ f();
+}
+</script>
+
+Not much to see here...
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug653741.html b/docshell/test/mochitest/file_bug653741.html
new file mode 100644
index 0000000000..3202b52573
--- /dev/null
+++ b/docshell/test/mochitest/file_bug653741.html
@@ -0,0 +1,13 @@
+<html>
+<body onload='(parent || opener).childLoad()'>
+
+<div style='height:500px; background:yellow'>
+<a id='#top'>Top of the page</a>
+</div>
+
+<div id='bottom'>
+<a id='#bottom'>Bottom of the page</a>
+</div>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug660404 b/docshell/test/mochitest/file_bug660404
new file mode 100644
index 0000000000..ed773420ef
--- /dev/null
+++ b/docshell/test/mochitest/file_bug660404
@@ -0,0 +1,13 @@
+--testingtesting
+Content-Type: text/html
+
+<script>
+ var bc = new BroadcastChannel("bug660404_multipart");
+ bc.postMessage({command: "finishTest",
+ textContent: window.document.documentElement.textContent,
+ innerHTML: window.document.documentElement.innerHTML
+ });
+ bc.close();
+ window.close();
+</script>
+--testingtesting--
diff --git a/docshell/test/mochitest/file_bug660404-1.html b/docshell/test/mochitest/file_bug660404-1.html
new file mode 100644
index 0000000000..878bd80426
--- /dev/null
+++ b/docshell/test/mochitest/file_bug660404-1.html
@@ -0,0 +1,12 @@
+<script>
+ var bc = new BroadcastChannel("bug660404");
+ window.onload = function() {
+ setTimeout(() => {
+ window.onpagehide = function(ev) {
+ bc.postMessage({command: "pagehide", persisted: ev.persisted});
+ bc.close();
+ };
+ window.location.href = "file_bug660404";
+ }, 0);
+ };
+</script>
diff --git a/docshell/test/mochitest/file_bug660404^headers^ b/docshell/test/mochitest/file_bug660404^headers^
new file mode 100644
index 0000000000..5c821f3f48
--- /dev/null
+++ b/docshell/test/mochitest/file_bug660404^headers^
@@ -0,0 +1 @@
+Content-Type: multipart/x-mixed-replace; boundary="testingtesting"
diff --git a/docshell/test/mochitest/file_bug662170.html b/docshell/test/mochitest/file_bug662170.html
new file mode 100644
index 0000000000..3202b52573
--- /dev/null
+++ b/docshell/test/mochitest/file_bug662170.html
@@ -0,0 +1,13 @@
+<html>
+<body onload='(parent || opener).childLoad()'>
+
+<div style='height:500px; background:yellow'>
+<a id='#top'>Top of the page</a>
+</div>
+
+<div id='bottom'>
+<a id='#bottom'>Bottom of the page</a>
+</div>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug668513.html b/docshell/test/mochitest/file_bug668513.html
new file mode 100644
index 0000000000..ae417a35bd
--- /dev/null
+++ b/docshell/test/mochitest/file_bug668513.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for Bug 668513</title>
+<script>
+ var SimpleTest = opener.SimpleTest;
+ var ok = opener.ok;
+ var is = opener.is;
+
+ function finish() {
+ SimpleTest.finish();
+ close();
+ }
+
+ function onload_test() {
+ var win = frames[0];
+ ok(win.performance, "Window.performance should be defined");
+ ok(win.performance.navigation, "Window.performance.navigation should be defined");
+ var navigation = win.performance && win.performance.navigation;
+ if (navigation === undefined) {
+ // avoid script errors
+ finish();
+ return;
+ }
+
+ // do this with a timeout to see the visuals of the navigations.
+ setTimeout(nav_frame, 100);
+ }
+
+ var step = 1;
+ function nav_frame() {
+ var navigation_frame = frames[0];
+ var navigation = navigation_frame.performance.navigation;
+ switch (step) {
+ case 1:
+ {
+ navigation_frame.location.href = "bug570341_recordevents.html";
+ step++;
+ break;
+ }
+ case 2:
+ {
+ is(navigation.type, navigation.TYPE_NAVIGATE,
+ "Expected window.performance.navigation.type == TYPE_NAVIGATE");
+ navigation_frame.history.back();
+ step++;
+ break;
+ }
+ case 3:
+ {
+ is(navigation.type, navigation.TYPE_BACK_FORWARD,
+ "Expected window.performance.navigation.type == TYPE_BACK_FORWARD");
+ step++;
+ navigation_frame.history.forward();
+ break;
+ }
+ case 4:
+ {
+ is(navigation.type, navigation.TYPE_BACK_FORWARD,
+ "Expected window.performance.navigation.type == TYPE_BACK_FORWARD");
+ navigation_frame.location.href = "bug668513_redirect.html";
+ step++;
+ break;
+ }
+ case 5:
+ {
+ is(navigation.type, navigation.TYPE_NAVIGATE,
+ "Expected timing.navigation.type as TYPE_NAVIGATE");
+ is(navigation.redirectCount, 1,
+ "Expected navigation.redirectCount == 1 on an server redirected navigation");
+
+ var timing = navigation_frame.performance && navigation_frame.performance.timing;
+ if (timing === undefined) {
+ // avoid script errors
+ finish();
+ break;
+ }
+ ok(timing.navigationStart > 0, "navigationStart should be > 0");
+ var sequence = ["navigationStart", "redirectStart", "redirectEnd", "fetchStart"];
+ for (var j = 1; j < sequence.length; ++j) {
+ var prop = sequence[j];
+ var prevProp = sequence[j - 1];
+ ok(timing[prevProp] <= timing[prop],
+ ["Expected ", prevProp, " to happen before ", prop,
+ ", got ", prevProp, " = ", timing[prevProp],
+ ", ", prop, " = ", timing[prop]].join(""));
+ }
+ step++;
+ finish();
+ break;
+ }
+ }
+ }
+</script>
+</head>
+<body>
+<div id="frames">
+<iframe name="child0" onload="onload_test();" src="navigation/blank.html"></iframe>
+</div>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug669671.sjs b/docshell/test/mochitest/file_bug669671.sjs
new file mode 100644
index 0000000000..5871419de8
--- /dev/null
+++ b/docshell/test/mochitest/file_bug669671.sjs
@@ -0,0 +1,17 @@
+function handleRequest(request, response) {
+ var count = parseInt(getState("count"));
+ if (!count || request.queryString == "countreset") {
+ count = 0;
+ }
+
+ setState("count", count + 1 + "");
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "max-age=0");
+ response.write(
+ '<html><body onload="opener.onChildLoad()" ' +
+ "onunload=\"parseInt('0')\">" +
+ count +
+ "</body></html>"
+ );
+}
diff --git a/docshell/test/mochitest/file_bug675587.html b/docshell/test/mochitest/file_bug675587.html
new file mode 100644
index 0000000000..842ab9ae79
--- /dev/null
+++ b/docshell/test/mochitest/file_bug675587.html
@@ -0,0 +1 @@
+<script>location.hash = "";</script>
diff --git a/docshell/test/mochitest/file_bug680257.html b/docshell/test/mochitest/file_bug680257.html
new file mode 100644
index 0000000000..ff480e96a5
--- /dev/null
+++ b/docshell/test/mochitest/file_bug680257.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <style type='text/css'>
+ a { color: black; }
+ a:target { color: red; }
+ </style>
+</head>
+
+<body onload='(opener || parent).popupLoaded()'>
+
+<a id='a' href='#a'>link</a>
+<a id='b' href='#b'>link2</a>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_bug703855.html b/docshell/test/mochitest/file_bug703855.html
new file mode 100644
index 0000000000..fe15b6e3df
--- /dev/null
+++ b/docshell/test/mochitest/file_bug703855.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<!-- Just need an empty file here, as long as it's served over HTTP -->
diff --git a/docshell/test/mochitest/file_bug728939.html b/docshell/test/mochitest/file_bug728939.html
new file mode 100644
index 0000000000..1cd52a44e1
--- /dev/null
+++ b/docshell/test/mochitest/file_bug728939.html
@@ -0,0 +1,3 @@
+<html>
+<body onload="opener.popupLoaded()">file_bug728939</body>
+</html>
diff --git a/docshell/test/mochitest/file_close_onpagehide1.html b/docshell/test/mochitest/file_close_onpagehide1.html
new file mode 100644
index 0000000000..ccf3b625a1
--- /dev/null
+++ b/docshell/test/mochitest/file_close_onpagehide1.html
@@ -0,0 +1,5 @@
+<script>
+ window.onload = () => {
+ opener.postMessage("initial", "*");
+ };
+</script>
diff --git a/docshell/test/mochitest/file_close_onpagehide2.html b/docshell/test/mochitest/file_close_onpagehide2.html
new file mode 100644
index 0000000000..a8e9479f47
--- /dev/null
+++ b/docshell/test/mochitest/file_close_onpagehide2.html
@@ -0,0 +1,5 @@
+<script>
+ window.onload = () => {
+ opener.postMessage("second", "*");
+ };
+</script>;
diff --git a/docshell/test/mochitest/file_compressed_multipart b/docshell/test/mochitest/file_compressed_multipart
new file mode 100644
index 0000000000..3c56226951
--- /dev/null
+++ b/docshell/test/mochitest/file_compressed_multipart
Binary files differ
diff --git a/docshell/test/mochitest/file_compressed_multipart^headers^ b/docshell/test/mochitest/file_compressed_multipart^headers^
new file mode 100644
index 0000000000..9376927812
--- /dev/null
+++ b/docshell/test/mochitest/file_compressed_multipart^headers^
@@ -0,0 +1,2 @@
+Content-Type: multipart/x-mixed-replace; boundary="testingtesting"
+Content-Encoding: gzip
diff --git a/docshell/test/mochitest/file_content_javascript_loads_frame.html b/docshell/test/mochitest/file_content_javascript_loads_frame.html
new file mode 100644
index 0000000000..9e2851aa8b
--- /dev/null
+++ b/docshell/test/mochitest/file_content_javascript_loads_frame.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<script type="application/javascript">
+"use strict";
+
+addEventListener("message", event => {
+ if ("ping" in event.data) {
+ event.source.postMessage({ pong: event.data.ping }, event.origin);
+ }
+});
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_content_javascript_loads_root.html b/docshell/test/mochitest/file_content_javascript_loads_root.html
new file mode 100644
index 0000000000..b9f2c1faa7
--- /dev/null
+++ b/docshell/test/mochitest/file_content_javascript_loads_root.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<script type="application/javascript">
+"use strict";
+
+window.onload = () => {
+ opener.postMessage("ready", "*");
+};
+
+// eslint-disable-next-line no-shadow
+function promiseMessage(source, filter = event => true) {
+ return new Promise(resolve => {
+ function listener(event) {
+ if (event.source == source && filter(event)) {
+ removeEventListener("message", listener);
+ resolve(event);
+ }
+ }
+ addEventListener("message", listener);
+ });
+}
+
+// Sends a message to the given target window and waits for the response.
+function ping(target) {
+ let msg = { ping: Math.random() };
+ target.postMessage(msg, "*");
+ return promiseMessage(
+ target,
+ event => event.data && event.data.pong == msg.ping
+ );
+}
+
+function setFrameLocation(name, uri) {
+ window[name].location = uri;
+}
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_form_restoration_no_store.html b/docshell/test/mochitest/file_form_restoration_no_store.html
new file mode 100644
index 0000000000..6e8756a693
--- /dev/null
+++ b/docshell/test/mochitest/file_form_restoration_no_store.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ window.addEventListener("pageshow", ({ persisted }) => {
+ let bc = new BroadcastChannel("form_restoration");
+ bc.addEventListener("message", ({ data }) => {
+ switch (data) {
+ case "enter_data":
+ document.getElementById("formElement").value = "test";
+ break;
+ case "reload":
+ bc.close();
+ location.reload();
+ break;
+ case "navigate":
+ bc.close();
+ document.location = "file_form_restoration_no_store.html?1";
+ break;
+ case "back":
+ bc.close();
+ history.back();
+ break;
+ case "close":
+ bc.close();
+ window.close();
+ break;
+ }
+ });
+ bc.postMessage({ persisted, formData: document.getElementById("formElement").value });
+ });
+ </script>
+</head>
+<body>
+ <input id="formElement" type="text" value="initial">
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_form_restoration_no_store.html^headers^ b/docshell/test/mochitest/file_form_restoration_no_store.html^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/docshell/test/mochitest/file_form_restoration_no_store.html^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/docshell/test/mochitest/file_framedhistoryframes.html b/docshell/test/mochitest/file_framedhistoryframes.html
new file mode 100644
index 0000000000..314f9c72d8
--- /dev/null
+++ b/docshell/test/mochitest/file_framedhistoryframes.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<iframe id="iframe" src="historyframes.html"></iframe>
+<script type="application/javascript">
+
+var SimpleTest = window.opener.SimpleTest;
+var is = window.opener.is;
+
+function done() {
+ window.opener.done();
+}
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/file_load_during_reload.html b/docshell/test/mochitest/file_load_during_reload.html
new file mode 100644
index 0000000000..600d5c1728
--- /dev/null
+++ b/docshell/test/mochitest/file_load_during_reload.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <script>
+ function notifyOpener() {
+ opener.postMessage("loaded", "*");
+ }
+ </script>
+ </head>
+ <body onload="notifyOpener()">
+ </body>
+</html>
diff --git a/docshell/test/mochitest/file_pushState_after_document_open.html b/docshell/test/mochitest/file_pushState_after_document_open.html
new file mode 100644
index 0000000000..97a6954f2e
--- /dev/null
+++ b/docshell/test/mochitest/file_pushState_after_document_open.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+ document.addEventListener("DOMContentLoaded", function() {
+ document.open();
+ document.write("<!DOCTYPE html>New Document here");
+ document.close();
+ // Notify parent via postMessage, since otherwise exceptions will not get
+ // caught by its onerror handler.
+ parent.postMessage("doTest", "*");
+ });
+</script>
diff --git a/docshell/test/mochitest/file_redirect_history.html b/docshell/test/mochitest/file_redirect_history.html
new file mode 100644
index 0000000000..3971faf4fd
--- /dev/null
+++ b/docshell/test/mochitest/file_redirect_history.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <script>
+ function loaded() {
+ addEventListener("message", ({ data }) => {
+ document.getElementById("form").action = data;
+ document.getElementById("button").click();
+ }, { once: true });
+ opener.postMessage("loaded", "*");
+ }
+ </script>
+ </head>
+ <body onload="loaded();">
+ <form id="form" method="POST">
+ <input id="button" type="submit" />
+ </form>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/form_submit.sjs b/docshell/test/mochitest/form_submit.sjs
new file mode 100644
index 0000000000..1a1fa5d89c
--- /dev/null
+++ b/docshell/test/mochitest/form_submit.sjs
@@ -0,0 +1,40 @@
+"use strict";
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+function log(str) {
+ // dump(`LOG: ${str}\n`);
+}
+
+async function handleRequest(request, response) {
+ if (request.method !== "POST") {
+ throw new Error("Expected a post request");
+ } else {
+ log("Reading request");
+ let available = 0;
+ let inputStream = new BinaryInputStream(request.bodyInputStream);
+ while ((available = inputStream.available()) > 0) {
+ log(inputStream.readBytes(available));
+ }
+ }
+
+ log("Setting Headers");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "OK");
+ log("Writing body");
+ response.write(
+ '<script>"use strict"; let target = opener ? opener : parent; target.postMessage("done", "*");</script>'
+ );
+ log("Done");
+}
diff --git a/docshell/test/mochitest/form_submit_redirect.sjs b/docshell/test/mochitest/form_submit_redirect.sjs
new file mode 100644
index 0000000000..96fd84561c
--- /dev/null
+++ b/docshell/test/mochitest/form_submit_redirect.sjs
@@ -0,0 +1,13 @@
+"use strict";
+
+async function handleRequest(request, response) {
+ if (request.method !== "POST") {
+ throw new Error("Expected a post request");
+ } else {
+ let params = new URLSearchParams(request.queryString);
+ let redirect = params.get("redirectTo");
+
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ response.setHeader("Location", redirect);
+ }
+}
diff --git a/docshell/test/mochitest/historyframes.html b/docshell/test/mochitest/historyframes.html
new file mode 100644
index 0000000000..31f46a5071
--- /dev/null
+++ b/docshell/test/mochitest/historyframes.html
@@ -0,0 +1,176 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=602256
+-->
+<head>
+ <title>Test for Bug 602256</title>
+</head>
+<body onload="SimpleTest.executeSoon(run_test)">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602256">Mozilla Bug 602256</a>
+<div id="content">
+ <iframe id="iframe" src="start_historyframe.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 602256 */
+
+var testWin = window.opener ? window.opener : window.parent;
+
+var SimpleTest = testWin.SimpleTest;
+function is() { testWin.is.apply(testWin, arguments); }
+
+var gFrame = null;
+
+function gState() {
+ return location.hash.replace(/^#/, "");
+}
+
+function waitForLoad(aCallback) {
+ function listener() {
+ gFrame.removeEventListener("load", listener);
+ SimpleTest.executeSoon(aCallback);
+ }
+
+ gFrame.addEventListener("load", listener);
+}
+
+function loadContent(aURL, aCallback) {
+ waitForLoad(aCallback);
+
+ gFrame.src = aURL;
+}
+
+function getURL() {
+ return gFrame.contentDocument.documentURI;
+}
+
+function getContent() {
+ return gFrame.contentDocument.getElementById("text").textContent;
+}
+
+var BASE_URI = "http://mochi.test:8888/tests/docshell/test/mochitest/";
+var START = BASE_URI + "start_historyframe.html";
+var URL1 = BASE_URI + "url1_historyframe.html";
+var URL2 = BASE_URI + "url2_historyframe.html";
+
+function run_test() {
+ window.location.hash = "START";
+
+ gFrame = document.getElementById("iframe");
+
+ test_basic_inner_navigation();
+}
+
+function end_test() {
+ testWin.done();
+}
+
+var gTestContinuation = null;
+function continueAsync() {
+ setTimeout(function() { gTestContinuation.next(); })
+}
+
+function test_basic_inner_navigation() {
+ // Navigate the inner frame a few times
+ loadContent(URL1, function() {
+ is(getURL(), URL1, "URL should be correct");
+ is(getContent(), "Test1", "Page should be correct");
+
+ loadContent(URL2, function() {
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ // Test that history is working
+ waitForLoad(function() {
+ is(getURL(), URL1, "URL should be correct");
+ is(getContent(), "Test1", "Page should be correct");
+
+ waitForLoad(function() {
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ gTestContinuation = test_state_navigation();
+ gTestContinuation.next();
+ });
+ window.history.forward();
+ });
+ window.history.back();
+ });
+ });
+}
+
+function* test_state_navigation() {
+ window.location.hash = "STATE1";
+
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ window.location.hash = "STATE2";
+
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ window.addEventListener("popstate", (e) => {
+ continueAsync();
+ }, {once: true});
+ window.history.back();
+ yield;
+
+ is(gState(), "STATE1", "State should be correct after going back");
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ window.addEventListener("popstate", (e) => {
+ continueAsync();
+ }, {once: true});
+ window.history.forward();
+ yield;
+
+ is(gState(), "STATE2", "State should be correct after going forward");
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ window.addEventListener("popstate", (e) => {
+ continueAsync();
+ }, {once: true});
+ window.history.back();
+ yield;
+
+ window.addEventListener("popstate", (e) => {
+ continueAsync();
+ }, {once: true});
+ window.history.back();
+ yield;
+
+ is(gState(), "START", "State should be correct");
+ is(getURL(), URL2, "URL should be correct");
+ is(getContent(), "Test2", "Page should be correct");
+
+ waitForLoad(function() {
+ is(getURL(), URL1, "URL should be correct");
+ is(getContent(), "Test1", "Page should be correct");
+
+ waitForLoad(function() {
+ is(gState(), "START", "State should be correct");
+ is(getURL(), START, "URL should be correct");
+ is(getContent(), "Start", "Page should be correct");
+
+ end_test();
+ });
+
+ window.history.back();
+
+ is(gState(), "START", "State should be correct after going back twice");
+ });
+
+ window.history.back();
+ continueAsync();
+ yield;
+ is(gState(), "START", "State should be correct");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/mochitest.toml b/docshell/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..8d593c9e36
--- /dev/null
+++ b/docshell/test/mochitest/mochitest.toml
@@ -0,0 +1,321 @@
+[DEFAULT]
+support-files = [
+ "bug404548-subframe.html",
+ "bug404548-subframe_window.html",
+ "bug413310-post.sjs",
+ "bug413310-subframe.html",
+ "bug529119-window.html",
+ "bug570341_recordevents.html",
+ "bug668513_redirect.html",
+ "bug668513_redirect.html^headers^",
+ "bug691547_frame.html",
+ "dummy_page.html",
+ "file_anchor_scroll_after_document_open.html",
+ "file_bfcache_plus_hash_1.html",
+ "file_bfcache_plus_hash_2.html",
+ "file_bug385434_1.html",
+ "file_bug385434_2.html",
+ "file_bug385434_3.html",
+ "file_bug475636.sjs",
+ "file_bug509055.html",
+ "file_bug540462.html",
+ "file_bug580069_1.html",
+ "file_bug580069_2.sjs",
+ "file_bug598895_1.html",
+ "file_bug598895_2.html",
+ "file_bug590573_1.html",
+ "file_bug590573_2.html",
+ "file_bug634834.html",
+ "file_bug637644_1.html",
+ "file_bug637644_2.html",
+ "file_bug640387.html",
+ "file_bug653741.html",
+ "file_bug660404",
+ "file_bug660404^headers^",
+ "file_bug660404-1.html",
+ "file_bug662170.html",
+ "file_bug669671.sjs",
+ "file_bug680257.html",
+ "file_bug703855.html",
+ "file_bug728939.html",
+ "file_bug1121701_1.html",
+ "file_bug1121701_2.html",
+ "file_bug1186774.html",
+ "file_bug1151421.html",
+ "file_bug1450164.html",
+ "file_close_onpagehide1.html",
+ "file_close_onpagehide2.html",
+ "file_compressed_multipart",
+ "file_compressed_multipart^headers^",
+ "file_pushState_after_document_open.html",
+ "historyframes.html",
+ "ping.html ",
+ "start_historyframe.html",
+ "url1_historyframe.html",
+ "url2_historyframe.html",
+]
+
+["test_anchor_scroll_after_document_open.html"]
+
+["test_bfcache_plus_hash.html"]
+
+["test_bug385434.html"]
+
+["test_bug387979.html"]
+
+["test_bug402210.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug404548.html"]
+
+["test_bug413310.html"]
+skip-if = ["true"] # Disabled for too many intermittent failures (bug 719186)
+
+["test_bug475636.html"]
+
+["test_bug509055.html"]
+
+["test_bug511449.html"]
+skip-if = [
+ "os == 'win'",
+ "os == 'linux'",
+ "os == 'android'",
+ "headless", # Headless: bug 1410525
+]
+support-files = ["file_bug511449.html"]
+
+["test_bug529119-1.html"]
+
+["test_bug529119-2.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug530396.html"]
+support-files = [
+ "bug530396-noref.sjs",
+ "bug530396-subframe.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug540462.html"]
+skip-if = ["os == 'android' && debug"]
+
+["test_bug551225.html"]
+
+["test_bug570341.html"]
+skip-if = ["(verify && !debug && (os == 'win'))"]
+
+["test_bug580069.html"]
+skip-if = ["(verify && !debug && (os == 'win'))"]
+
+["test_bug590573.html"]
+
+["test_bug598895.html"]
+
+["test_bug634834.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug637644.html"]
+
+["test_bug640387_1.html"]
+
+["test_bug640387_2.html"]
+
+["test_bug653741.html"]
+
+["test_bug660404.html"]
+
+["test_bug662170.html"]
+
+["test_bug668513.html"]
+support-files = ["file_bug668513.html"]
+
+["test_bug669671.html"]
+
+["test_bug675587.html"]
+support-files = ["file_bug675587.html"]
+
+["test_bug680257.html"]
+
+["test_bug691547.html"]
+
+["test_bug694612.html"]
+
+["test_bug703855.html"]
+
+["test_bug728939.html"]
+
+["test_bug797909.html"]
+
+["test_bug1045096.html"]
+
+["test_bug1121701.html"]
+
+["test_bug1151421.html"]
+
+["test_bug1186774.html"]
+
+["test_bug1422334.html"]
+support-files = [
+ "bug1422334_redirect.html",
+ "bug1422334_redirect.html^headers^",
+ "!/docshell/test/navigation/blank.html",
+]
+
+["test_bug1450164.html"]
+
+["test_bug1507702.html"]
+
+["test_bug1645781.html"]
+support-files = ["form_submit.sjs"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1729662.html"]
+support-files = ["file_bug1729662.html"]
+
+["test_bug1740516.html"]
+support-files = [
+ "file_bug1740516_1.html",
+ "file_bug1740516_1_inner.html",
+ "file_bug1740516_2.html",
+]
+
+["test_bug1741132.html"]
+support-files = ["file_bug1741132.html"]
+skip-if = ["os == 'android' && !sessionHistoryInParent"]
+
+["test_bug1742865.html"]
+support-files = [
+ "file_bug1742865.sjs",
+ "file_bug1742865_outer.sjs",
+]
+skip-if = [
+ "os == 'android' && debug && fission && verify", # Bug 1745937
+ "http3",
+ "http2",
+]
+
+["test_bug1743353.html"]
+support-files = ["file_bug1743353.html"]
+
+["test_bug1747033.html"]
+support-files = ["file_bug1747033.sjs"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1773192.html"]
+support-files = [
+ "file_bug1773192_1.html",
+ "file_bug1773192_2.html",
+ "file_bug1773192_3.sjs",
+]
+
+["test_bug1850335.html"]
+support-files = [
+ "file_bug1850335_1.html",
+ "file_bug1850335_2.html",
+ "file_bug1850335_3.html",
+]
+
+["test_close_onpagehide_by_history_back.html"]
+
+["test_close_onpagehide_by_window_close.html"]
+
+["test_compressed_multipart.html"]
+
+["test_content_javascript_loads.html"]
+support-files = [
+ "file_content_javascript_loads_root.html",
+ "file_content_javascript_loads_frame.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_double_submit.html"]
+support-files = [
+ "clicker.html",
+ "double_submit.sjs",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_forceinheritprincipal_overrule_owner.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_form_restoration.html"]
+support-files = [
+ "file_form_restoration_no_store.html",
+ "file_form_restoration_no_store.html^headers^",
+]
+
+["test_framedhistoryframes.html"]
+support-files = ["file_framedhistoryframes.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_iframe_srcdoc_to_remote.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_javascript_sandboxed_popup.html"]
+
+["test_load_during_reload.html"]
+support-files = ["file_load_during_reload.html"]
+
+["test_navigate_after_pagehide.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_pushState_after_document_open.html"]
+
+["test_redirect_history.html"]
+support-files = [
+ "file_redirect_history.html",
+ "form_submit_redirect.sjs",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_triggeringprincipal_location_seturi.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_windowedhistoryframes.html"]
+skip-if = [
+ "!debug && os == 'android'", # Bug 1573892
+ "http3",
+ "http2",
+]
diff --git a/docshell/test/mochitest/ping.html b/docshell/test/mochitest/ping.html
new file mode 100644
index 0000000000..7d84560dd1
--- /dev/null
+++ b/docshell/test/mochitest/ping.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<script>
+ "use strict";
+ let target = (window.opener || window.parent);
+ target.postMessage("ping", "*");
+</script>
diff --git a/docshell/test/mochitest/start_historyframe.html b/docshell/test/mochitest/start_historyframe.html
new file mode 100644
index 0000000000..a791af4e64
--- /dev/null
+++ b/docshell/test/mochitest/start_historyframe.html
@@ -0,0 +1 @@
+<p id='text'>Start</p>
diff --git a/docshell/test/mochitest/test_anchor_scroll_after_document_open.html b/docshell/test/mochitest/test_anchor_scroll_after_document_open.html
new file mode 100644
index 0000000000..5309f6cf19
--- /dev/null
+++ b/docshell/test/mochitest/test_anchor_scroll_after_document_open.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=881487
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 881487</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 881487 */
+ SimpleTest.waitForExplicitFinish();
+ // Child needs to invoke us, otherwise our onload will fire before the child
+ // has done the write/close bit.
+ var gotOnload = false;
+ addLoadEvent(function() {
+ gotOnload = true;
+ });
+ onmessage = function handleMessage(msg) {
+ if (msg.data == "doTest") {
+ if (!gotOnload) {
+ addLoadEvent(function() { handleMessage(msg); });
+ return;
+ }
+ frames[0].onscroll = function() {
+ ok(true, "Got a scroll event");
+ SimpleTest.finish();
+ };
+ frames[0].location.hash = "#target";
+ return;
+ }
+ if (msg.data == "haveHash") {
+ ok(false, "Child got reloaded");
+ } else {
+ ok(false, "Unexpected message");
+ }
+ SimpleTest.finish();
+ };
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=881487">Mozilla Bug 881487</a>
+<p id="display">
+ <!-- iframe goes here so it can scroll -->
+<iframe src="file_anchor_scroll_after_document_open.html"></iframe>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bfcache_plus_hash.html b/docshell/test/mochitest/test_bfcache_plus_hash.html
new file mode 100644
index 0000000000..cb5bc06f21
--- /dev/null
+++ b/docshell/test/mochitest/test_bfcache_plus_hash.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=646641
+-->
+<head>
+ <title>Test for Bug 646641</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=646641">Mozilla Bug 646641</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 646641 */
+
+/**
+ * Steps:
+ * - Main page (this one) opens file_bfcache_plus_hash_1.html (subpage 1)
+ * - subpage 1 sends msg { "childLoad", 1 }
+ * - subpage 1 sends msg { "childPageshow", 1 }
+ * - main page sends message "pushState"
+ * - subpage 1 does pushState()
+ * - subpage 1 navigates to file_bfcache_plus_hash_2.html (subpage 2)
+ * - subpage 2 sends msg { "childLoad", 2 }
+ * - subpage 2 sends msg { "childPageshow", 2 }
+ * - main page sends msg "go-2"
+ * - subpage 2 goes back two history entries
+ * - subpage 1 sends msg { "childPageshow", 1 }
+ * - Receiving only this msg shows we have retrieved the document from bfcache
+ * - main page sends msg "close"
+ * - subpage 1 sends msg "closed"
+ */
+SimpleTest.waitForExplicitFinish();
+
+function debug(msg) {
+ // Wrap dump so we can turn debug messages on and off easily.
+ dump(msg + "\n");
+}
+
+var expectedLoadNum = -1;
+var expectedPageshowNum = -1;
+
+function waitForLoad(n) {
+ debug("Waiting for load " + n);
+ expectedLoadNum = n;
+}
+
+function waitForShow(n) {
+ debug("Waiting for show " + n);
+ expectedPageshowNum = n;
+}
+
+
+
+function executeTest() {
+ function* test() {
+ window.open("file_bfcache_plus_hash_1.html", "", "noopener");
+ waitForLoad(1);
+ waitForShow(1);
+ yield undefined;
+ yield undefined;
+
+ bc1.postMessage("pushState");
+
+ waitForLoad(2);
+ waitForShow(2);
+ yield undefined;
+ yield undefined;
+
+ // Now go back 2. The first page should be retrieved from bfcache.
+ bc2.postMessage("go-2");
+ waitForShow(1);
+ yield undefined;
+
+ bc1.postMessage("close");
+ }
+
+ var bc1 = new BroadcastChannel("bug646641_1");
+ var bc2 = new BroadcastChannel("bug646641_2");
+ bc1.onmessage = (msgEvent) => {
+ var msg = msgEvent.data.message;
+ var n = msgEvent.data.num;
+ if (msg == "childLoad") {
+ if (n == expectedLoadNum) {
+ debug("Got load " + n);
+ expectedLoadNum = -1;
+
+ // Spin the event loop before calling gGen.next() so the generator runs
+ // outside the onload handler. This prevents us from encountering all
+ // sorts of docshell quirks.
+ setTimeout(function() { gGen.next(); }, 0);
+ } else {
+ debug("Got unexpected load " + n);
+ ok(false, "Got unexpected load " + n);
+ }
+ } else if (msg == "childPageshow") {
+ if (n == expectedPageshowNum) {
+ debug("Got expected pageshow " + n);
+ expectedPageshowNum = -1;
+ ok(true, "Got expected pageshow " + n);
+ setTimeout(function() { gGen.next(); }, 0);
+ } else {
+ debug("Got unexpected pageshow " + n);
+ ok(false, "Got unexpected pageshow " + n);
+ }
+ } else if (msg == "closed") {
+ bc1.close();
+ bc2.close();
+ SimpleTest.finish();
+ }
+ }
+
+ bc2.onmessage = bc1.onmessage;
+
+ var gGen = test();
+
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ gGen.next();
+ });
+}
+if (isXOrigin) {
+ // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+ // Acquire storage access permission here so that the BroadcastChannel used to
+ // communicate with the opened windows works in xorigin tests. Otherwise,
+ // the iframe containing this page is isolated from first-party storage access,
+ // which isolates BroadcastChannel communication.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => {
+ SpecialPowers.wrap(document).requestStorageAccess().then(() => {
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]],
+ }).then(() => {
+ executeTest();
+ });
+ });
+ });
+} else {
+ executeTest();
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1045096.html b/docshell/test/mochitest/test_bug1045096.html
new file mode 100644
index 0000000000..2df2232b7e
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1045096.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1045096
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1045096</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045096">Mozilla Bug 1045096</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+ <script type="application/javascript">
+
+ /** Test for Bug 1045096 */
+ var i = document.createElement("iframe");
+ i.src = "javascript:false"; // This is required!
+ $("content").appendChild(i);
+ ok(i.contentWindow.performance, "Should have a performance object");
+ </script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1121701.html b/docshell/test/mochitest/test_bug1121701.html
new file mode 100644
index 0000000000..cd0b2529d4
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1121701.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1121701
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1121701</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1121701 */
+ SimpleTest.waitForExplicitFinish();
+
+ var testUrl1 = "file_bug1121701_1.html";
+ var testUrl2 = "file_bug1121701_2.html";
+
+ var page1LoadCount = 0;
+ let page1Done = {};
+ page1Done.promise = new Promise(resolve => {
+ page1Done.resolve = resolve;
+ });
+ let page2Done = {};
+ page2Done.promise = new Promise(resolve => {
+ page2Done.resolve = resolve;
+ });
+
+ addLoadEvent(async function() {
+
+ // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+ // Acquire storage access permission here so that the BroadcastChannel used to
+ // communicate with the opened windows works in xorigin tests. Otherwise,
+ // the iframe containing this page is isolated from first-party storage access,
+ // which isolates BroadcastChannel communication.
+ if (isXOrigin) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]],
+ });
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await SpecialPowers.addPermission("storageAccessAPI", true, window.location.href);
+ await SpecialPowers.wrap(document).requestStorageAccess();
+ }
+
+ var bc = new BroadcastChannel("file_bug1121701_1");
+ var bc2 = new BroadcastChannel("file_bug1121701_2");
+
+ async function scheduleFinish() {
+ await Promise.all([page1Done.promise, page2Done.promise]);
+ bc2.close();
+ bc.close();
+ SimpleTest.finish();
+ }
+
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "child1PageShow") {
+ ++page1LoadCount;
+ var persisted = msg.persisted;
+ var pageHideAsserts = msg.pageHideAsserts;
+ if (pageHideAsserts) {
+ ok(pageHideAsserts.persisted, "onpagehide: test page 1 should get persisted");
+ is(pageHideAsserts.innerHTML, "modified", "onpagehide: innerHTML text is 'modified'");
+ }
+ if (page1LoadCount == 1) {
+ SimpleTest.executeSoon(function() {
+ is(persisted, false, "Initial page load shouldn't be persisted.");
+ bc.postMessage({command: "setInnerHTML", testUrl2});
+ });
+ } else if (page1LoadCount == 2) {
+ is(persisted, true, "Page load from bfcache should be persisted.");
+ is(msg.innerHTML, "modified", "innerHTML text is 'modified'");
+ bc.postMessage({command: "close"});
+ }
+ } else if (command == "closed") {
+ page1Done.resolve();
+ }
+ }
+ bc2.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "child2PageShow") {
+ bc2.postMessage({command: "setInnerHTML", location: location.href});
+ } else if (command == "onmessage") {
+ page2Done.resolve();
+ }
+ }
+
+ scheduleFinish();
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ window.open(testUrl1, "", "noopener");
+ });
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121701">Mozilla Bug 1121701</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1151421.html b/docshell/test/mochitest/test_bug1151421.html
new file mode 100644
index 0000000000..0738ee783e
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1151421.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151421
+-->
+<head>
+ <title>Test for Bug 1151421</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151421">Mozilla Bug 1151421</a>
+
+<script type="application/javascript">
+
+/** Test for Bug 1151421 */
+SimpleTest.waitForExplicitFinish();
+
+function childLoad() {
+ // Spin the event loop so we leave the onload handler.
+ SimpleTest.executeSoon(childLoad2);
+}
+
+function childLoad2() {
+ let iframe = document.getElementById("iframe");
+ let cw = iframe.contentWindow;
+ let content = cw.document.getElementById("content");
+
+ // Create a function to calculate an invariant.
+ let topPlusOffset = function() {
+ return Math.round(content.getBoundingClientRect().top + cw.pageYOffset);
+ };
+
+ let initialTPO = topPlusOffset();
+
+ // Scroll the iframe to various positions, and check the TPO.
+ // Scrolling down to the bottom will adjust the page offset by a fractional amount.
+ let positions = [-100, 0.17, 0, 1.5, 10.41, 1e6, 12.1];
+
+ // Run some tests with scrollTo() and ensure we have the same invariant after scrolling.
+ positions.forEach(function(pos) {
+ cw.scrollTo(0, pos);
+ is(topPlusOffset(), initialTPO, "Top plus offset should remain invariant across scrolling.");
+ });
+
+ positions.reverse().forEach(function(pos) {
+ cw.scrollTo(0, pos);
+ is(topPlusOffset(), initialTPO, "(reverse) Top plus offset should remain invariant across scrolling.");
+ });
+
+ SimpleTest.finish();
+}
+
+</script>
+
+<!-- When the iframe loads, it calls childLoad(). -->
+<br>
+<iframe height='100px' id='iframe' src='file_bug1151421.html'></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1186774.html b/docshell/test/mochitest/test_bug1186774.html
new file mode 100644
index 0000000000..9ec56baf11
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1186774.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1186774
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1186774</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1186774 */
+
+var child;
+
+function runTest() {
+ child = window.open("file_bug1186774.html", "", "width=100,height=100");
+ child.onload = function() {
+ setTimeout(function() {
+ child.scrollTo(0, 0);
+ child.history.pushState({}, "initial");
+ child.scrollTo(0, 3000);
+ child.history.pushState({}, "scrolled");
+ child.scrollTo(0, 6000);
+ child.history.back();
+ });
+ };
+
+ child.onpopstate = function() {
+ is(Math.round(child.scrollY), 6000, "Shouldn't have scrolled before popstate");
+ child.close();
+ SimpleTest.finish();
+ };
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186774">Mozilla Bug 1186774</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1422334.html b/docshell/test/mochitest/test_bug1422334.html
new file mode 100644
index 0000000000..b525ae1d9c
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1422334.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure that reload after replaceState after 3xx redirect does the right thing.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function() {
+ var ifr = document.querySelector("iframe");
+ var win = ifr.contentWindow;
+ is(win.location.href, location.href.replace(location.search, "")
+ .replace("mochitest/test_bug1422334.html",
+ "navigation/blank.html?x=y"),
+ "Should have the right location on initial load");
+
+ win.history.replaceState(null, '', win.location.pathname);
+ is(win.location.href, location.href.replace(location.search, "")
+ .replace("mochitest/test_bug1422334.html",
+ "navigation/blank.html"),
+ "Should have the right location after replaceState call");
+
+ ifr.onload = function() {
+ is(win.location.href, location.href.replace(location.search, "")
+ .replace("mochitest/test_bug1422334.html",
+ "navigation/blank.html"),
+ "Should have the right location after reload");
+ SimpleTest.finish();
+ }
+ win.location.reload();
+ });
+ </script>
+</head>
+<body>
+<p id="display"><iframe src="bug1422334_redirect.html"></iframe></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1450164.html b/docshell/test/mochitest/test_bug1450164.html
new file mode 100644
index 0000000000..546a988394
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1450164.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1450164
+ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1450164</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1450164 */
+
+ function runTest() {
+ var child = window.open("file_bug1450164.html", "", "width=100,height=100");
+ child.onload = function() {
+ // After the window loads, close it. If we don't crash in debug, consider that a pass.
+ child.close();
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(runTest);
+
+ </script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1450164">Mozilla Bug 1450164</a>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1507702.html b/docshell/test/mochitest/test_bug1507702.html
new file mode 100644
index 0000000000..fd88ee60a5
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1507702.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1507702
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1507702</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <link rel="icon" href="about:crashparent"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1507702">Mozilla Bug 1507702</a>
+<img src="about:crashparent">
+<img src="about:crashcontent">
+<iframe src="about:crashparent"></iframe>
+<iframe src="about:crashcontent"></iframe>
+<script>
+ let urls = ["about:crashparent", "about:crashcontent"];
+ async function testFetch() {
+ const url = urls.shift();
+ if (!url) {
+ return Promise.resolve();
+ }
+
+ let threw;
+ try {
+ await fetch(url);
+ threw = false;
+ } catch (e) {
+ threw = true;
+ }
+
+ ok(threw === true, "fetch should reject");
+ return testFetch();
+ }
+
+ document.body.onload = async () => {
+ for (const url of ["about:crashparent", "about:crashcontent"]) {
+ SimpleTest.doesThrow(() => {
+ top.location.href = url;
+ }, "navigation should throw");
+
+ SimpleTest.doesThrow(() => {
+ location.href = url;
+ }, "navigation should throw");
+ }
+
+ await testFetch();
+ SimpleTest.finish();
+ };
+
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1645781.html b/docshell/test/mochitest/test_bug1645781.html
new file mode 100644
index 0000000000..6cf676b7a9
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1645781.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Test for Bug 1590762</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <form id="form" action="form_submit.sjs" method="POST" target="targetFrame">
+ <input id="input" type="text" name="name" value="">
+ <input id="button" type="submit">
+ </form>
+ <script>
+ "use strict";
+ const PATH = "/tests/docshell/test/mochitest/";
+ const SAME_ORIGIN = new URL(PATH, window.location.origin);;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const CROSS_ORIGIN_1 = new URL(PATH, "http://test1.example.com/");
+ const CROSS_ORIGIN_2 = new URL(PATH, "https://example.com/");
+ const TARGET = "ping.html";
+ const ACTION = "form_submit.sjs";
+
+ function generateBody(size) {
+ let data = new Uint8Array(size);
+ for (let i = 0; i < size; ++i) {
+ data[i] = 97 + Math.random() * (123 - 97);
+ }
+
+ return new TextDecoder().decode(data);
+ }
+
+ async function withFrame(url) {
+ info("Creating frame");
+ let frame = document.createElement('iframe');
+ frame.name = "targetFrame";
+
+ return new Promise(resolve => {
+ addEventListener('message', async function({source}) {
+ info("Frame loaded");
+ if (frame.contentWindow == source) {
+ resolve(frame);
+ }
+ }, { once: true });
+ frame.src = url;
+ document.body.appendChild(frame);
+ });
+ }
+
+ function click() {
+ synthesizeMouse(document.getElementById('button'), 5, 5, {});
+ }
+
+ function* spec() {
+ let urls = [SAME_ORIGIN, CROSS_ORIGIN_1, CROSS_ORIGIN_2];
+ for (let action of urls) {
+ for (let target of urls) {
+ yield { action: new URL(ACTION, action),
+ target: new URL(TARGET, target) };
+ }
+ }
+ }
+
+ info("Starting tests");
+ let form = document.getElementById('form');
+
+ // The body of the POST needs to be large to trigger this.
+ // 1024*1024 seems to be enough, but scaling to get a margin.
+ document.getElementById('input').value = generateBody(1024*1024);
+ for (let { target, action } of spec()) {
+ add_task(async function runTest() {
+ info(`Running test ${target} with ${action}`);
+ form.action = action;
+ let frame = await withFrame(target);
+ await new Promise(resolve => {
+ addEventListener('message', async function() {
+ info("Form loaded");
+ frame.remove();
+ resolve();
+ }, { once: true });
+
+ click();
+ });
+
+ ok(true, `Submitted to ${origin} with target ${action}`)
+ });
+ };
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1729662.html b/docshell/test/mochitest/test_bug1729662.html
new file mode 100644
index 0000000000..ec43508494
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1729662.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test back/forward after pushState</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("Need to wait to make sure an event does not fire");
+
+ async function runTest() {
+ let win = window.open();
+ let goneBackAndForwardOnce = new Promise((resolve) => {
+ let timeoutID;
+
+ // We should only get one load event in win.
+ let bc = new BroadcastChannel("bug1729662");
+ bc.addEventListener("message", () => {
+ bc.addEventListener("message", () => {
+ clearTimeout(timeoutID);
+ resolve(false);
+ });
+ }, { once: true });
+
+ let goneBack = false, goneForward = false;
+ win.addEventListener("popstate", ({ state }) => {
+ // We should only go back and forward once, if we get another
+ // popstate after that then we should fall through to the
+ // failure case below.
+ if (!(goneBack && goneForward)) {
+ // Check if this is the popstate for the forward (the one for
+ // back will have state == undefined).
+ if (state == 1) {
+ ok(goneBack, "We should have gone back before going forward");
+
+ goneForward = true;
+
+ // Wait a bit to make sure there are no more popstate events.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ timeoutID = setTimeout(resolve, 1000, true);
+
+ return;
+ }
+
+ // Check if we've gone back once before, if we get another
+ // popstate after that then we should fall through to the
+ // failure case below.
+ if (!goneBack) {
+ goneBack = true;
+
+ return;
+ }
+ }
+
+ clearTimeout(timeoutID);
+ resolve(false);
+ });
+ });
+
+ win.location = "file_bug1729662.html";
+
+ ok(await goneBackAndForwardOnce, "Stopped navigating history");
+
+ win.close();
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1740516.html b/docshell/test/mochitest/test_bug1740516.html
new file mode 100644
index 0000000000..b54932c736
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1740516.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test pageshow event order for iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ function waitForPageShow(outer, inner) {
+ return new Promise((resolve) => {
+ let results = [];
+ outer.addEventListener("message", ({ data: persisted }) => {
+ results.push({ name: outer.name, persisted });
+ if (results.length == 2) {
+ resolve(results);
+ }
+ }, { once: true });
+ inner.addEventListener("message", ({ data: persisted }) => {
+ results.push({ name: inner.name, persisted });
+ if (results.length == 2) {
+ resolve(results);
+ }
+ }, { once: true });
+ });
+ }
+ async function runTest() {
+ let outerBC = new BroadcastChannel("bug1740516_1");
+ let innerBC = new BroadcastChannel("bug1740516_1_inner");
+
+ let check = waitForPageShow(outerBC, innerBC).then(([first, second]) => {
+ is(first.name, "bug1740516_1_inner", "Should get pageShow from inner iframe page first.");
+ ok(!first.persisted, "First navigation shouldn't come from BFCache.");
+ is(second.name, "bug1740516_1", "Should get pageShow from outer page second.");
+ ok(!second.persisted, "First navigation shouldn't come from BFCache.");
+ }, () => {
+ ok(false, "The promises should not be rejected.");
+ });
+ window.open("file_bug1740516_1.html", "", "noopener");
+ await check;
+
+ check = waitForPageShow(outerBC, innerBC).then(([first, second]) => {
+ is(first.name, "bug1740516_1_inner", "Should get pageShow from inner iframe page first.");
+ ok(first.persisted, "Second navigation should come from BFCache");
+ is(second.name, "bug1740516_1", "Should get pageShow from outer page second.");
+ ok(second.persisted, "Second navigation should come from BFCache");
+ }, () => {
+ ok(false, "The promises should not be rejected.");
+ });
+ outerBC.postMessage("navigate");
+ await check;
+
+ check = waitForPageShow(outerBC, innerBC).then(([first, second]) => {
+ is(first.name, "bug1740516_1_inner", "Should get pageShow from inner iframe page first.");
+ ok(!first.persisted, "Third navigation should not come from BFCache");
+ is(second.name, "bug1740516_1", "Should get pageShow from outer page second.");
+ ok(!second.persisted, "Third navigation should not come from BFCache");
+ }, () => {
+ ok(false, "The promises should not be rejected.");
+ });
+ outerBC.postMessage("block_bfcache_and_navigate");
+ await check;
+
+ outerBC.postMessage("close");
+
+ outerBC.close();
+ innerBC.close();
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1741132.html b/docshell/test/mochitest/test_bug1741132.html
new file mode 100644
index 0000000000..1ae9727d9c
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1741132.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form restoration for no-store pages</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ // The number of entries which we keep in the BFCache (see nsSHistory.h).
+ const VIEWER_WINDOW = 3;
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ let bc = new BroadcastChannel("bug1741132");
+
+ // Setting the pref to 0 should evict all content viewers.
+ let load = SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionhistory.max_total_viewers", 0]],
+ }).then(() => {
+ // Set the pref to VIEWER_WINDOW + 2 now, to be sure we
+ // could fit all entries.
+ SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionhistory.max_total_viewers", VIEWER_WINDOW + 2]],
+ });
+ }).then(() => {
+ return new Promise(resolve => {
+ bc.addEventListener("message", resolve, { once: true });
+ window.open("file_bug1741132.html", "", "noopener");
+ });
+ });
+ // We want to try to keep one entry too many in the BFCache,
+ // so we ensure that there's at least VIEWER_WINDOW + 2
+ // entries in session history (with one for the displayed
+ // page).
+ for (let i = 0; i < VIEWER_WINDOW + 2; ++i) {
+ load = load.then(() => {
+ return new Promise((resolve) => {
+ bc.addEventListener("message", resolve, { once: true });
+ bc.postMessage({ cmd: "load", arg: `file_bug1741132.html?${i}` });
+ });
+ });
+ }
+ load.then(() => {
+ return new Promise((resolve) => {
+ bc.addEventListener("message", ({ data: persisted }) => {
+ resolve(persisted);
+ }, { once: true });
+ // Go back past the first entry that should be in the BFCache.
+ bc.postMessage({ cmd: "go", arg: -(VIEWER_WINDOW + 1) });
+ });
+ }).then((persisted) => {
+ ok(!persisted, "Only 3 pages should be kept in the BFCache");
+ }).then(() => {
+ return new Promise((resolve) => {
+ bc.addEventListener("message", ({ data: persisted }) => {
+ resolve(persisted);
+ }, { once: true });
+ // Go forward to the first entry that should be in the BFCache.
+ bc.postMessage({ cmd: "go", arg: 1 });
+ });
+ }).then((persisted) => {
+ ok(persisted, "3 pages should be kept in the BFCache");
+
+ bc.postMessage("close");
+
+ bc.close();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1742865.html b/docshell/test/mochitest/test_bug1742865.html
new file mode 100644
index 0000000000..c8f9a4eca3
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1742865.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Auto refreshing pages shouldn't add an entry to session history</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ const REFRESH_REDIRECT_TIMER = 15;
+
+ // 2 tests (same and cross origin) consisting of 2 refreshes of maximum 1 seconds
+ // 2 tests (same and cross origin) consisting of 2 refreshes of REFRESH_REDIRECT_TIMER seconds
+ // => We need (2 * 1) + (2 * 15) seconds
+ SimpleTest.requestLongerTimeout(3);
+ SimpleTest.waitForExplicitFinish();
+
+ const SJS = new URL("file_bug1742865.sjs", location.href);
+ const SJS_OUTER = new URL("file_bug1742865_outer.sjs", location.href);
+ const SCROLL = 500;
+
+ let tolerance;
+ function setup() {
+ return SpecialPowers.spawn(window.top, [], () => {
+ return SpecialPowers.getDOMWindowUtils(content.window).getResolution();
+ }).then(resolution => {
+ // Allow a half pixel difference if the top document's resolution is lower
+ // than 1.0 because the scroll position is aligned with screen pixels
+ // instead of CSS pixels.
+ tolerance = resolution < 1.0 ? 0.5 : 0.0;
+ });
+ }
+
+ function checkScrollPosition(scrollPosition, shouldKeepScrollPosition) {
+ isfuzzy(scrollPosition, shouldKeepScrollPosition ? SCROLL : 0, tolerance,
+ `Scroll position ${shouldKeepScrollPosition ? "should" : "shouldn't"} be maintained for meta refresh`);
+ }
+
+ function openWindowAndCheckRefresh(url, params, shouldAddToHistory, shouldKeepScrollPosition) {
+ info(`Running test for ${JSON.stringify(params)}`);
+
+ url = new URL(url);
+ Object.entries(params).forEach(([k, v]) => { url.searchParams.append(k, v) });
+ url.searchParams.append("scrollTo", SCROLL);
+
+ let resetURL = new URL(SJS);
+ resetURL.search = "?reset";
+ return fetch(resetURL).then(() => {
+ return new Promise((resolve) => {
+ let count = 0;
+ window.addEventListener("message", function listener({ data: { commandType, commandData = {} } }) {
+ if (commandType == "onChangedInputValue") {
+ let { historyLength, inputValue } = commandData;
+
+ if (shouldAddToHistory) {
+ is(historyLength, count, "Auto-refresh should add entries to session history");
+ } else {
+ is(historyLength, 1, "Auto-refresh shouldn't add entries to session history");
+ }
+
+ is(inputValue, "1234", "Input's value should have been changed");
+
+ win.postMessage("loadNext", "*");
+ return;
+ }
+
+ is(commandType, "pageShow", "Unknown command type");
+
+ let { inputValue, scrollPosition } = commandData;
+
+ switch (++count) {
+ // file_bug1742865.sjs causes 3 loads:
+ // * first load, returns first meta refresh
+ // * second load, caused by first meta refresh, returns second meta refresh
+ // * third load, caused by second meta refresh, doesn't return a meta refresh
+ case 2:
+ checkScrollPosition(scrollPosition, shouldKeepScrollPosition);
+ break;
+ case 3:
+ checkScrollPosition(scrollPosition, shouldKeepScrollPosition);
+ win.postMessage("changeInputValue", "*");
+ break;
+ case 4:
+ win.postMessage("back", "*");
+ break;
+ case 5:
+ is(inputValue, "1234", "Entries for auto-refresh should be attached to session history");
+ checkScrollPosition(scrollPosition, shouldKeepScrollPosition);
+ removeEventListener("message", listener);
+ win.close();
+ resolve();
+ break;
+ }
+ });
+ let win = window.open(url);
+ });
+ });
+ }
+
+ function doTest(seconds, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition) {
+ let params = {
+ seconds,
+ crossOrigin,
+ };
+
+ return openWindowAndCheckRefresh(SJS, params, shouldAddToHistory, shouldKeepScrollPosition).then(() =>
+ openWindowAndCheckRefresh(SJS_OUTER, params, shouldAddToHistory, shouldKeepScrollPosition)
+ );
+ }
+
+ async function runTest() {
+ const FAST = Math.min(1, REFRESH_REDIRECT_TIMER);
+ const SLOW = REFRESH_REDIRECT_TIMER + 1;
+ let tests = [
+ // [ time, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition ]
+ [ FAST, false, false, true ],
+ [ FAST, true, false, false ],
+ [ SLOW, false, false, true ],
+ [ SLOW, true, true, false ],
+ ];
+
+ await setup();
+
+ for (let [ time, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition ] of tests) {
+ await doTest(time, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition);
+ }
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1743353.html b/docshell/test/mochitest/test_bug1743353.html
new file mode 100644
index 0000000000..a5d88df3f6
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1743353.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test back/forward after pushState</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ let bc = new BroadcastChannel("bug1743353");
+ new Promise((resolve) => {
+ bc.addEventListener("message", () => {
+ resolve();
+ }, { once: true });
+
+ window.open("file_bug1743353.html", "", "noopener");
+ }).then(() => {
+ return new Promise(resolve => {
+ bc.addEventListener("message", () => {
+ resolve();
+ }, { once: true });
+
+ bc.postMessage("load");
+ })
+ }).then(() => {
+ return new Promise(resolve => {
+ let results = [];
+ bc.addEventListener("message", function listener({ data }) {
+ results.push(data);
+ if (results.length == 3) {
+ bc.removeEventListener("message", listener);
+ resolve(results);
+ }
+ });
+
+ bc.postMessage("back");
+ });
+ }).then((results) => {
+ is(results[0], "pagehide", "First event should be 'pagehide'.");
+ is(results[1], "unload", "Second event should be 'unload'.");
+ is(results[2], "pageshow", "Third event should be 'pageshow'.");
+
+ bc.postMessage("close");
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1747033.html b/docshell/test/mochitest/test_bug1747033.html
new file mode 100644
index 0000000000..539b78fec0
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1747033.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test history after loading multipart</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ let bc = new BroadcastChannel("bug1747033");
+ new Promise(resolve => {
+ bc.addEventListener("message", ({ data: { historyLength } }) => {
+ is(historyLength, 1, "Correct length for first normal load.");
+
+ resolve();
+ }, { once: true });
+
+ window.open("file_bug1747033.sjs", "", "noopener");
+ }).then(() => {
+ return new Promise(resolve => {
+ let loaded = 0;
+ bc.addEventListener("message", function listener({ data: { historyLength } }) {
+ ++loaded;
+
+ is(historyLength, 2, `Correct length for multipart load ${loaded}.`);
+
+ // We want 3 parts in total.
+ if (loaded < 3) {
+ if (loaded == 2) {
+ // We've had 2 parts, make the server send the last part.
+ fetch("file_bug1747033.sjs?sendLastPart");
+ } else {
+ fetch("file_bug1747033.sjs?sendNextPart");
+ }
+ return;
+ }
+
+ bc.removeEventListener("message", listener);
+ resolve();
+ });
+
+ bc.postMessage({ cmd: "load", arg: "file_bug1747033.sjs?multipart" });
+ });
+ }).then(() => {
+ return new Promise(resolve => {
+ bc.addEventListener("message", ({ data: { historyLength } }) => {
+ is(historyLength, 2, "Correct length after calling replaceState in multipart.");
+
+ resolve();
+ }, { once: true });
+
+ bc.postMessage({ cmd: "replaceState", arg: "file_bug1747033.sjs?replaced" });
+ });
+ }).then(() => {
+ return new Promise(resolve => {
+ bc.addEventListener("message", ({ data: { historyLength } }) => {
+ is(historyLength, 3, "Correct length for first normal load after multipart.");
+
+ resolve();
+ }, { once: true });
+
+ bc.postMessage({ cmd: "load", arg: "file_bug1747033.sjs" });
+ });
+ }).then(() => {
+ return new Promise(resolve => {
+ let goneBack = 0;
+ bc.addEventListener("message", function listener({ data: { historyLength } }) {
+ ++goneBack;
+
+ is(historyLength, 3, "Correct length after going back.");
+
+ if (goneBack == 1) {
+ bc.postMessage({ cmd: "back" });
+ } else if (goneBack == 2) {
+ bc.removeEventListener("message", listener);
+ resolve();
+ }
+ });
+
+ bc.postMessage({ cmd: "back" });
+ });
+ }).then(() => {
+ bc.postMessage({ cmd: "close" });
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1773192.html b/docshell/test/mochitest/test_bug1773192.html
new file mode 100644
index 0000000000..d4c42dc1a7
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1773192.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test referrer with going back</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ // file_bug1773192_1.html will send a message with some data on pageshow.
+ function waitForData(bc) {
+ return new Promise(resolve => {
+ bc.addEventListener(
+ "message",
+ ({ data }) => {
+ resolve(data);
+ },
+ { once: true }
+ );
+ });
+ }
+ async function runTest() {
+ let bc = new BroadcastChannel("bug1743353");
+
+ let getData = waitForData(bc);
+
+ window.open("file_bug1773192_1.html", "", "noreferrer");
+
+ await getData.then(({ referrer }) => {
+ is(referrer, "", "Referrer should be empty at first.");
+ });
+
+ getData = waitForData(bc);
+
+ // When file_bug1773192_1.html receives this message it will navigate to
+ // file_bug1773192_2.html. file_bug1773192_2.html removes itself from
+ // history with replaceState and submits a form with the POST method to
+ // file_bug1773192_3.sjs. file_bug1773192_3.sjs goes back in history.
+ // We should end up back at file_bug1773192_1.html, which will send a
+ // message with some data on pageshow.
+ bc.postMessage("next");
+
+ await getData.then(({ location, referrer }) => {
+ let firstURL = new URL("file_bug1773192_1.html", location).toString();
+ is(location, firstURL, "Location should be the first page again.");
+ is(referrer, firstURL, "Referrer should also be the first page.");
+ });
+
+ bc.postMessage("close");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug1850335.html b/docshell/test/mochitest/test_bug1850335.html
new file mode 100644
index 0000000000..546aab59c8
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1850335.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test referrer with going back</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ function waitForMessage(bc) {
+ return new Promise(resolve => {
+ bc.addEventListener("message", ({ data }) => { resolve(data); }, { once: true });
+ });
+ }
+
+ async function runTest() {
+ let bc = new BroadcastChannel("bug1850335");
+
+ // Load the first page.
+ let waitForPage1 = waitForMessage(bc);
+ window.open("file_bug1850335_1.html", "_blank", "noopener");
+ let { persisted, value: initial } = await waitForPage1;
+
+ ok(!persisted, "Loaded first page");
+ is(initial, "", "Initial value is empty string");
+
+ // Set the value of the input element in the first page.
+ let waitForValue = waitForMessage(bc);
+ bc.postMessage({ cmd: "setValue", arg: "ok" });
+ let { value: expected }= await waitForValue;
+ is(expected, "ok", "Loaded first page");
+
+ // Load the second page (same origin with the first page).
+ let waitForPage2 = waitForMessage(bc);
+ bc.postMessage({ cmd: "load", arg: "file_bug1850335_2.html" });
+ ({ persisted } = await waitForPage2);
+
+ ok(!persisted, "Loaded second page (same-origin)");
+
+ // Load the third page (cross origin with the first and second pages). The
+ // third page will immediately do |history.back()| to go back to the
+ // second page.
+ waitForPage2 = waitForMessage(bc);
+ const crossOrigin = new URL("file_bug1850335_3.html", `https://example.com${location.pathname}`);
+ bc.postMessage({ cmd: "load", arg: crossOrigin.href });
+ ({ persisted } = await waitForPage2);
+
+ ok(!persisted, "Second page should not be in the BFCache");
+
+ // Go back to the first page.
+ waitForPage1 = waitForMessage(bc);
+ bc.postMessage({ cmd: "back" });
+ let { persisted: fromBFCache, value: result } = await waitForPage1;
+
+ ok(fromBFCache, "Page came from BFCache");
+ is(result, expected, "Value wasn't cleared");
+
+ bc.postMessage({ cmd: "close" });
+
+ bc.close();
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="runTest();">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug385434.html b/docshell/test/mochitest/test_bug385434.html
new file mode 100644
index 0000000000..bc82de9daf
--- /dev/null
+++ b/docshell/test/mochitest/test_bug385434.html
@@ -0,0 +1,211 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=385434
+-->
+<head>
+ <title>Test for Bug 385434</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=385434">Mozilla Bug 385434</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="frame" style="height:100px; width:100px; border:0"></iframe>
+ <div id="status" style="display: none"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 385434 */
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+SimpleTest.expectAssertions(0, 1); // bug 1333702
+
+var gNumHashchanges = 0;
+var gCallbackOnIframeLoad = false;
+var gSampleEvent;
+
+function statusMsg(msg) {
+ var msgElem = document.createElement("p");
+ msgElem.appendChild(document.createTextNode(msg));
+
+ document.getElementById("status").appendChild(msgElem);
+}
+
+function longWait() {
+ setTimeout(function() { gGen.next(); }, 1000);
+}
+
+// onIframeHashchange, onIframeLoad, and onIframeScroll are all called by the
+// content we load into our iframe in order to notify the parent frame of an
+// event which was fired.
+function onIframeHashchange() {
+ gNumHashchanges++;
+ gGen.next();
+}
+
+function onIframeLoad() {
+ if (gCallbackOnIframeLoad) {
+ gCallbackOnIframeLoad = false;
+ gGen.next();
+ }
+}
+
+function onIframeScroll() {
+ is(gNumHashchanges, 0, "onscroll should fire before onhashchange.");
+}
+
+function enableIframeLoadCallback() {
+ gCallbackOnIframeLoad = true;
+}
+
+function noEventExpected(msg) {
+ is(gNumHashchanges, 0, msg);
+
+ // Even if there's an error, set gNumHashchanges to 0 so other tests don't
+ // fail.
+ gNumHashchanges = 0;
+}
+
+function eventExpected(msg) {
+ is(gNumHashchanges, 1, msg);
+
+ // Eat up this event, whether the test above was true or not
+ gNumHashchanges = 0;
+}
+
+/*
+ * The hashchange event is dispatched asynchronously, so if we want to observe
+ * it, we have to yield within run_test(), transferring control back to the
+ * event loop.
+ *
+ * When we're expecting our iframe to observe a hashchange event after we poke
+ * it, we just yield and wait for onIframeHashchange() to call gGen.next() and
+ * wake us up.
+ *
+ * When we're testing to ensure that the iframe doesn't dispatch a hashchange
+ * event, we try to hook onto the iframe's load event. We call
+ * enableIframeLoadCallback(), which causes onIframeLoad() to call gGen.next()
+ * upon the next observed load. After we get our callback, we check that a
+ * hashchange didn't occur.
+ *
+ * We can't always just wait for page load in order to observe that a
+ * hashchange didn't happen. In these cases, we call longWait() and yield
+ * until either a hashchange occurs or longWait's callback is scheduled. This
+ * is something of a hack; it's entirely possible that longWait won't wait long
+ * enough, and we won't observe what should have been a failure of the test.
+ * But it shouldn't happen that good code will randomly *fail* this test.
+ */
+function* run_test() {
+ /*
+ * TEST 1 tests that:
+ * <body onhashchange = ... > works,
+ * the event is (not) fired at the correct times
+ */
+ var frame = document.getElementById("frame");
+ var frameCw = frame.contentWindow;
+
+ enableIframeLoadCallback();
+ frameCw.document.location = "file_bug385434_1.html";
+ // Wait for the iframe to load and for our callback to fire
+ yield undefined;
+
+ noEventExpected("No hashchange expected initially.");
+
+ sendMouseEvent({type: "click"}, "link1", frameCw);
+ yield undefined;
+ eventExpected("Clicking link1 should trigger a hashchange.");
+
+ sendMouseEvent({type: "click"}, "link1", frameCw);
+ longWait();
+ yield undefined;
+ // succeed if a hashchange event wasn't triggered while we were waiting
+ noEventExpected("Clicking link1 again should not trigger a hashchange.");
+
+ sendMouseEvent({type: "click"}, "link2", frameCw);
+ yield undefined;
+ eventExpected("Clicking link2 should trigger a hashchange.");
+
+ frameCw.history.go(-1);
+ yield undefined;
+ eventExpected("Going back should trigger a hashchange.");
+
+ frameCw.history.go(1);
+ yield undefined;
+ eventExpected("Going forward should trigger a hashchange.");
+
+ // window.location has a trailing '#' right now, so we append "link1", not
+ // "#link1".
+ frameCw.window.location = frameCw.window.location + "link1";
+ yield undefined;
+ eventExpected("Assigning to window.location should trigger a hashchange.");
+
+ // Set up history in the iframe which looks like:
+ // file_bug385434_1.html#link1
+ // file_bug385434_2.html
+ // file_bug385434_1.html#foo <-- current page
+ enableIframeLoadCallback();
+ frameCw.window.location = "file_bug385434_2.html";
+ yield undefined;
+
+ enableIframeLoadCallback();
+ frameCw.window.location = "file_bug385434_1.html#foo";
+ yield undefined;
+
+ // Now when we do history.go(-2) on the frame, it *shouldn't* fire a
+ // hashchange. Although the URIs differ only by their hashes, they belong to
+ // two different Documents.
+ frameCw.history.go(-2);
+ longWait();
+ yield undefined;
+ noEventExpected("Moving between different Documents shouldn't " +
+ "trigger a hashchange.");
+
+ /*
+ * TEST 2 tests that:
+ * <frameset onhashchange = ... > works,
+ * the event is targeted at the window object
+ * the event's cancelable, bubbles settings are correct
+ */
+
+ enableIframeLoadCallback();
+ frameCw.document.location = "file_bug385434_2.html";
+ yield undefined;
+
+ frameCw.document.location = "file_bug385434_2.html#foo";
+ yield undefined;
+
+ eventExpected("frame onhashchange should fire events.");
+ // iframe should set gSampleEvent
+ is(gSampleEvent.target, frameCw,
+ "The hashchange event should be targeted to the window.");
+ is(gSampleEvent.type, "hashchange",
+ "Event type should be 'hashchange'.");
+ is(gSampleEvent.cancelable, false,
+ "The hashchange event shouldn't be cancelable.");
+ is(gSampleEvent.bubbles, false,
+ "The hashchange event should not bubble.");
+
+ /*
+ * TEST 3 tests that:
+ * hashchange is dispatched if the current document readyState is
+ * not "complete" (bug 504837).
+ */
+ frameCw.document.location = "file_bug385434_3.html";
+ yield undefined;
+ eventExpected("Hashchange should fire even if the document " +
+ "hasn't finished loading.");
+
+ SimpleTest.finish();
+}
+
+var gGen = run_test();
+gGen.next();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug387979.html b/docshell/test/mochitest/test_bug387979.html
new file mode 100644
index 0000000000..0aca2ee89b
--- /dev/null
+++ b/docshell/test/mochitest/test_bug387979.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=387979
+-->
+<head>
+ <title>Test for Bug 387979</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=387979">Mozilla Bug 387979</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 387979 */
+function a(s) {
+ var r;
+ try { r = frames[0].document.body; } catch (e) { r = e; }
+ is(r instanceof frames[0].HTMLBodyElement, true, "Can't get body" + s);
+}
+var p = 0;
+function b() {
+ switch (++p) {
+ case 1:
+ frames[0].location = "about:blank";
+ break;
+ case 2:
+ a("before reload");
+ frames[0].location.reload();
+ break;
+ case 3:
+ a("after reload");
+ SimpleTest.finish();
+ break;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<p id="display">
+ <iframe onload="b()"></iframe>
+ <pre id="p">-</pre>
+</p>
+</body>
+</html>
+
diff --git a/docshell/test/mochitest/test_bug402210.html b/docshell/test/mochitest/test_bug402210.html
new file mode 100644
index 0000000000..326f98cf9f
--- /dev/null
+++ b/docshell/test/mochitest/test_bug402210.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+While working on bug 402210, it came up that the code was doing
+
+a.href = proto + host
+
+which technically produces "https:host" instead of "https://host" and
+that the code was relying on href's setting having fixup behaviour
+for this kind of thing.
+
+If we rely on it, we might as well test for it, even if it isn't the
+problem 402210 was meant to fix.
+
+https://bugzilla.mozilla.org/show_bug.cgi?id=402210
+-->
+<head>
+ <title>Test for Bug 402210</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402210">Mozilla Bug 402210</a>
+<p id="display">
+ <a id="testlink">Test Link</a>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ $("testlink").href = "https:example.com";
+ is($("testlink").href, "https://example.com/", "Setting href on an anchor tag should fixup missing slashes after https protocol");
+
+ $("testlink").href = "ftp:example.com";
+ is($("testlink").href, "ftp://example.com/", "Setting href on an anchor tag should fixup missing slashes after non-http protocol");
+
+ SimpleTest.finish();
+}
+
+addLoadEvent(runTest);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/docshell/test/mochitest/test_bug404548.html b/docshell/test/mochitest/test_bug404548.html
new file mode 100644
index 0000000000..a8a773dce5
--- /dev/null
+++ b/docshell/test/mochitest/test_bug404548.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=404548
+-->
+<head>
+ <title>Test for Bug 404548</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=404548">Mozilla Bug 404548</a>
+<p id="display">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 404548 */
+var firstRemoved = false;
+var secondHidden = false;
+
+SimpleTest.waitForExplicitFinish();
+
+var w = window.open("bug404548-subframe.html", "", "width=10,height=10");
+
+function finishTest() {
+ is(firstRemoved, true, "Should have removed iframe from the DOM");
+ is(secondHidden, true, "Should have fired pagehide on second kid");
+ w.close();
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/docshell/test/mochitest/test_bug413310.html b/docshell/test/mochitest/test_bug413310.html
new file mode 100644
index 0000000000..4299605575
--- /dev/null
+++ b/docshell/test/mochitest/test_bug413310.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=413310
+-->
+<head>
+ <title>Test for Bug 413310</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=413310">Mozilla Bug 413310</a>
+<p id="display">
+<script class="testbody" type="text/javascript">
+
+if (navigator.platform.startsWith("Mac")) {
+ SimpleTest.expectAssertions(0, 2);
+} else {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+/** Test for Bug 413310 */
+
+// NOTE: If we ever make subframes do bfcache stuff, this test will need to be
+// modified accordingly! It assumes that subframes do NOT get bfcached.
+var onloadCount = 0;
+
+var step = -1; // One increment will come from the initial subframe onload.
+ // Note that this script should come before the subframe,
+ // so that doNextStep is defined when its onload handler fires.
+
+var textContent;
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(doNextStep);
+
+function doNextStep() {
+ ++step;
+ switch (step) {
+ case 1:
+ is(onloadCount, 1, "Loaded initial page");
+ is($("i").contentWindow.location.href,
+ location.href.replace(/test_bug413310.html/,
+ "bug413310-subframe.html"),
+ "Unexpected subframe location after initial load");
+ $("i").contentDocument.forms[0].submit();
+ break;
+ case 2:
+ is(onloadCount, 2, "Loaded POST result");
+
+ is($("i").contentWindow.location.href,
+ location.href.replace(/test_bug413310.html/,
+ "bug413310-post.sjs"),
+ "Unexpected subframe location after POST load");
+
+ textContent = $("i").contentDocument.body.textContent;
+ isDeeply(textContent.match(/^POST /), ["POST "], "Not a POST?");
+
+ $("i").contentWindow.location.hash = "foo";
+ setTimeout(doNextStep, 0);
+ break;
+ case 3:
+ is(onloadCount, 2, "Anchor scroll should not fire onload");
+ is($("i").contentWindow.location.href,
+ location.href.replace(/test_bug413310.html/,
+ "bug413310-post.sjs#foo"),
+ "Unexpected subframe location after anchor scroll");
+ is(textContent, $("i").contentDocument.body.textContent,
+ "Did a load when scrolling?");
+ $("i").contentWindow.location.href = "bug413310-subframe.html";
+ break;
+ case 4:
+ is(onloadCount, 3, "Done new load");
+ is($("i").contentWindow.location.href,
+ location.href.replace(/test_bug413310.html/,
+ "bug413310-subframe.html"),
+ "Unexpected subframe location after new load");
+ history.back();
+ break;
+ case 5:
+ is(onloadCount, 4,
+ "History traversal didn't fire onload: bfcache issues!");
+ is($("i").contentWindow.location.href,
+ location.href.replace(/test_bug413310.html/,
+ "bug413310-post.sjs#foo"),
+ "Unexpected subframe location");
+ is(textContent, $("i").contentDocument.body.textContent,
+ "Did a load when going back?");
+ SimpleTest.finish();
+ break;
+ }
+}
+</script>
+<!-- Use a timeout in onload so that we don't do a load immediately inside onload -->
+<iframe id="i" src="bug413310-subframe.html" onload="setTimeout(doNextStep, 20)">
+</iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
+
diff --git a/docshell/test/mochitest/test_bug475636.html b/docshell/test/mochitest/test_bug475636.html
new file mode 100644
index 0000000000..fb1827ad04
--- /dev/null
+++ b/docshell/test/mochitest/test_bug475636.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=475636
+Test that refresh to data: URIs don't inherit the principal
+-->
+<head>
+ <title>Test for Bug 475636</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="gen.next()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=475636">Mozilla Bug 475636</a>
+
+<div id="content" style="display: none">
+
+</div>
+<iframe id=loader></iframe>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var gen = runTests();
+
+window.private = 42;
+
+window.addEventListener("message", function(e) {
+ gen.next(e.data);
+});
+
+var url = "file_bug475636.sjs?";
+
+function* runTests() {
+ var loader = document.getElementById("loader");
+ for (var testNum = 1; ; ++testNum) {
+ loader.src = url + testNum;
+ let res = (yield);
+ if (res == "done") {
+ SimpleTest.finish();
+ return;
+ }
+ is(res, "pass");
+ }
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug509055.html b/docshell/test/mochitest/test_bug509055.html
new file mode 100644
index 0000000000..57ede19b43
--- /dev/null
+++ b/docshell/test/mochitest/test_bug509055.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=509055
+-->
+<head>
+ <title>Test for Bug 509055</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=509055">Mozilla Bug 509055</a>
+<p id="display"></p>
+<div id="status"></div>
+<div id="content">
+</div>
+<pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 509055 */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var gGen;
+
+ function shortWait() {
+ setTimeout(function() { gGen.next(); }, 0, false);
+ }
+
+ function onChildHashchange(e) {
+ // gGen might be undefined when we refresh the page, so we have to check here
+ dump("onChildHashchange() called.\n");
+ if (gGen)
+ gGen.next();
+ }
+
+ function onChildLoad(e) {
+ if (gGen)
+ gGen.next();
+ }
+
+ async function* runTest() {
+ var popup = window.open("file_bug509055.html", "popup 0",
+ "height=200,width=200,location=yes," +
+ "menubar=yes,status=yes,toolbar=yes,dependent=yes");
+ popup.hashchangeCallback = onChildHashchange;
+ popup.onload = onChildLoad;
+ dump("Waiting for initial load.\n");
+ yield undefined;
+
+ // Without this wait, the change to location.hash below doesn't create a
+ // SHEntry or enable the back button.
+ shortWait();
+ dump("Got initial load. Spinning event loop.\n");
+ yield undefined;
+
+ popup.location.hash = "#1";
+ dump("Waiting for hashchange.\n");
+ yield undefined;
+
+ popup.history.back();
+ dump("Waiting for second hashchange.\n");
+ yield undefined; // wait for hashchange
+
+ popup.document.title = "Changed";
+
+ // Wait for listeners to be notified of the title change.
+ shortWait();
+ dump("Got second hashchange. Spinning event loop.\n");
+ yield undefined;
+
+ let sheTitle = "";
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ var sh = SpecialPowers.wrap(popup)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .sessionHistory;
+
+ // Get the title of the inner popup's current SHEntry
+ sheTitle = sh.legacySHistory.getEntryAtIndex(sh.index).title;
+ } else {
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("getTitle", browsingContext => {
+ // eslint-disable-next-line no-shadow
+ let sh = browsingContext.sessionHistory;
+ let title = sh.getEntryAtIndex(sh.index).title;
+ sendAsyncMessage("title", title);
+ });
+ });
+
+ let p = chromeScript.promiseOneMessage("title");
+ let browsingContext = SpecialPowers.wrap(popup)
+ .docShell.browsingContext;
+ chromeScript.sendAsyncMessage("getTitle", browsingContext);
+ sheTitle = await p;
+ chromeScript.destroy();
+ }
+ is(sheTitle, "Changed", "SHEntry's title should change when we change.");
+
+ popup.close();
+
+ SimpleTest.executeSoon(SimpleTest.finish);
+ }
+
+ window.addEventListener("load", function() {
+ gGen = runTest();
+ gGen.next();
+ });
+
+ </script>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug511449.html b/docshell/test/mochitest/test_bug511449.html
new file mode 100644
index 0000000000..da95909d1c
--- /dev/null
+++ b/docshell/test/mochitest/test_bug511449.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=511449
+-->
+<head>
+ <title>Test for Bug 511449</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=511449">Mozilla Bug 511449</a>
+<p id="display"></p>
+<div id="status"></div>
+<div id="content">
+</div>
+<input type="text" id="input">
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 511449 */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+window.addEventListener("load", runTest);
+
+var win = null;
+
+function runTest() {
+ document.getElementById("input").focus();
+ win = window.open("file_bug511449.html", "");
+ SimpleTest.waitForFocus(runNextTest, win);
+}
+
+function runNextTest() {
+ var didClose = false;
+ win.onunload = function() {
+ didClose = true;
+ };
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_W, {metaKey: 1}, "w", "w");
+
+ setTimeout(function() {
+ ok(didClose, "Cmd+W should have closed the tab");
+ if (!didClose) {
+ win.close();
+ }
+ SimpleTest.finish();
+ }, 1000);
+}
+
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug529119-1.html b/docshell/test/mochitest/test_bug529119-1.html
new file mode 100644
index 0000000000..1c89780fc7
--- /dev/null
+++ b/docshell/test/mochitest/test_bug529119-1.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test bug 529119</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var workingURL = "http://mochi.test:8888/tests/docshell/test/mochitest/bug529119-window.html";
+var faultyURL = "https://www.some-nonexistent-domain-27489274c892748217cn2384.test/";
+
+var w = null;
+var phase = 0;
+var gotWrongPageOnTryAgainClick = false;
+// Token that represents which page we currently have loaded.
+var token = 0;
+
+function delay(msec) {
+ return new Promise(resolve => setTimeout(resolve, msec));
+}
+
+async function assignToken(tokenToAssign) {
+ await SpecialPowers.spawn(w, [tokenToAssign],
+ newToken => { this.content.token = newToken });
+}
+
+async function pollForPage(win) {
+ while (true) {
+ try {
+ // When we do our navigation, there may be an interstitial about:blank
+ // page if the navigation involves a process switch. That about:blank
+ // will exist between the new process's docshell being created and the
+ // actual page that's being loaded loading (which can happen async from
+ // the docshell creation). We want to avoid treating the initial
+ // about:blank as a new page.
+ //
+ // We could conceivably expose Document::IsInitialDocument() as a
+ // ChromeOnly thing and use it here, but let's just filter out all
+ // about:blank, since we don't expect any in this test.
+ var haveNewPage = await SpecialPowers.spawn(w, [token],
+ currentToken => this.content.token != currentToken &&
+ this.content.location.href != "about:blank");
+
+ if (haveNewPage) {
+ ++token;
+ assignToken(token);
+ break;
+ }
+ } catch (e) {
+ // Something went wrong; just keep waiting.
+ }
+
+ await delay(100);
+ }
+}
+
+async function windowLoaded() {
+ switch (phase) {
+ case 0:
+ assignToken(token);
+
+ /* 2. We have succeededfully loaded a page, now go to a faulty URL */
+ window.setTimeout(function() {
+ w.location.href = faultyURL;
+ }, 0);
+
+ phase = 1;
+
+ await pollForPage(w);
+ is(await SpecialPowers.spawn(w, [], () => this.content.location.href),
+ faultyURL,
+ "Is on an error page initially");
+
+ /* 3. now, while we are on the error page, try to reload it, actually
+ click the "Try Again" button */
+ SpecialPowers.spawn(w, [], () => this.content.location.reload());
+
+ await pollForPage(w);
+
+ /* 4-finish, check we are still on the error page */
+ is(await SpecialPowers.spawn(w, [], () => this.content.location.href),
+ faultyURL,
+ "Is on an error page");
+ is(gotWrongPageOnTryAgainClick, false,
+ "Must not get www.example.com page on reload of an error page");
+ w.close();
+ SimpleTest.finish();
+ break;
+
+ case 1:
+ /* 4-check, we must not get here! */
+ gotWrongPageOnTryAgainClick = true;
+ break;
+ }
+}
+
+function startTest() {
+ /* 1. load a URL that leads to an error page */
+ w = window.open(workingURL);
+}
+
+</script>
+</head>
+<body onload="startTest();">
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug529119-2.html b/docshell/test/mochitest/test_bug529119-2.html
new file mode 100644
index 0000000000..a8bd57d4f7
--- /dev/null
+++ b/docshell/test/mochitest/test_bug529119-2.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test bug 529119</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var workingURL = "http://mochi.test:8888/tests/docshell/test/mochitest/bug529119-window.html";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var faultyURL = "http://some-nonexistent-domain-27489274c892748217cn2384.test/";
+
+var w = null;
+var phase = 0;
+var isWindowLoaded = false;
+// Token that represents which page we currently have loaded.
+var token = 0;
+
+function delay(msec) {
+ return new Promise(resolve => setTimeout(resolve, msec));
+}
+
+async function assignToken(tokenToAssign) {
+ await SpecialPowers.spawn(w, [tokenToAssign],
+ newToken => { this.content.token = newToken });
+}
+
+// Returns when a new page is loaded and returns whether that page is an
+// error page.
+async function pollForPage(win) {
+ while (true) {
+ try {
+ // When we do our navigation, there may be an interstitial about:blank
+ // page if the navigation involves a process switch. That about:blank
+ // will exist between the new process's docshell being created and the
+ // actual page that's being loaded loading (which can happen async from
+ // the docshell creation). We want to avoid treating the initial
+ // about:blank as a new page.
+ //
+ // We could conceivably expose Document::IsInitialDocument() as a
+ // ChromeOnly thing and use it here, but let's just filter out all
+ // about:blank, since we don't expect any in this test.
+ var haveNewPage = await SpecialPowers.spawn(w, [token],
+ currentToken => this.content.token != currentToken &&
+ this.content.location.href != "about:blank");
+
+ if (haveNewPage) {
+ ++token;
+ assignToken(token);
+
+ // In this test, error pages are non-same-origin with us, and non-error
+ // pages are same-origin.
+ let haveErrorPage = false;
+ try {
+ win.document.title;
+ } catch (ex) {
+ haveErrorPage = true;
+ }
+ return haveErrorPage;
+ }
+ } catch (e) {
+ // Something went wrong; just keep waiting.
+ }
+
+ await delay(100);
+ }
+}
+
+async function windowLoaded() {
+ // The code under here should only be run once
+ // The test popup window workingURL was already opened
+ if (isWindowLoaded)
+ return;
+ isWindowLoaded = true;
+
+ assignToken(token);
+
+ /* 2. We have successfully loaded a page, now go to a faulty URL */
+ // XXX The test fails when we change the location synchronously
+ window.setTimeout(function() {
+ w.location.href = faultyURL;
+ }, 0);
+
+ ok(await pollForPage(w), "Waiting for error page succeeded");
+ /* 3. now, while we are on the error page, navigate back */
+ try {
+ // We need the SpecialPowers bit, because this is a cross-origin window
+ // and we normally can't touch .history on those.
+ await SpecialPowers.spawn(w, [], () => this.content.history.back());
+ } catch (ex) {
+ ok(false, "w.history.back() threw " + ex);
+ }
+
+ ok(!await pollForPage(w), "Waiting for original page succeeded");
+ /* 4-finish, check we are back at the original page */
+ is(await SpecialPowers.spawn(w, [], () => this.content.location.href),
+ workingURL,
+ "Is on the previous page");
+ w.close();
+ SimpleTest.finish();
+}
+
+function startTest() {
+ /* 1. load a URL that leads to an error page */
+ w = window.open(workingURL);
+}
+
+</script>
+</head>
+<body onload="startTest();">
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug530396.html b/docshell/test/mochitest/test_bug530396.html
new file mode 100644
index 0000000000..fa3ddc6db6
--- /dev/null
+++ b/docshell/test/mochitest/test_bug530396.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=530396
+-->
+<head>
+ <title>Test for Bug 530396</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=530396">Mozilla Bug 530396</a>
+
+<p>
+
+<iframe id="testFrame" src="http://mochi.test:8888/tests/docshell/test/mochitest/bug530396-subframe.html"></iframe>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// NOTE: If we ever make subframes do bfcache stuff, this test will need to be
+// modified accordingly! It assumes that subframes do NOT get bfcached.
+var onloadCount = 0;
+
+var step = 0;
+
+var gTestFrame = document.getElementById("testFrame");
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+addLoadEvent(doNextStep);
+
+function doNextStep() {
+ ++step;
+ switch (step) {
+ case 1:
+ is(onloadCount, 1, "Loaded initial page");
+ sendMouseEvent({type: "click"}, "target2", gTestFrame.contentWindow);
+ window.setTimeout(doNextStep, 1000);
+ break;
+
+ case 2:
+ is(onloadCount, 1, "opener must be null");
+ sendMouseEvent({type: "click"}, "target1", gTestFrame.contentWindow);
+ break;
+
+ case 3:
+ is(onloadCount, 2, "don't send referrer with rel=referrer");
+ SimpleTest.finish();
+ break;
+ }
+}
+</script>
+</pre>
+</html>
diff --git a/docshell/test/mochitest/test_bug540462.html b/docshell/test/mochitest/test_bug540462.html
new file mode 100644
index 0000000000..e0a0861aaf
--- /dev/null
+++ b/docshell/test/mochitest/test_bug540462.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=540462
+-->
+<head>
+ <title>Test for Bug 540462</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=540462">Mozilla Bug 540462</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 540462 */
+
+var win;
+function runTest() {
+ win = window.open("file_bug540462.html", "", "width=100,height=100");
+}
+
+var dwlCount = 0;
+var originalURL;
+function documentWriteLoad() {
+ if (++dwlCount == 1) {
+ originalURL = win.document.body.firstChild.href;
+ } else if (dwlCount == 2) {
+ is(win.document.body.firstChild.href, originalURL, "Wrong href!");
+ win.close();
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug551225.html b/docshell/test/mochitest/test_bug551225.html
new file mode 100644
index 0000000000..b2862fcad8
--- /dev/null
+++ b/docshell/test/mochitest/test_bug551225.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=551225
+-->
+<head>
+ <title>Test for Bug 551225</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551225">Mozilla Bug 551225</a>
+
+<script type="application/javascript">
+
+/** Test for Bug 551225 */
+
+var obj = {
+ a: new Date("1/1/2000"),
+ b: /^foo$/,
+ c: "bar",
+};
+
+history.replaceState(obj, "", "");
+is(history.state.a.toString(), new Date("1/1/2000").toString(), "Date object.");
+is(history.state.b.toString(), "/^foo$/", "Regex");
+is(history.state.c, "bar", "Other state");
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug570341.html b/docshell/test/mochitest/test_bug570341.html
new file mode 100644
index 0000000000..363f985407
--- /dev/null
+++ b/docshell/test/mochitest/test_bug570341.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=570341
+-->
+<head>
+ <title>Test for Bug 570341</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ var start = Date.now();
+ var moments = {};
+
+ var unload = 0;
+ var wasEnabled = true;
+
+ function collectMoments() {
+ var win = frames[0];
+ var timing = (win.performance && win.performance.timing) || {};
+ for (let p in timing) {
+ moments[p] = timing[p];
+ }
+ for (let p in win) {
+ if (p.substring(0, 9) == "_testing_") {
+ moments[p.substring(9)] = win[p];
+ }
+ }
+ moments.evt_unload = unload;
+ return moments;
+ }
+
+ function showSequence(node) {
+ while (node.firstChild) {
+ node.firstChild.remove();
+ }
+ var sequence = [];
+ for (var p in moments) {
+ sequence.push(p);
+ }
+ sequence.sort(function(a, b) {
+ return moments[a] - moments[b];
+ });
+ var table = document.createElement("table");
+ node.appendChild(table);
+ var row = document.createElement("tr");
+ table.appendChild(row);
+ var cell = document.createElement("td");
+ row.appendChild(cell);
+ cell.appendChild(document.createTextNode("start"));
+ cell = document.createElement("td");
+ row.appendChild(cell);
+ cell.appendChild(document.createTextNode(start));
+ for (var i = 0; i < sequence.length; ++i) {
+ var prop = sequence[i];
+ row = document.createElement("tr");
+ table.appendChild(row);
+ cell = document.createElement("td");
+ row.appendChild(cell);
+ cell.appendChild(document.createTextNode(prop));
+ cell = document.createElement("td");
+ row.appendChild(cell);
+ cell.appendChild(document.createTextNode(moments[prop]));
+ }
+ }
+
+ function checkValues() {
+ var win = frames[0];
+ ok(win.performance,
+ "window.performance is missing or not accessible for frame");
+ ok(!win.performance || win.performance.timing,
+ "window.performance.timing is missing or not accessible for frame");
+ collectMoments();
+
+ var sequences = [
+ ["navigationStart", "unloadEventStart", "unloadEventEnd"],
+ ["navigationStart", "fetchStart", "domainLookupStart", "domainLookupEnd",
+ "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd"],
+ ["responseStart", "domLoading", "domInteractive", "domComplete"],
+ ["domContentLoadedEventStart", "domContentLoadedEventEnd",
+ "loadEventStart", "loadEventEnd"],
+ ];
+
+ for (var i = 0; i < sequences.length; ++i) {
+ var seq = sequences[i];
+ for (var j = 0; j < seq.length; ++j) {
+ var prop = seq[j];
+ if (j > 0) {
+ var prevProp = seq[j - 1];
+ ok(moments[prevProp] <= moments[prop],
+ ["Expected ", prevProp, " to happen before ", prop,
+ ", got ", prevProp, " = ", moments[prevProp],
+ ", ", prop, " = ", moments[prop]].join(""));
+ }
+ }
+ }
+
+ SimpleTest.finish();
+ }
+
+window.onload = function() {
+ var win = frames[0];
+ win.addEventListener("unload", function() {
+ unload = Date.now();
+ }, true);
+ var seenLoad = 0;
+ win.addEventListener("load", function() {
+ seenLoad = Date.now();
+ }, true);
+ frames[0].location = "bug570341_recordevents.html";
+ var interval = setInterval(function() {
+ // time constants here are arbitrary, chosen to allow the test to pass
+ var stopPolling = (win.performance && win.performance.loadEventEnd) ||
+ (seenLoad && Date.now() >= seenLoad + 3000) ||
+ Date.now() >= start + 30000;
+ if (stopPolling) {
+ clearInterval(interval);
+ checkValues();
+ } else if (win._testing_evt_load) {
+ seenLoad = Date.now();
+ }
+ }, 100);
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570341">Mozilla Bug 570341</a>
+<div id="frames">
+<iframe name="child0" src="navigation/blank.html"></iframe>
+</div>
+<button type="button" onclick="showSequence(document.getElementById('display'))">
+ Show Events</button>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug580069.html b/docshell/test/mochitest/test_bug580069.html
new file mode 100644
index 0000000000..bb0a3bc823
--- /dev/null
+++ b/docshell/test/mochitest/test_bug580069.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=580069
+-->
+<head>
+ <title>Test for Bug 580069</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=580069">Mozilla Bug 580069</a>
+
+<script type="application/javascript">
+
+add_task(async function() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "file_bug580069_1.html");
+
+ // Insert the initial <iframe> document, and wait for page1Load to be called
+ // after it loads.
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ window.page1Load = resolve;
+ });
+ let iframeCw = iframe.contentWindow;
+
+ info("iframe's location is: " + iframeCw.location + "\n");
+
+ // Submit the forum and wait for the initial page load using a POST load.
+ iframeCw.document.getElementById("form").submit();
+ let method1 = await new Promise(resolve => {
+ window.page2Load = resolve;
+ });
+ info("iframe's location is: " + iframeCw.location + ", method is " + method1 + "\n");
+ is(method1, "POST", "Method for first load should be POST.");
+
+ // Push a new state, and refresh the page. This refresh shouldn't pop up the
+ // "are you sure you want to refresh a page with POST data?" dialog. If it
+ // does, this test will hang and fail, and we'll see 'Refreshing iframe...' at
+ // the end of the test log.
+ iframeCw.history.replaceState("", "", "?replaced");
+
+ info("Refreshing iframe...\n");
+ iframeCw.location.reload();
+ let method2 = await new Promise(resolve => {
+ window.page2Load = resolve;
+ });
+
+ info("iframe's location is: " + iframeCw.location + ", method is " + method2 + "\n");
+ is(method2, "GET", "Method for second load should be GET.");
+ is(iframeCw.location.search, "?replaced", "Wrong search on iframe after refresh.");
+});
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug590573.html b/docshell/test/mochitest/test_bug590573.html
new file mode 100644
index 0000000000..83554a7a66
--- /dev/null
+++ b/docshell/test/mochitest/test_bug590573.html
@@ -0,0 +1,198 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=590573
+-->
+<head>
+ <title>Test for Bug 590573</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590573">Mozilla Bug 590573</a>
+
+<script type='application/javascript'>
+SimpleTest.waitForExplicitFinish();
+
+// Listen to the first callback, since this indicates that the page loaded.
+var page1LoadCallbackEnabled = true;
+function page1Load() {
+ if (page1LoadCallbackEnabled) {
+ page1LoadCallbackEnabled = false;
+ dump("Got page1 load.\n");
+ pageLoad();
+ } else {
+ dump("Ignoring page1 load.\n");
+ }
+}
+
+var page1PageShowCallbackEnabled = false;
+function page1PageShow() {
+ if (page1PageShowCallbackEnabled) {
+ page1PageShowCallbackEnabled = false;
+ dump("Got page1 pageshow.\n");
+ pageLoad();
+ } else {
+ dump("Ignoring page1 pageshow.\n");
+ }
+}
+
+var page2LoadCallbackEnabled = false;
+function page2Load() {
+ if (page2LoadCallbackEnabled) {
+ page2LoadCallbackEnabled = false;
+ dump("Got page2 popstate.\n");
+ pageLoad();
+ } else {
+ dump("Ignoring page2 popstate.\n");
+ }
+}
+
+var page2PopstateCallbackEnabled = false;
+function page2Popstate() {
+ if (page2PopstateCallbackEnabled) {
+ page2PopstateCallbackEnabled = false;
+ dump("Got page2 popstate.\n");
+ pageLoad();
+ } else {
+ dump("Ignoring page2 popstate.\n");
+ }
+}
+
+var page2PageShowCallbackEnabled = false;
+function page2PageShow() {
+ if (page2PageShowCallbackEnabled) {
+ page2PageShowCallbackEnabled = false;
+ dump("Got page2 pageshow.\n");
+ pageLoad();
+ } else {
+ dump("Ignoring page2 pageshow.\n");
+ }
+}
+
+var popup = window.open("file_bug590573_1.html");
+
+var gTestContinuation = null;
+var loads = 0;
+function pageLoad() {
+ loads++;
+ dump("pageLoad(loads=" + loads + ", page location=" + popup.location + ")\n");
+
+ if (!gTestContinuation) {
+ gTestContinuation = testBody();
+ }
+ var ret = gTestContinuation.next();
+ if (ret.done) {
+ SimpleTest.finish();
+ }
+}
+
+function continueAsync() {
+ popup.addEventListener("popstate", function() {
+ popup.requestAnimationFrame(function() { gTestContinuation.next(); });
+ },
+ {once: true});
+}
+
+function* testBody() {
+ is(popup.scrollY, 0, "test 1");
+ popup.scroll(0, 100);
+
+ popup.history.pushState("", "", "?pushed");
+ is(Math.round(popup.scrollY), 100, "test 2");
+ popup.scroll(0, 200); // set state-2's position to 200
+
+ popup.history.back();
+ continueAsync();
+ yield;
+ is(Math.round(popup.scrollY), 100, "test 3");
+ popup.scroll(0, 150); // set original page's position to 150
+
+ popup.history.forward();
+ continueAsync();
+ yield;
+ is(Math.round(popup.scrollY), 200, "test 4");
+
+ popup.history.back();
+ continueAsync();
+ yield;
+ is(Math.round(popup.scrollY), 150, "test 5");
+
+ popup.history.forward();
+ continueAsync();
+ yield;
+ is(Math.round(popup.scrollY), 200, "test 6");
+
+ // At this point, the history looks like:
+ // PATH POSITION
+ // file_bug590573_1.html 150 <-- oldest
+ // file_bug590573_1.html?pushed 200 <-- newest, current
+
+ // Now test that the scroll position is persisted when we have real
+ // navigations involved. First, we need to spin the event loop so that the
+ // navigation doesn't replace our current history entry.
+
+ setTimeout(pageLoad, 0);
+ yield;
+
+ page2LoadCallbackEnabled = true;
+ popup.location = "file_bug590573_2.html";
+ yield;
+
+ ok(popup.location.href.match("file_bug590573_2.html$"),
+ "Location was " + popup.location +
+ " but should end with file_bug590573_2.html");
+
+ is(popup.scrollY, 0, "test 7");
+ popup.scroll(0, 300);
+
+ // We need to spin the event loop again before we go back, otherwise the
+ // scroll positions don't get updated properly.
+ setTimeout(pageLoad, 0);
+ yield;
+
+ page1PageShowCallbackEnabled = true;
+ popup.history.back();
+ yield;
+
+ // Spin the event loop again so that we get the right scroll positions.
+ setTimeout(pageLoad, 0);
+ yield;
+
+ is(popup.location.search, "?pushed");
+ ok(popup.document.getElementById("div1"), "page should have div1.");
+
+ is(Math.round(popup.scrollY), 200, "test 8");
+
+ popup.history.back();
+ continueAsync();
+ yield;
+ is(Math.round(popup.scrollY), 150, "test 9");
+ popup.history.forward();
+ continueAsync();
+ yield;
+
+ is(Math.round(popup.scrollY), 200, "test 10");
+
+ // Spin one last time...
+ setTimeout(pageLoad, 0);
+ yield;
+
+ page2PageShowCallbackEnabled = true;
+ popup.history.forward();
+ yield;
+
+ // Bug 821821, on Android tegras we get 299 instead of 300 sometimes
+ const scrollY = Math.floor(popup.scrollY);
+ if (scrollY >= 299 && scrollY <= 300) {
+ is(1, 1, "test 11");
+ } else {
+ is(1, 0, "test 11, got " + popup.scrollY + " for popup.scrollY instead of 299|300");
+ }
+ popup.close();
+}
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug598895.html b/docshell/test/mochitest/test_bug598895.html
new file mode 100644
index 0000000000..e0b17e2663
--- /dev/null
+++ b/docshell/test/mochitest/test_bug598895.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=598895
+-->
+<head>
+ <title>Test for Bug 598895</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=598895">Mozilla Bug 598895</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 598895 */
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+var win1 = window.open();
+win1.document.body.textContent = "Should show";
+
+var windowsLoaded = 0;
+
+window.onmessage = async function(ev) {
+ is(ev.data, "loaded", "Message should be 'loaded'");
+ if (++windowsLoaded == 2) {
+ var one = await snapshotWindow(win1);
+ var two = await snapshotWindow(win2);
+ var three = await snapshotWindow(win3);
+ win1.close();
+ win2.close();
+ win3.close();
+ ok(compareSnapshots(one, two, true)[0], "Popups should look identical");
+ ok(compareSnapshots(one, three, false)[0], "Popups should not look identical");
+
+ SimpleTest.finish();
+ }
+};
+
+var win2 = window.open("file_bug598895_1.html");
+var win3 = window.open("file_bug598895_2.html");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug634834.html b/docshell/test/mochitest/test_bug634834.html
new file mode 100644
index 0000000000..e1f87de000
--- /dev/null
+++ b/docshell/test/mochitest/test_bug634834.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=634834
+-->
+<head>
+ <title>Test for Bug 634834</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=634834">Mozilla Bug 634834</a>
+
+<script type='application/javascript'>
+SimpleTest.waitForExplicitFinish();
+
+function iframe_loaded() {
+ var loadedAfterPushstate = false;
+ $("iframe").onload = function() {
+ loadedAfterPushstate = true;
+ };
+
+ var obj = { name: "name" };
+ obj.__defineGetter__("a", function() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ $("iframe").contentWindow.location = "http://example.com";
+
+ // Wait until we've loaded example.com.
+ do {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ try { r.send(null); } catch (e) {}
+ } while (!loadedAfterPushstate);
+ });
+
+ try {
+ $("iframe").contentWindow.history.pushState(obj, "");
+ ok(false, "pushState should throw exception.");
+ } catch (e) {
+ ok(true, "pushState threw an exception.");
+ }
+ SimpleTest.finish();
+}
+
+</script>
+
+<iframe id='iframe' src='file_bug634834.html' onload='iframe_loaded()'></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug637644.html b/docshell/test/mochitest/test_bug637644.html
new file mode 100644
index 0000000000..1e5f4380b4
--- /dev/null
+++ b/docshell/test/mochitest/test_bug637644.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=637644
+-->
+<head>
+ <title>Test for Bug 637644</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=637644">Mozilla Bug 637644</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 637644 */
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+var win1 = window.open("", "", "height=500,width=500");
+win1.document.body.textContent = "Should show";
+
+var windowsLoaded = 0;
+
+window.onmessage = async function(ev) {
+ is(ev.data, "loaded", "Message should be 'loaded'");
+ if (++windowsLoaded == 2) {
+ var one = await snapshotWindow(win1);
+ var two = await snapshotWindow(win2);
+ var three = await snapshotWindow(win3);
+ win1.close();
+ win2.close();
+ win3.close();
+ ok(compareSnapshots(one, two, true)[0], "Popups should look identical");
+ ok(compareSnapshots(one, three, false)[0], "Popups should not look identical");
+
+ SimpleTest.finish();
+ }
+};
+
+var win2 = window.open("file_bug637644_1.html", "", "height=500,width=500");
+var win3 = window.open("file_bug637644_2.html", "", "height=500,width=500");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug640387_1.html b/docshell/test/mochitest/test_bug640387_1.html
new file mode 100644
index 0000000000..b8aab054a1
--- /dev/null
+++ b/docshell/test/mochitest/test_bug640387_1.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=640387
+-->
+<head>
+ <title>Test for Bug 640387</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=640387">Mozilla Bug 640387</a>
+
+<script type='application/javascript'>
+SimpleTest.waitForExplicitFinish();
+
+function* test() {
+ /* Spin the event loop so we get out of the onload handler. */
+ SimpleTest.executeSoon(function() { gGen.next(); });
+ yield undefined;
+
+ popup.history.pushState("", "", "#hash1");
+ popup.history.pushState("", "", "#hash2");
+
+ // Now the history looks like:
+ // file_bug640387.html
+ // file_bug640387.html#hash1
+ // file_bug640387.html#hash2 <-- current
+
+ // Going back should trigger a hashchange, which will wake us up from the
+ // yield.
+ popup.history.back();
+ yield undefined;
+ ok(true, "Got first hashchange.");
+
+ // Going back should wake us up again.
+ popup.history.back();
+ yield undefined;
+ ok(true, "Got second hashchange.");
+
+ // Now the history looks like:
+ // file_bug640387.html <-- current
+ // file_bug640387.html#hash1
+ // file_bug640387.html#hash2
+
+ // Going forward should trigger a hashchange.
+ popup.history.forward();
+ yield undefined;
+ ok(true, "Got third hashchange.");
+
+ // Now modify the history so it looks like:
+ // file_bug640387.html
+ // file_bug640387.html#hash1
+ // file_bug640387.html#hash1 <-- current
+ popup.history.pushState("", "", "#hash1");
+
+ // Now when we go back, we should not get a hashchange. Instead, wait for a
+ // popstate. We need to asynchronously go back because popstate is fired
+ // sync.
+ gHashchangeExpected = false;
+ gCallbackOnPopstate = true;
+ SimpleTest.executeSoon(function() { popup.history.back(); });
+ yield undefined;
+ ok(true, "Got popstate.");
+ gCallbackOnPopstate = false;
+
+ // Spin the event loop so hashchange has a chance to fire, if it's going to.
+ SimpleTest.executeSoon(function() { gGen.next(); });
+ yield undefined;
+
+ popup.close();
+ SimpleTest.finish();
+}
+
+var gGen = null;
+function childLoad() {
+ gGen = test();
+ gGen.next();
+}
+
+var gHashchangeExpected = true;
+function childHashchange() {
+ if (gHashchangeExpected) {
+ gGen.next();
+ } else {
+ ok(false, "Got hashchange when we weren't expecting one.");
+ }
+}
+
+var gCallbackOnPopstate = false;
+function childPopstate() {
+ if (gCallbackOnPopstate) {
+ gGen.next();
+ }
+}
+
+/* We need to run this test in a popup, because navigating an iframe
+ * back/forwards tends to cause intermittent orange. */
+var popup = window.open("file_bug640387.html");
+
+/* Control now flows up to childLoad(), called once the popup loads. */
+
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug640387_2.html b/docshell/test/mochitest/test_bug640387_2.html
new file mode 100644
index 0000000000..c248a64836
--- /dev/null
+++ b/docshell/test/mochitest/test_bug640387_2.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=640387
+-->
+<head>
+ <title>Test for Bug 640387</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=640387">Mozilla Bug 640387</a>
+
+<!-- Test that, when going from
+
+ http://example.com/#foo
+
+to
+
+ http://example.com/
+
+via a non-history load, we do a true load, rather than a scroll. -->
+
+<script type='application/javascript'>
+SimpleTest.waitForExplicitFinish();
+
+var callbackOnLoad = false;
+function childLoad() {
+ if (callbackOnLoad) {
+ callbackOnLoad = false;
+ gGen.next();
+ }
+}
+
+var errorOnHashchange = false;
+var callbackOnHashchange = false;
+function childHashchange() {
+ if (errorOnHashchange) {
+ ok(false, "Got unexpected hashchange.");
+ }
+ if (callbackOnHashchange) {
+ callbackOnHashchange = false;
+ gGen.next();
+ }
+}
+
+function* run_test() {
+ var iframe = $("iframe").contentWindow;
+
+ ok(true, "Got first load");
+
+ // Spin the event loop so we exit the onload handler.
+ SimpleTest.executeSoon(function() { gGen.next(); });
+ yield undefined;
+
+ let origLocation = iframe.location + "";
+ callbackOnHashchange = true;
+ iframe.location.hash = "#1";
+ // Wait for a hashchange event.
+ yield undefined;
+
+ ok(true, "Got hashchange.");
+
+ iframe.location = origLocation;
+ // This should produce a load event and *not* a hashchange, because the
+ // result of the load is a different document than we had previously.
+ callbackOnLoad = true;
+ errorOnHashchange = true;
+ yield undefined;
+
+ ok(true, "Got final load.");
+
+ // Spin the event loop to give hashchange a chance to fire, if it's going to.
+ SimpleTest.executeSoon(function() { gGen.next(); });
+ yield undefined;
+
+ SimpleTest.finish();
+}
+
+callbackOnLoad = true;
+var gGen = run_test();
+
+</script>
+
+<iframe id='iframe' src='file_bug640387.html'></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug653741.html b/docshell/test/mochitest/test_bug653741.html
new file mode 100644
index 0000000000..33ba7077e4
--- /dev/null
+++ b/docshell/test/mochitest/test_bug653741.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=653741
+-->
+<head>
+ <title>Test for Bug 653741</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=653741">Mozilla Bug 653741</a>
+
+<script type="application/javascript">
+
+/** Test for Bug 653741 */
+SimpleTest.waitForExplicitFinish();
+
+function childLoad() {
+ // Spin the event loop so we leave the onload handler.
+ SimpleTest.executeSoon(childLoad2);
+}
+
+function childLoad2() {
+ let cw = $("iframe").contentWindow;
+
+ // Save the Y offset. For sanity's sake, make sure it's not 0, because we
+ // should be at the bottom of the page!
+ let origYOffset = Math.round(cw.pageYOffset);
+ ok(origYOffset != 0, "Original Y offset is not 0.");
+
+ // Scroll the iframe to the top, then navigate to #bottom again.
+ cw.scrollTo(0, 0);
+
+ // Our current location is #bottom, so this should scroll us down to the
+ // bottom again.
+ cw.location = cw.location + "";
+
+ is(Math.round(cw.pageYOffset), origYOffset, "Correct offset after reloading page.");
+ SimpleTest.finish();
+}
+
+</script>
+
+<iframe height='100px' id='iframe' src='file_bug653741.html#bottom'></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug660404.html b/docshell/test/mochitest/test_bug660404.html
new file mode 100644
index 0000000000..94e3f67aa1
--- /dev/null
+++ b/docshell/test/mochitest/test_bug660404.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660404
+-->
+<head>
+ <title>Test for Bug 660404</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660404">Mozilla Bug 660404</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 660404 */
+SimpleTest.waitForExplicitFinish();
+
+var textContent =
+`
+ var bc = new BroadcastChannel("bug660404_multipart");
+ bc.postMessage({command: "finishTest",
+ textContent: window.document.documentElement.textContent,
+ innerHTML: window.document.documentElement.innerHTML
+ });
+ bc.close();
+ window.close();
+`;
+var innerHTML =
+`<head><script>
+ var bc = new BroadcastChannel("bug660404_multipart");
+ bc.postMessage({command: "finishTest",
+ textContent: window.document.documentElement.textContent,
+ innerHTML: window.document.documentElement.innerHTML
+ });
+ bc.close();
+ window.close();
+</`
+// eslint-disable-next-line no-useless-concat
++ `script></head>`
+;
+var bc_multipart = new BroadcastChannel("bug660404_multipart");
+bc_multipart.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "finishTest") {
+ is(msg.textContent, textContent);
+ is(msg.innerHTML, innerHTML);
+ bc_multipart.close();
+ SimpleTest.finish();
+ }
+}
+var bc = new BroadcastChannel("bug660404");
+bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "pagehide") {
+ is(msg.persisted, true, "Should be bfcached when navigating to multipart");
+ bc.close();
+ }
+}
+
+// If Fission is disabled, the pref is no-op.
+SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ // Have to open a new window, since there's no bfcache in subframes
+ window.open("file_bug660404-1.html", "", "noopener");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug662170.html b/docshell/test/mochitest/test_bug662170.html
new file mode 100644
index 0000000000..d25ee3bd0f
--- /dev/null
+++ b/docshell/test/mochitest/test_bug662170.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=662170
+-->
+<head>
+ <title>Test for Bug 662170</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=662170">Mozilla Bug 662170</a>
+
+<script type="application/javascript">
+
+/** Test for Bug 662170 */
+SimpleTest.waitForExplicitFinish();
+
+function childLoad() {
+ // Spin the event loop so we leave the onload handler.
+ SimpleTest.executeSoon(childLoad2);
+}
+
+function childLoad2() {
+ let cw = $("iframe").contentWindow;
+
+ // When we initially load the page, we should be at the top.
+ is(cw.pageYOffset, 0, "Initial Y offset should be 0.");
+
+ // Scroll the iframe to the bottom.
+ cw.scrollTo(0, 300);
+
+ // Did we actually scroll somewhere?
+ isnot(Math.round(cw.pageYOffset), 0, "Y offset should be non-zero after scrolling.");
+
+ // Now load file_bug662170.html#, which should take us to the top of the
+ // page.
+ cw.location = cw.location + "#";
+
+ is(cw.pageYOffset, 0, "Correct Y offset after loading #.");
+ SimpleTest.finish();
+}
+
+</script>
+
+<!-- When the iframe loads, it calls childLoad(). -->
+<iframe height='100px' id='iframe' src='file_bug662170.html'></iframe>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug668513.html b/docshell/test/mochitest/test_bug668513.html
new file mode 100644
index 0000000000..09c848b6c1
--- /dev/null
+++ b/docshell/test/mochitest/test_bug668513.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=668513
+-->
+<head>
+ <title>Test for Bug 668513</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668513">Mozilla Bug 668513</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+if (navigator.platform.startsWith("Linux")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+SimpleTest.waitForExplicitFinish();
+window.open("file_bug668513.html");
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug669671.html b/docshell/test/mochitest/test_bug669671.html
new file mode 100644
index 0000000000..4470cd6682
--- /dev/null
+++ b/docshell/test/mochitest/test_bug669671.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=669671
+-->
+<head>
+ <title>Test for Bug 669671</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=669671">Mozilla Bug 669671</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 669671.
+ *
+ * This is a bit complicated. We have a script, file_bug669671.sjs, which counts
+ * how many times it's loaded and returns that count in the body of an HTML
+ * document. For brevity, call this page X.
+ *
+ * X is sent with Cache-Control: max-age=0 and can't be bfcached (it has an
+ * onunload handler). Our test does the following in a popup:
+ *
+ * 1) Load X?pushed, to prime the cache.
+ * 2) Navigate to X.
+ * 3) Call pushState and navigate from X to X?pushed.
+ * 4) Navigate to X?navigated.
+ * 5) Go back (to X?pushed).
+ *
+ * We do all this work so we can check that in step 5, we fetch X?pushed from
+ * the network -- we shouldn't use our cached copy, because of the
+ * cache-control header X sends.
+ *
+ * Then we go back and repeat the whole process but call history.replaceState
+ * instead of pushState. And for good measure, we test once more, this time
+ * modifying only the hash of the URI using replaceState. In this case, we
+ * should* load from the cache.
+ *
+ */
+SimpleTest.requestLongerTimeout(2);
+SimpleTest.waitForExplicitFinish();
+
+function onChildLoad() {
+ SimpleTest.executeSoon(function() { gGen.next(); });
+}
+
+var _loadCount = 0;
+function checkPopupLoadCount() {
+ is(popup.document.body.innerHTML, _loadCount + "", "Load count");
+
+ // We normally want to increment _loadCount here. But if the test fails
+ // because we didn't do a load we should have, let's not cause a cascade of
+ // failures by incrementing _loadCount.
+ var origCount = _loadCount;
+ if (popup.document.body.innerHTML >= _loadCount + "")
+ _loadCount++;
+ return origCount;
+}
+
+function* test() {
+ // Step 0 - Make sure the count is reset to 0 in case of reload
+ popup.location = "file_bug669671.sjs?countreset";
+ yield;
+ is(popup.document.body.innerHTML, "0",
+ "Load count should be reset to 0");
+
+ // Step 1 - The popup's body counts how many times we've requested the
+ // resource. This is the first time we've requested it, so it should be '0'.
+ checkPopupLoadCount();
+
+ // Step 2 - We'll get another onChildLoad when this finishes.
+ popup.location = "file_bug669671.sjs";
+ yield undefined;
+
+ // Step 3 - Call pushState and change the URI back to ?pushed.
+ checkPopupLoadCount();
+ popup.history.pushState("", "", "?pushed");
+
+ // Step 4 - Navigate away. This should trigger another onChildLoad.
+ popup.location = "file_bug669671.sjs?navigated-1";
+ yield undefined;
+
+ // Step 5 - Go back. This should result in another onload (because the file is
+ // not in bfcache) and should be the fourth time we've requested the sjs file.
+ checkPopupLoadCount();
+ popup.history.back();
+ yield undefined;
+
+ // This is the check which was failing before we fixed the bug.
+ checkPopupLoadCount();
+
+ popup.close();
+
+ // Do the whole thing again, but with replaceState.
+ popup = window.open("file_bug669671.sjs?replaced");
+ yield undefined;
+ checkPopupLoadCount();
+ popup.location = "file_bug669671.sjs";
+ yield undefined;
+ checkPopupLoadCount();
+ popup.history.replaceState("", "", "?replaced");
+ popup.location = "file_bug669671.sjs?navigated-2";
+ yield undefined;
+ checkPopupLoadCount();
+ popup.history.back();
+ yield undefined;
+ checkPopupLoadCount();
+ popup.close();
+
+ // Once more, with feeling. Notice that we don't have to prime the cache
+ // with an extra load here, because X and X#hash share the same cache entry.
+ popup = window.open("file_bug669671.sjs?hash-test");
+ yield undefined;
+ var initialCount = checkPopupLoadCount();
+ popup.history.replaceState("", "", "#hash");
+ popup.location = "file_bug669671.sjs?navigated-3";
+ yield undefined;
+ checkPopupLoadCount();
+ popup.history.back();
+ yield undefined;
+ is(popup.document.body.innerHTML, initialCount + "",
+ "Load count (should be cached)");
+ popup.close();
+
+ SimpleTest.finish();
+}
+
+var gGen = test();
+var popup;
+
+// Disable RCWN to make cache behavior deterministic.
+SpecialPowers.pushPrefEnv({set: [["network.http.rcwn.enabled", false]]}, () => {
+ // This will call into onChildLoad once it loads.
+ popup = window.open("file_bug669671.sjs?pushed");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug675587.html b/docshell/test/mochitest/test_bug675587.html
new file mode 100644
index 0000000000..452bbc8058
--- /dev/null
+++ b/docshell/test/mochitest/test_bug675587.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=675587
+-->
+<head>
+ <title>Test for Bug 675587</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=675587">Mozilla Bug 675587</a>
+<p id="display">
+ <iframe src="file_bug675587.html#hash"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 675587 */
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ ok(window.frames[0].location.href.endsWith("file_bug675587.html#"),
+ "Should have the right href");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug680257.html b/docshell/test/mochitest/test_bug680257.html
new file mode 100644
index 0000000000..4d5736ac0a
--- /dev/null
+++ b/docshell/test/mochitest/test_bug680257.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=680257
+-->
+<head>
+ <title>Test for Bug 680257</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=680257">Mozilla Bug 680257</a>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var popup = window.open("file_bug680257.html");
+
+var gTestContinuation = null;
+function continueAsync() {
+ popup.addEventListener("hashchange",
+ function(e) { gTestContinuation.next(); }, { once: true });
+}
+
+// The popup will call into popupLoaded() once it loads.
+function popupLoaded() {
+ // runTests() needs to be called from outside popupLoaded's onload handler.
+ // Otherwise, the navigations we do in runTests won't create new SHEntries.
+ SimpleTest.executeSoon(function() {
+ if (!gTestContinuation) {
+ gTestContinuation = runTests();
+ }
+ gTestContinuation.next();
+ });
+}
+
+function* runTests() {
+ checkPopupLinkStyle(false, "Initial");
+
+ popup.location.hash = "a";
+ continueAsync();
+ yield;
+ checkPopupLinkStyle(true, "After setting hash");
+
+ popup.history.back();
+ continueAsync();
+ yield;
+
+ checkPopupLinkStyle(false, "After going back");
+
+ popup.history.forward();
+ continueAsync();
+ yield;
+ checkPopupLinkStyle(true, "After going forward");
+
+ popup.close();
+ SimpleTest.finish();
+}
+
+function checkPopupLinkStyle(isTarget, desc) {
+ var link = popup.document.getElementById("a");
+ var style = popup.getComputedStyle(link);
+ var color = style.getPropertyValue("color");
+
+ // Color is red if isTarget, black otherwise.
+ if (isTarget) {
+ is(color, "rgb(255, 0, 0)", desc);
+ } else {
+ is(color, "rgb(0, 0, 0)", desc);
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug691547.html b/docshell/test/mochitest/test_bug691547.html
new file mode 100644
index 0000000000..706cd5013b
--- /dev/null
+++ b/docshell/test/mochitest/test_bug691547.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=691547
+-->
+<head>
+ <title>Test for Bug 691547</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ var navStart = 0;
+ var beforeReload = 0;
+ function onContentLoad() {
+ var frame = frames[0];
+ if (!navStart) {
+ // First time we perform navigation in subframe. The bug is that
+ // load in subframe causes timing.navigationStart to be recorded
+ // as if it was a start of the next navigation.
+ var innerFrame = frame.frames[0];
+ navStart = frame.performance.timing.navigationStart;
+ innerFrame.location = "bug570341_recordevents.html";
+ // Let's wait a bit so the difference is clear anough.
+ setTimeout(reload, 3000);
+ } else {
+ // Content reloaded, time to check. We are allowing a huge time slack,
+ // in case clock is imprecise. If we have a bug, the difference is
+ // expected to be about the timeout value set above.
+ var diff = frame.performance.timing.navigationStart - beforeReload;
+ ok(diff >= -200,
+ "navigationStart should be set after reload request. " +
+ "Measured difference: " + diff + " (should be positive)");
+ SimpleTest.finish();
+ }
+ }
+ function reload() {
+ var frame = frames[0];
+ ok(navStart == frame.performance.timing.navigationStart,
+ "navigationStart should not change when frame loads.");
+ beforeReload = Date.now();
+ frame.location.reload();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570341">Mozilla Bug 570341</a>
+<div id="frames">
+<iframe name="frame0" id="frame0" src="bug691547_frame.html" onload="onContentLoad()"></iframe>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug694612.html b/docshell/test/mochitest/test_bug694612.html
new file mode 100644
index 0000000000..8ea4331c43
--- /dev/null
+++ b/docshell/test/mochitest/test_bug694612.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=694612
+-->
+<head>
+ <title>Test for Bug 694612</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=694612">Mozilla Bug 694612</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 694612 */
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ ok(event.data.result, "should have performance API in an <object>");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+</script>
+<object type="text/html"
+ data="data:text/html,<script>parent.postMessage({result:performance!=null},'*');</script>">
+</object>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug703855.html b/docshell/test/mochitest/test_bug703855.html
new file mode 100644
index 0000000000..4aa7b08800
--- /dev/null
+++ b/docshell/test/mochitest/test_bug703855.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=703855
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 703855</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=703855">Mozilla Bug 703855</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <iframe id="f" src="file_bug703855.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 703855 */
+
+SimpleTest.waitForExplicitFinish();
+
+var timingAttributes = [
+ "connectEnd",
+ "connectStart",
+ "domComplete",
+ "domContentLoadedEventEnd",
+ "domContentLoadedEventStart",
+ "domInteractive",
+ "domLoading",
+ "domainLookupEnd",
+ "domainLookupStart",
+ "fetchStart",
+ "loadEventEnd",
+ "loadEventStart",
+ "navigationStart",
+ "redirectEnd",
+ "redirectStart",
+ "requestStart",
+ "responseEnd",
+ "responseStart",
+ "unloadEventEnd",
+ "unloadEventStart",
+];
+var originalTiming = {};
+
+function runTest() {
+ var timing = $("f").contentWindow.performance.timing;
+ for (let i in timingAttributes) {
+ originalTiming[timingAttributes[i]] = timing[timingAttributes[i]];
+ }
+
+ var doc = $("f").contentDocument;
+ doc.open();
+ doc.write("<!DOCTYPE html>");
+ doc.close();
+
+ SimpleTest.executeSoon(function() {
+ var newTiming = $("f").contentWindow.performance.timing;
+ for (let i in timingAttributes) {
+ is(newTiming[timingAttributes[i]], originalTiming[timingAttributes[i]],
+ "document.open should not affect value of " + timingAttributes[i]);
+ }
+ SimpleTest.finish();
+ });
+}
+
+addLoadEvent(function() {
+ SimpleTest.executeSoon(runTest);
+});
+
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug728939.html b/docshell/test/mochitest/test_bug728939.html
new file mode 100644
index 0000000000..168184099a
--- /dev/null
+++ b/docshell/test/mochitest/test_bug728939.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=728939
+-->
+<head>
+ <title>Test for Bug 728939</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=728939">Mozilla Bug 728939</a>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Called when the popup finishes loading.
+function popupLoaded() {
+ popup.location.hash = "#foo";
+ is(popup.document.URL, popup.location.href, "After hashchange.");
+
+ popup.history.pushState("", "", "bar");
+ is(popup.document.URL, popup.location.href, "After pushState.");
+
+ popup.history.replaceState("", "", "baz");
+ is(popup.document.URL, popup.location.href, "After replaceState.");
+
+ popup.close();
+ SimpleTest.finish();
+}
+
+var popup = window.open("file_bug728939.html");
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_bug797909.html b/docshell/test/mochitest/test_bug797909.html
new file mode 100644
index 0000000000..15b7c27c0f
--- /dev/null
+++ b/docshell/test/mochitest/test_bug797909.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=797909
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 797909</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=797909">Mozilla Bug 797909</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+ /** Test for Bug 797909 */
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ var iframe = document.getElementById("ifr");
+ try {
+ iframe.contentWindow.document;
+ ok(false, "Should have thrown an exception");
+ } catch (ex) {
+ ok(true, "Got an exception");
+ }
+
+ iframe = document.createElement("iframe");
+ // set sandbox attribute
+ iframe.sandbox = "allow-scripts";
+ // and then insert into the doc
+ document.body.appendChild(iframe);
+
+ try {
+ iframe.contentWindow.document;
+ ok(false, "Should have thrown an exception");
+ } catch (ex) {
+ ok(true, "Got an exception");
+ }
+
+ iframe = document.createElement("iframe");
+ // set sandbox attribute
+ iframe.sandbox = "allow-same-origin";
+ // and then insert into the doc
+ document.body.appendChild(iframe);
+
+ try {
+ iframe.contentWindow.document;
+ ok(true, "Shouldn't have thrown an exception");
+ } catch (ex) {
+ ok(false, "Got an unexpected exception");
+ }
+
+ SimpleTest.finish();
+ }
+
+</script>
+</pre>
+<iframe id="ifr" sandbox = "allow-scripts"></iframe>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_close_onpagehide_by_history_back.html b/docshell/test/mochitest/test_close_onpagehide_by_history_back.html
new file mode 100644
index 0000000000..33140502f7
--- /dev/null
+++ b/docshell/test/mochitest/test_close_onpagehide_by_history_back.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>Test for closing window in pagehide event callback caused by history.back()</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432396">Mozilla Bug 1432396</a>
+<p id="display"></p>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+const w = window.open("file_close_onpagehide1.html");
+window.addEventListener("message", e => {
+ is(e.data, "initial", "The initial page loaded");
+ window.addEventListener("message", evt => {
+ is(evt.data, "second", "The second page loaded");
+ w.onpagehide = () => {
+ w.close();
+ info("try to close the popped up window in onpagehide");
+ SimpleTest.finish();
+ };
+ w.history.back();
+ }, { once: true });
+ w.location = "file_close_onpagehide2.html";
+}, { once: true });
+</script>
diff --git a/docshell/test/mochitest/test_close_onpagehide_by_window_close.html b/docshell/test/mochitest/test_close_onpagehide_by_window_close.html
new file mode 100644
index 0000000000..8b094cdaa4
--- /dev/null
+++ b/docshell/test/mochitest/test_close_onpagehide_by_window_close.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Test for closing window in pagehide event callback caused by window.close()</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432396">Mozilla Bug 1432396</a>
+<p id="display"></p>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+const w = window.open("file_close_onpagehide1.html");
+window.addEventListener("message", e => {
+ is(e.data, "initial", "The initial page loaded");
+ w.onpagehide = () => {
+ w.close();
+ info("try to close the popped up window in onpagehide");
+ SimpleTest.finish();
+ };
+ w.close();
+}, { once: true });
+</script>
diff --git a/docshell/test/mochitest/test_compressed_multipart.html b/docshell/test/mochitest/test_compressed_multipart.html
new file mode 100644
index 0000000000..438819b643
--- /dev/null
+++ b/docshell/test/mochitest/test_compressed_multipart.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1600211
+
+Loads a document that is served as multipart/x-mixed-replace as well as gzip compressed.
+Checks that we correctly decompress and display it (via running JS within the document to notify us).
+-->
+<head>
+ <title>Test for Bug 1600211</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1600211">Mozilla Bug 1600211</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1600211 */
+SimpleTest.waitForExplicitFinish();
+
+var w;
+
+function finishTest() {
+ is(w.document.documentElement.textContent, "opener.finishTest();");
+ is(w.document.documentElement.innerHTML, "<head><script>opener.finishTest();</" +
+ "script></head>");
+ w.close();
+ SimpleTest.finish();
+}
+
+w = window.open("file_compressed_multipart");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_content_javascript_loads.html b/docshell/test/mochitest/test_content_javascript_loads.html
new file mode 100644
index 0000000000..eabc1d314e
--- /dev/null
+++ b/docshell/test/mochitest/test_content_javascript_loads.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for Bug 1647519</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1647519">Mozilla Bug 1647519</a>
+
+<script type="application/javascript">
+"use strict";
+
+function promiseMessage(source, filter = event => true) {
+ return new Promise(resolve => {
+ function listener(event) {
+ if (event.source == source && filter(event)) {
+ window.removeEventListener("message", listener);
+ resolve(event);
+ }
+ }
+ window.addEventListener("message", listener);
+ });
+}
+
+async function runTests(resourcePath) {
+ /* globals Assert, content */
+ let doc = content.document;
+
+ // Sends a message to the given target window and waits for a response a few
+ // times to (more or less) ensure that a `javascript:` load request has had
+ // time to succeed, if it were going to.
+ async function doSomeRoundTrips(target) {
+ for (let i = 0; i < 3; i++) {
+ // Note: The ping message needs to be sent from a script running in the
+ // content scope or there will be no source window for the reply to be
+ // sent to.
+ await content.wrappedJSObject.ping(target);
+ }
+ }
+
+ function promiseEvent(target, name) {
+ return new Promise(resolve => {
+ target.addEventListener(name, resolve, { once: true });
+ });
+ }
+
+ function createIframe(host, id) {
+ let iframe = doc.createElement("iframe");
+ iframe.id = id;
+ iframe.name = id;
+ iframe.src = `https://${host}${resourcePath}file_content_javascript_loads_frame.html`;
+ doc.body.appendChild(iframe);
+ return promiseEvent(iframe, "load");
+ }
+
+ const ID_SAME_ORIGIN = "frame-same-origin";
+ const ID_SAME_BASE_DOMAIN = "frame-same-base-domain";
+ const ID_CROSS_BASE_DOMAIN = "frame-cross-base-domain";
+
+ await Promise.all([
+ createIframe("example.com", ID_SAME_ORIGIN),
+ createIframe("test1.example.com", ID_SAME_BASE_DOMAIN),
+ createIframe("example.org", ID_CROSS_BASE_DOMAIN),
+ ]);
+
+ let gotJSLoadFrom = null;
+ let pendingJSLoadID = null;
+ content.addEventListener("message", event => {
+ if ("javascriptLoadID" in event.data) {
+ Assert.equal(
+ event.data.javascriptLoadID,
+ pendingJSLoadID,
+ "Message from javascript: load should have the expected ID"
+ );
+ Assert.equal(
+ gotJSLoadFrom,
+ null,
+ "Should not have seen a previous load message this cycle"
+ );
+ gotJSLoadFrom = event.source.name;
+ }
+ });
+
+ async function watchForJSLoads(frameName, expected, task) {
+ let loadId = Math.random();
+
+ let jsURI =
+ "javascript:" +
+ encodeURI(`parent.postMessage({ javascriptLoadID: ${loadId} }, "*")`);
+
+ pendingJSLoadID = loadId;
+ gotJSLoadFrom = null;
+
+ await task(jsURI);
+
+ await doSomeRoundTrips(content.wrappedJSObject[frameName]);
+
+ if (expected) {
+ Assert.equal(
+ gotJSLoadFrom,
+ frameName,
+ `Should have seen javascript: URI loaded into ${frameName}`
+ );
+ } else {
+ Assert.equal(
+ gotJSLoadFrom,
+ null,
+ "Should not have seen javascript: URI loaded"
+ );
+ }
+ }
+
+ let frames = [
+ { name: ID_SAME_ORIGIN, expectLoad: true },
+ { name: ID_SAME_BASE_DOMAIN, expectLoad: false },
+ { name: ID_CROSS_BASE_DOMAIN, expectLoad: false },
+ ];
+ for (let { name, expectLoad } of frames) {
+ info(`Checking loads for frame "${name}". Expecting loads: ${expectLoad}`);
+
+ info("Checking location setter");
+ await watchForJSLoads(name, expectLoad, jsURI => {
+ // Note: We need to do this from the content scope since security checks
+ // depend on the JS caller scope.
+ content.wrappedJSObject.setFrameLocation(name, jsURI);
+ });
+
+ info("Checking targeted <a> load");
+ await watchForJSLoads(name, expectLoad, jsURI => {
+ let a = doc.createElement("a");
+ a.target = name;
+ a.href = jsURI;
+ doc.body.appendChild(a);
+ a.click();
+ a.remove();
+ });
+
+ info("Checking targeted window.open load");
+ await watchForJSLoads(name, expectLoad, jsURI => {
+ content.wrappedJSObject.open(jsURI, name);
+ });
+ }
+}
+
+add_task(async function() {
+ const resourcePath = location.pathname.replace(/[^\/]+$/, "");
+
+ let win = window.open(
+ `https://example.com${resourcePath}file_content_javascript_loads_root.html`
+ );
+ await promiseMessage(win, event => event.data == "ready");
+
+ await SpecialPowers.spawn(win, [resourcePath], runTests);
+
+ win.close();
+});
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_double_submit.html b/docshell/test/mochitest/test_double_submit.html
new file mode 100644
index 0000000000..640930718d
--- /dev/null
+++ b/docshell/test/mochitest/test_double_submit.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Test for Bug 1590762</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <iframe name="targetFrame" id="targetFrame"></iframe>
+ <form id="form" action="double_submit.sjs?delay=1000" method="POST" target="targetFrame">
+ <input id="token" type="text" name="token" value="">
+ <input id="button" type="submit">
+ </form>
+ <script>
+ "use strict";
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const CROSS_ORIGIN_URI = "http://test1.example.com/tests/docshell/test/mochitest/ping.html";
+
+ function asyncClick(counts) {
+ let frame = document.createElement('iframe');
+ frame.addEventListener(
+ 'load', () => frame.contentWindow.postMessage({command: "start"}, "*"),
+ { once:true });
+ frame.src = "clicker.html";
+
+ addEventListener('message', ({source}) => {
+ if (source === frame.contentWindow) {
+ counts.click++;
+ synthesizeMouse(document.getElementById('button'), 5, 5, {});
+ }
+ }, { once: true });
+
+ document.body.appendChild(frame);
+ return stop;
+ }
+
+ function click(button) {
+ synthesizeMouse(button, 5, 5, {});
+ }
+
+ add_task(async function runTest() {
+ let frame = document.getElementById('targetFrame');
+ await new Promise(resolve => {
+ addEventListener('message', resolve, {once: true});
+ frame.src = CROSS_ORIGIN_URI;
+ });
+
+ let form = document.getElementById('form');
+ let button = document.getElementById('button');
+
+ let token = document.getElementById('token');
+ token.value = "first";
+
+ await new Promise((resolve, reject) => {
+ let counts = { click: 0, submit: 0 };
+ form.addEventListener('submit', () => counts.submit++);
+ asyncClick(counts);
+ form.requestSubmit(button);
+ token.value = "bad";
+ let steps = {
+ good: {
+ entered: false,
+ next: () => { steps.good.entered = true; resolve(); },
+ assertion: () => {
+ ok(steps.first.entered && !steps.bad.entered, "good comes after first, but not bad")
+ }
+ },
+ first: {
+ entered: false,
+ next: () => { steps.first.entered = true; token.value = "good"; click(button); },
+ assertion: () => {
+ ok(!steps.good.entered && !steps.bad.entered, "first message is first")
+ is(counts.click, 1, "clicked");
+ is(counts.submit, 2, "did submit");
+ }
+ },
+ bad: {
+ entered: false,
+ next: () => { reject(); },
+ assertion: () => ok(false, "we got a bad message")
+ }
+ };
+ addEventListener('message', ({source, data}) => {
+ if (source !== frame.contentWindow) {
+ return;
+ }
+
+ let step = steps[data] || reject;
+ step.assertion();
+ step.next();
+ })
+ });
+ });
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html b/docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html
new file mode 100644
index 0000000000..70d610a677
--- /dev/null
+++ b/docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+
+var channel = SpecialPowers.wrap(window).docShell.currentDocumentChannel;
+var loadInfo = channel.loadInfo;
+
+// 1) perform some sanity checks
+var triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec;
+var loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec;
+var principalToInherit = channel.loadInfo.principalToInherit.asciiSpec;
+
+ok(triggeringPrincipal.startsWith("http://mochi.test:8888/")
+ || triggeringPrincipal.startsWith("http://mochi.xorigin-test:8888/"),
+ "initial triggeringPrincipal correct");
+ok(loadingPrincipal.startsWith("http://mochi.test:8888/")
+ || loadingPrincipal.startsWith("http://mochi.xorigin-test:8888/"),
+ "initial loadingPrincipal correct");
+ok(principalToInherit.startsWith("http://mochi.test:8888/")
+ || principalToInherit.startsWith("http://mochi.xorigin-test:8888/"),
+ "initial principalToInherit correct");
+
+// reset principals on the loadinfo
+loadInfo.resetPrincipalToInheritToNullPrincipal();
+
+// 2) verify loadInfo contains the correct principals
+triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec;
+loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec;
+principalToInherit = channel.loadInfo.principalToInherit;
+
+ok(triggeringPrincipal.startsWith("http://mochi.test:8888/")
+ || triggeringPrincipal.startsWith("http://mochi.xorigin-test:8888/"),
+ "triggeringPrincipal after resetting correct");
+ok(loadingPrincipal.startsWith("http://mochi.test:8888/")
+ || loadingPrincipal.startsWith("http://mochi.xorigin-test:8888/"),
+ "loadingPrincipal after resetting correct");
+ok(principalToInherit.isNullPrincipal
+ || principalToInherit.startsWith("http://mochi.xorigin-test:8888/"),
+ "principalToInherit after resetting correct");
+
+// 3) verify that getChannelResultPrincipal returns right principal
+var resultPrincipal = SpecialPowers.Services.scriptSecurityManager
+ .getChannelResultPrincipal(channel);
+
+ok(resultPrincipal.isNullPrincipal,
+ "resultPrincipal after resetting correct");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_form_restoration.html b/docshell/test/mochitest/test_form_restoration.html
new file mode 100644
index 0000000000..b929236770
--- /dev/null
+++ b/docshell/test/mochitest/test_form_restoration.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form restoration for no-store pages</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ function waitForMessage(aBroadcastChannel) {
+ return new Promise(resolve => {
+ aBroadcastChannel.addEventListener("message", ({ data }) => {
+ resolve(data);
+ }, { once: true });
+ });
+ }
+
+ function postMessageAndWait(aBroadcastChannel, aMsg) {
+ let promise = waitForMessage(aBroadcastChannel);
+ aBroadcastChannel.postMessage(aMsg);
+ return promise;
+ }
+
+ async function startTest(aTestFun) {
+ let bc = new BroadcastChannel("form_restoration");
+
+ let promise = waitForMessage(bc);
+ window.open("file_form_restoration_no_store.html", "", "noopener");
+ await promise;
+
+ // test steps
+ await aTestFun(bc);
+
+ // close broadcast channel and window
+ bc.postMessage("close");
+ bc.close();
+ }
+
+ /* Test for bug1740517 */
+ add_task(async function history_back() {
+ await startTest(async (aBroadcastChannel) => {
+ // update form data
+ aBroadcastChannel.postMessage("enter_data");
+
+ // navigate
+ await postMessageAndWait(aBroadcastChannel, "navigate");
+
+ // history back
+ let { persisted, formData } = await postMessageAndWait(aBroadcastChannel, "back");
+
+ // check form data
+ ok(!persisted, "Page with a no-store header shouldn't be bfcached.");
+ is(formData, "initial", "We shouldn't restore form data when going back to a page with a no-store header.");
+ });
+ });
+
+ /* Test for bug1752250 */
+ add_task(async function location_reload() {
+ await startTest(async (aBroadcastChannel) => {
+ // update form data
+ aBroadcastChannel.postMessage("enter_data");
+
+ // reload
+ let { persisted, formData } = await postMessageAndWait(aBroadcastChannel, "reload");
+
+ // check form data
+ ok(!persisted, "Page with a no-store header shouldn't be bfcached.");
+ is(formData, "initial", "We shouldn't restore form data when reload a page with a no-store header.");
+ });
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_framedhistoryframes.html b/docshell/test/mochitest/test_framedhistoryframes.html
new file mode 100644
index 0000000000..f90028af0a
--- /dev/null
+++ b/docshell/test/mochitest/test_framedhistoryframes.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=602256
+-->
+<head>
+ <title>Test for Bug 602256</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602256">Mozilla Bug 602256</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 602256 */
+
+SimpleTest.waitForExplicitFinish();
+var win = window.open("file_framedhistoryframes.html");
+
+function done() {
+ win.close();
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html b/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html
new file mode 100644
index 0000000000..dac11853d0
--- /dev/null
+++ b/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body onload="test()">
+ <script>
+ /*
+ Test to verify that when we change an OOP iframe to one that has a
+ srcdoc it loads in the correct process, which in this case is this
+ test document.
+ */
+ SimpleTest.waitForExplicitFinish();
+ async function test() {
+ // Create an OOP iframe
+ let frame = document.createElement("iframe");
+ await new Promise(r => {
+ frame.onload = r;
+ document.body.appendChild(frame);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ frame.contentWindow.location = "http://example.net/tests/docshell/test/dummy_page.html";
+ });
+
+ if (SpecialPowers.effectiveIsolationStrategy() == SpecialPowers.ISOLATION_STRATEGY.IsolateEverything) {
+ ok(SpecialPowers.Cu.isRemoteProxy(frame.contentWindow), "should be a remote frame");
+ }
+
+ // Remove the attribute so we can set a srcdoc attribute on it
+ frame.removeAttribute("src");
+
+ // Set a srcdoc attribute on this iframe and wait for the load
+ await new Promise(r => {
+ frame.onload = r;
+ frame.setAttribute("srcdoc", '<html><body>body of the srcdoc frame</body></html>');
+ });
+
+ // We should be in the same process as this test document
+ ok(!SpecialPowers.Cu.isRemoteProxy(frame.contentWindow), "should NOT be a remote frame");
+ SimpleTest.finish();
+ }
+ </script>
+</body>
+
diff --git a/docshell/test/mochitest/test_javascript_sandboxed_popup.html b/docshell/test/mochitest/test_javascript_sandboxed_popup.html
new file mode 100644
index 0000000000..edce93c26f
--- /dev/null
+++ b/docshell/test/mochitest/test_javascript_sandboxed_popup.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<iframe srcdoc="<a href='javascript:opener.parent.ok(false, `The JS ran!`)' target=_blank rel=opener>click</a>"
+ sandbox="allow-popups allow-same-origin"></iframe>
+
+<script>
+add_task(async function() {
+ let promise = new Promise(resolve =>{
+ SpecialPowers.addObserver(function obs(subject) {
+ is(subject.opener, window[0],
+ "blocked javascript URI should have been targeting the pop-up document");
+ subject.close();
+ SpecialPowers.removeObserver(obs, "javascript-uri-blocked-by-sandbox");
+ resolve();
+ }, "javascript-uri-blocked-by-sandbox");
+ });
+ document.querySelector("iframe").contentDocument.querySelector("a").click();
+ await promise;
+});
+</script>
+</body>
diff --git a/docshell/test/mochitest/test_load_during_reload.html b/docshell/test/mochitest/test_load_during_reload.html
new file mode 100644
index 0000000000..88856b0100
--- /dev/null
+++ b/docshell/test/mochitest/test_load_during_reload.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test loading a new page after calling reload()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+
+ function promiseForLoad() {
+ return new Promise(resolve => {
+ addEventListener("message", resolve, { once: true });
+ });
+ }
+
+ add_task(async function runTest() {
+ let win = window.open("file_load_during_reload.html");
+ await promiseForLoad();
+
+ win.location.reload();
+ win.location.href = "file_load_during_reload.html?nextpage";
+ await promiseForLoad();
+
+ ok(win.location.href.includes("nextpage"), "Should have loaded the next page.");
+ win.close();
+ });
+
+ add_task(async function runTest2() {
+ let win = window.open("file_load_during_reload.html");
+ await promiseForLoad();
+
+ win.history.replaceState("", "", "?1");
+ win.location.reload();
+ win.history.pushState("", "", "?2");
+ win.location.reload();
+ await promiseForLoad();
+
+ ok(win.location.href.includes("2"), "Should have loaded the second page.");
+ win.close();
+ });
+
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_navigate_after_pagehide.html b/docshell/test/mochitest/test_navigate_after_pagehide.html
new file mode 100644
index 0000000000..17d58d6e62
--- /dev/null
+++ b/docshell/test/mochitest/test_navigate_after_pagehide.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Test for navigation attempts by scripts in inactive inner window</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+<body>
+<iframe src="dummy_page.html" id="iframe"></iframe>
+
+<script>
+"use strict";
+
+add_task(async function() {
+ let iframe = document.getElementById("iframe");
+
+ let navigate = iframe.contentWindow.eval(`(function() {
+ location.href = "/";
+ })`);
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ iframe.src = "http://example.com/";
+ await new Promise(resolve =>
+ iframe.addEventListener("load", resolve, { once: true })
+ );
+
+ // This should do nothing. But, importantly, it should especially not crash.
+ navigate();
+
+ ok(true, "We didn't crash");
+});
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_pushState_after_document_open.html b/docshell/test/mochitest/test_pushState_after_document_open.html
new file mode 100644
index 0000000000..b81951b7ae
--- /dev/null
+++ b/docshell/test/mochitest/test_pushState_after_document_open.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=957479
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 957479</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 957479 */
+ SimpleTest.waitForExplicitFinish();
+ // Child needs to invoke us, otherwise our onload will fire before the child
+ // has done the write/close bit.
+ onmessage = function doTest() {
+ is(frames[0].location.pathname, "/tests/docshell/test/mochitest/file_pushState_after_document_open.html",
+ "Should have the right path here");
+ is(frames[0].location.hash, "", "Should have the right hash here");
+ frames[0].history.pushState({}, "", frames[0].document.URL + "#foopy");
+ is(frames[0].location.pathname, "/tests/docshell/test/mochitest/file_pushState_after_document_open.html",
+ "Pathname should not have changed");
+ is(frames[0].location.hash, "#foopy", "Hash should have changed");
+ SimpleTest.finish();
+ };
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957479">Mozilla Bug 957479</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe src="file_pushState_after_document_open.html"></iframe>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_redirect_history.html b/docshell/test/mochitest/test_redirect_history.html
new file mode 100644
index 0000000000..a67c808405
--- /dev/null
+++ b/docshell/test/mochitest/test_redirect_history.html
@@ -0,0 +1,58 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Test for redirect from POST</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <script>
+ "use strict";
+
+ info("Starting tests");
+
+ let tests = new Map([
+ ["sameorigin", window.location.origin],
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ ["crossorigin", "http://test1.example.com"],
+ ]);
+ for (let [kind, origin] of tests) {
+ add_task(async function runTest() {
+ info(`Submitting to ${origin}`);
+
+ let win;
+ await new Promise(resolve => {
+ addEventListener("message", resolve, { once: true });
+ info("Loading file_redirect_history.html");
+ win = window.open("file_redirect_history.html");
+ });
+ info("Done loading file_redirect_history.html");
+
+ let length = win.history.length;
+ let loc = win.location.toString();
+
+ await new Promise(resolve => {
+ addEventListener("message", resolve, { once: true });
+ info("Posting");
+ win.postMessage(`${origin}/tests/docshell/test/mochitest/form_submit_redirect.sjs?redirectTo=${loc}`, "*")
+ });
+ info("Done posting\n");
+ is(win.history.length, length, `Test ${kind}: history length should not change.`);
+ info(`Length=${win.history.length}`);
+ is(win.location.toString(), loc, `Test ${kind}: location should not change.`);
+
+ await new Promise(resolve => {
+ addEventListener("message", resolve, { once: true });
+ info("Reloading");
+ win.location.reload();
+ });
+ info("Done reloading\n");
+ is(win.location.toString(), loc, `Test ${kind}: location should not change after reload.`);
+
+ win.close();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/mochitest/test_triggeringprincipal_location_seturi.html b/docshell/test/mochitest/test_triggeringprincipal_location_seturi.html
new file mode 100644
index 0000000000..92e20b03ea
--- /dev/null
+++ b/docshell/test/mochitest/test_triggeringprincipal_location_seturi.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN_URI = "http://mochi.test:8888/tests/docshell/test/dummy_page.html";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const CROSS_ORIGIN_URI = "http://example.com/tests/docshell/test/dummy_page.html";
+const NUMBER_OF_TESTS = 3;
+let testCounter = 0;
+
+function checkFinish() {
+ testCounter++;
+ if (testCounter < NUMBER_OF_TESTS) {
+ return;
+ }
+ SimpleTest.finish();
+}
+
+// ---- test 1 ----
+
+let myFrame1 = document.createElement("iframe");
+myFrame1.src = SAME_ORIGIN_URI;
+myFrame1.addEventListener("load", checkLoadFrame1);
+document.documentElement.appendChild(myFrame1);
+
+function checkLoadFrame1() {
+ myFrame1.removeEventListener("load", checkLoadFrame1);
+ // window.location.href is no longer cross-origin accessible in gecko.
+ is(SpecialPowers.wrap(myFrame1.contentWindow).location.href, SAME_ORIGIN_URI,
+ "initial same origin dummy loaded into frame1");
+
+ SpecialPowers.wrap(myFrame1.contentWindow).location.hash = "#bar";
+ is(SpecialPowers.wrap(myFrame1.contentWindow).location.href, SAME_ORIGIN_URI + "#bar",
+ "initial same origin dummy#bar loaded into iframe1");
+
+ myFrame1.addEventListener("load", checkNavFrame1);
+ myFrame1.src = CROSS_ORIGIN_URI;
+}
+
+async function checkNavFrame1() {
+ myFrame1.removeEventListener("load", checkNavFrame1);
+ is(await SpecialPowers.spawn(myFrame1, [], () => this.content.location.href),
+ CROSS_ORIGIN_URI,
+ "cross origin dummy loaded into frame1");
+
+ myFrame1.addEventListener("load", checkBackNavFrame1);
+ myFrame1.src = SAME_ORIGIN_URI + "#bar";
+}
+
+async function checkBackNavFrame1() {
+ myFrame1.removeEventListener("load", checkBackNavFrame1);
+ is(await SpecialPowers.spawn(myFrame1, [], () => this.content.location.href),
+ SAME_ORIGIN_URI + "#bar",
+ "navagiating back to same origin dummy for frame1");
+ checkFinish();
+}
+
+// ---- test 2 ----
+
+let myFrame2 = document.createElement("iframe");
+myFrame2.src = "about:blank";
+myFrame2.addEventListener("load", checkLoadFrame2);
+document.documentElement.appendChild(myFrame2);
+
+function checkLoadFrame2() {
+ myFrame2.removeEventListener("load", checkLoadFrame2);
+ is(SpecialPowers.wrap(myFrame2.contentWindow).location.href, "about:blank",
+ "initial about:blank frame loaded");
+
+ myFrame2.contentWindow.location.hash = "#foo";
+ is(SpecialPowers.wrap(myFrame2.contentWindow).location.href, "about:blank#foo",
+ "about:blank#foo frame loaded");
+
+ myFrame2.addEventListener("load", checkHistoryFrame2);
+ myFrame2.src = "about:blank";
+}
+
+function checkHistoryFrame2() {
+ myFrame2.removeEventListener("load", checkHistoryFrame2);
+ is(SpecialPowers.wrap(myFrame2.contentWindow).location.href, "about:blank",
+ "about:blank frame loaded again");
+ checkFinish();
+}
+
+// ---- test 3 ----
+
+let myFrame3 = document.createElement("frame");
+document.documentElement.appendChild(myFrame3);
+myFrame3.contentWindow.location.hash = "#foo";
+
+is(myFrame3.contentWindow.location.href, "about:blank#foo",
+ "created history entry with about:blank#foo");
+checkFinish();
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/mochitest/test_windowedhistoryframes.html b/docshell/test/mochitest/test_windowedhistoryframes.html
new file mode 100644
index 0000000000..a874da098c
--- /dev/null
+++ b/docshell/test/mochitest/test_windowedhistoryframes.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=602256
+-->
+<head>
+ <title>Test for Bug 602256</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602256">Mozilla Bug 602256</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 602256 */
+
+SimpleTest.waitForExplicitFinish();
+
+function done() {
+ subWin.close();
+ SimpleTest.finish();
+}
+
+var subWin = window.open("historyframes.html", "_blank");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/mochitest/url1_historyframe.html b/docshell/test/mochitest/url1_historyframe.html
new file mode 100644
index 0000000000..b86af4b3fa
--- /dev/null
+++ b/docshell/test/mochitest/url1_historyframe.html
@@ -0,0 +1 @@
+<p id='text'>Test1</p>
diff --git a/docshell/test/mochitest/url2_historyframe.html b/docshell/test/mochitest/url2_historyframe.html
new file mode 100644
index 0000000000..24374d1a5b
--- /dev/null
+++ b/docshell/test/mochitest/url2_historyframe.html
@@ -0,0 +1 @@
+<p id='text'>Test2</p>
diff --git a/docshell/test/moz.build b/docshell/test/moz.build
new file mode 100644
index 0000000000..0cdf1633c1
--- /dev/null
+++ b/docshell/test/moz.build
@@ -0,0 +1,134 @@
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Navigation")
+
+with Files("browser/*_bug234628*"):
+ BUG_COMPONENT = ("Core", "Internationalization")
+
+with Files("browser/*_bug349769*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("browser/*_bug388121*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("browser/*_bug655270*"):
+ BUG_COMPONENT = ("Toolkit", "Places")
+
+with Files("browser/*_bug655273*"):
+ BUG_COMPONENT = ("Firefox", "Menus")
+
+with Files("browser/*_bug852909*"):
+ BUG_COMPONENT = ("Firefox", "Menus")
+
+with Files("browser/*bug92473*"):
+ BUG_COMPONENT = ("Core", "Internationalization")
+
+with Files("browser/*loadDisallowInherit*"):
+ BUG_COMPONENT = ("Firefox", "Address Bar")
+
+with Files("browser/*tab_touch_events*"):
+ BUG_COMPONENT = ("Core", "DOM: Events")
+
+with Files("browser/*ua_emulation*"):
+ BUG_COMPONENT = ("DevTools", "General")
+
+with Files("chrome/*112564*"):
+ BUG_COMPONENT = ("Core", "Networking: HTTP")
+
+with Files("chrome/*303267*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("chrome/*453650*"):
+ BUG_COMPONENT = ("Core", "Layout")
+
+with Files("chrome/*565388*"):
+ BUG_COMPONENT = ("Core", "Widget")
+
+with Files("chrome/*582176*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("chrome/*608669*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("chrome/*690056*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("chrome/*92598*"):
+ BUG_COMPONENT = ("Core", "Networking: HTTP")
+
+with Files("iframesandbox/**"):
+ BUG_COMPONENT = ("Core", "Security")
+
+with Files("iframesandbox/*marquee_event_handlers*"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+
+with Files("mochitest/*1045096*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*1151421*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*402210*"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+with Files("mochitest/*509055*"):
+ BUG_COMPONENT = ("Firefox", "Bookmarks & History")
+
+with Files("mochitest/*511449*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("mochitest/*551225*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*570341*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*580069*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*637644*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*640387*"):
+ BUG_COMPONENT = ("Core", "DOM: Events")
+
+with Files("mochitest/*668513*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*797909*"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+with Files("mochitest/*forceinheritprincipal*"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+
+with Files("navigation/*13871.html"):
+ BUG_COMPONENT = ("Core", "Security")
+
+with Files("navigation/*386782*"):
+ BUG_COMPONENT = ("Core", "DOM: Editor")
+
+with Files("navigation/*430624*"):
+ BUG_COMPONENT = ("Core", "DOM: Editor")
+
+with Files("navigation/*430723*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("navigation/*child*"):
+ BUG_COMPONENT = ("Core", "Security")
+
+with Files("navigation/*opener*"):
+ BUG_COMPONENT = ("Core", "Security")
+
+with Files("navigation/*reserved*"):
+ BUG_COMPONENT = ("Core", "Security")
+
+with Files("navigation/*triggering*"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+
+with Files("unit/*442584*"):
+ BUG_COMPONENT = ("Core", "Networking: Cache")
+
+with Files("unit/*setUsePrivateBrowsing*"):
+ BUG_COMPONENT = ("Firefox", "Extension Compatibility")
diff --git a/docshell/test/navigation/NavigationUtils.js b/docshell/test/navigation/NavigationUtils.js
new file mode 100644
index 0000000000..c4b52dc62f
--- /dev/null
+++ b/docshell/test/navigation/NavigationUtils.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// /////////////////////////////////////////////////////////////////////////
+//
+// Utilities for navigation tests
+//
+// /////////////////////////////////////////////////////////////////////////
+
+var body = "This frame was navigated.";
+var target_url = "navigation_target_url.html";
+
+var popup_body = "This is a popup";
+var target_popup_url = "navigation_target_popup_url.html";
+
+// /////////////////////////////////////////////////////////////////////////
+// Functions that navigate frames
+// /////////////////////////////////////////////////////////////////////////
+
+function navigateByLocation(wnd) {
+ try {
+ wnd.location = target_url;
+ } catch (ex) {
+ // We need to keep our finished frames count consistent.
+ // Oddly, this ends up simulating the behavior of IE7.
+ window.open(target_url, "_blank", "width=10,height=10");
+ }
+}
+
+function navigateByOpen(name) {
+ window.open(target_url, name, "width=10,height=10");
+}
+
+function navigateByForm(name) {
+ var form = document.createElement("form");
+ form.action = target_url;
+ form.method = "POST";
+ form.target = name;
+ document.body.appendChild(form);
+ form.submit();
+}
+
+var hyperlink_count = 0;
+
+function navigateByHyperlink(name) {
+ var link = document.createElement("a");
+ link.href = target_url;
+ link.target = name;
+ link.id = "navigation_hyperlink_" + hyperlink_count++;
+ document.body.appendChild(link);
+ sendMouseEvent({ type: "click" }, link.id);
+}
+
+// /////////////////////////////////////////////////////////////////////////
+// Functions that call into Mochitest framework
+// /////////////////////////////////////////////////////////////////////////
+
+async function isNavigated(wnd, message) {
+ var result = null;
+ try {
+ result = await SpecialPowers.spawn(wnd, [], () =>
+ this.content.document.body.innerHTML.trim()
+ );
+ } catch (ex) {
+ result = ex;
+ }
+ is(result, body, message);
+}
+
+function isBlank(wnd, message) {
+ var result = null;
+ try {
+ result = wnd.document.body.innerHTML.trim();
+ } catch (ex) {
+ result = ex;
+ }
+ is(result, "This is a blank document.", message);
+}
+
+function isAccessible(wnd, message) {
+ try {
+ wnd.document.body.innerHTML;
+ ok(true, message);
+ } catch (ex) {
+ ok(false, message);
+ }
+}
+
+function isInaccessible(wnd, message) {
+ try {
+ wnd.document.body.innerHTML;
+ ok(false, message);
+ } catch (ex) {
+ ok(true, message);
+ }
+}
+
+function delay(msec) {
+ return new Promise(resolve => setTimeout(resolve, msec));
+}
+
+// /////////////////////////////////////////////////////////////////////////
+// Functions that uses SpecialPowers.spawn
+// /////////////////////////////////////////////////////////////////////////
+
+async function waitForFinishedFrames(numFrames) {
+ SimpleTest.requestFlakyTimeout("Polling");
+
+ var finishedWindows = new Set();
+
+ async function searchForFinishedFrames(win) {
+ try {
+ let { href, bodyText, readyState } = await SpecialPowers.spawn(
+ win,
+ [],
+ () => {
+ return {
+ href: this.content.location.href,
+ bodyText:
+ this.content.document.body &&
+ this.content.document.body.textContent.trim(),
+ readyState: this.content.document.readyState,
+ };
+ }
+ );
+
+ if (
+ (href.endsWith(target_url) || href.endsWith(target_popup_url)) &&
+ (bodyText == body || bodyText == popup_body) &&
+ readyState == "complete"
+ ) {
+ finishedWindows.add(SpecialPowers.getBrowsingContextID(win));
+ }
+ } catch (e) {
+ // This may throw if a frame is not fully initialized, in which
+ // case we'll handle it in a later iteration.
+ }
+
+ for (let i = 0; i < win.frames.length; i++) {
+ await searchForFinishedFrames(win.frames[i]);
+ }
+ }
+
+ while (finishedWindows.size < numFrames) {
+ await delay(500);
+
+ for (let win of SpecialPowers.getGroupTopLevelWindows(window)) {
+ win = SpecialPowers.unwrap(win);
+ await searchForFinishedFrames(win);
+ }
+ }
+
+ if (finishedWindows.size > numFrames) {
+ throw new Error("Too many frames loaded.");
+ }
+}
+
+async function getFramesByName(name) {
+ let results = [];
+ for (let win of SpecialPowers.getGroupTopLevelWindows(window)) {
+ win = SpecialPowers.unwrap(win);
+ if (
+ (await SpecialPowers.spawn(win, [], () => this.content.name)) === name
+ ) {
+ results.push(win);
+ }
+ }
+
+ return results;
+}
+
+async function cleanupWindows() {
+ for (let win of SpecialPowers.getGroupTopLevelWindows(window)) {
+ win = SpecialPowers.unwrap(win);
+ if (win.closed) {
+ continue;
+ }
+
+ let href = "";
+ try {
+ href = await SpecialPowers.spawn(
+ win,
+ [],
+ () =>
+ this.content && this.content.location && this.content.location.href
+ );
+ } catch (error) {
+ // SpecialPowers.spawn(win, ...) throws if win is closed. We did
+ // our best to not call it on a closed window, but races happen.
+ if (!win.closed) {
+ throw error;
+ }
+ }
+
+ if (
+ href &&
+ (href.endsWith(target_url) || href.endsWith(target_popup_url))
+ ) {
+ win.close();
+ }
+ }
+}
diff --git a/docshell/test/navigation/blank.html b/docshell/test/navigation/blank.html
new file mode 100644
index 0000000000..5360333f1d
--- /dev/null
+++ b/docshell/test/navigation/blank.html
@@ -0,0 +1 @@
+<html><body>This is a blank document.</body></html> \ No newline at end of file
diff --git a/docshell/test/navigation/bluebox_bug430723.html b/docshell/test/navigation/bluebox_bug430723.html
new file mode 100644
index 0000000000..5dcc533562
--- /dev/null
+++ b/docshell/test/navigation/bluebox_bug430723.html
@@ -0,0 +1,6 @@
+<html><head>
+<script> window.addEventListener("pageshow", function() { opener.nextTest(); }); </script>
+</head><body>
+<div style="position:absolute; left:0px; top:0px; width:50%; height:150%; background-color:blue">
+<p>This is a very tall blue box.</p>
+</div></body></html>
diff --git a/docshell/test/navigation/browser.toml b/docshell/test/navigation/browser.toml
new file mode 100644
index 0000000000..f56b1dea12
--- /dev/null
+++ b/docshell/test/navigation/browser.toml
@@ -0,0 +1,28 @@
+[DEFAULT]
+support-files = [
+ "bug343515_pg1.html",
+ "bug343515_pg2.html",
+ "bug343515_pg3.html",
+ "bug343515_pg3_1.html",
+ "bug343515_pg3_1_1.html",
+ "bug343515_pg3_2.html",
+ "blank.html",
+ "redirect_to_blank.sjs",
+]
+
+["browser_bug343515.js"]
+
+["browser_bug1757458.js"]
+
+["browser_ghistorymaxsize_is_0.js"]
+
+["browser_test-content-chromeflags.js"]
+tags = "openwindow"
+
+["browser_test_bfcache_eviction.js"]
+
+["browser_test_shentry_wireframe.js"]
+skip-if = ["!sessionHistoryInParent"]
+
+["browser_test_simultaneous_normal_and_history_loads.js"]
+skip-if = ["!sessionHistoryInParent"] # The test is for the new session history
diff --git a/docshell/test/navigation/browser_bug1757458.js b/docshell/test/navigation/browser_bug1757458.js
new file mode 100644
index 0000000000..958012f7eb
--- /dev/null
+++ b/docshell/test/navigation/browser_bug1757458.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ let testPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "view-source:http://example.com"
+ ) + "redirect_to_blank.sjs";
+
+ let testPage2 = "data:text/html,<div>Second page</div>";
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: testPage },
+ async function (browser) {
+ await ContentTask.spawn(browser, [], async () => {
+ Assert.ok(
+ content.document.getElementById("viewsource").localName == "body",
+ "view-source document's body should have id='viewsource'."
+ );
+ content.document
+ .getElementById("viewsource")
+ .setAttribute("onunload", "/* disable bfcache*/");
+ });
+
+ BrowserTestUtils.startLoadingURIString(browser, testPage2);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+ browser.browsingContext.goBack();
+ await pageShownPromise;
+
+ await ContentTask.spawn(browser, [], async () => {
+ Assert.ok(
+ content.document.getElementById("viewsource").localName == "body",
+ "view-source document's body should have id='viewsource'."
+ );
+ });
+ }
+ );
+});
diff --git a/docshell/test/navigation/browser_bug343515.js b/docshell/test/navigation/browser_bug343515.js
new file mode 100644
index 0000000000..aaf0df6a9c
--- /dev/null
+++ b/docshell/test/navigation/browser_bug343515.js
@@ -0,0 +1,276 @@
+// Test for bug 343515 - Need API for tabbrowsers to tell docshells they're visible/hidden
+
+// Globals
+var testPath = "http://mochi.test:8888/browser/docshell/test/navigation/";
+var ctx = {};
+
+add_task(async function () {
+ // Step 1.
+
+ // Get a handle on the initial tab
+ ctx.tab0 = gBrowser.selectedTab;
+ ctx.tab0Browser = gBrowser.getBrowserForTab(ctx.tab0);
+
+ await BrowserTestUtils.waitForCondition(
+ () => ctx.tab0Browser.docShellIsActive,
+ "Timed out waiting for initial tab to be active."
+ );
+
+ // Open a New Tab
+ ctx.tab1 = BrowserTestUtils.addTab(gBrowser, testPath + "bug343515_pg1.html");
+ ctx.tab1Browser = gBrowser.getBrowserForTab(ctx.tab1);
+ await BrowserTestUtils.browserLoaded(ctx.tab1Browser);
+
+ // Step 2.
+ is(
+ testPath + "bug343515_pg1.html",
+ ctx.tab1Browser.currentURI.spec,
+ "Got expected tab 1 url in step 2"
+ );
+
+ // Our current tab should still be active
+ ok(ctx.tab0Browser.docShellIsActive, "Tab 0 should still be active");
+ ok(!ctx.tab1Browser.docShellIsActive, "Tab 1 should not be active");
+
+ // Switch to tab 1
+ await BrowserTestUtils.switchTab(gBrowser, ctx.tab1);
+
+ // Tab 1 should now be active
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active");
+
+ // Open another tab
+ ctx.tab2 = BrowserTestUtils.addTab(gBrowser, testPath + "bug343515_pg2.html");
+ ctx.tab2Browser = gBrowser.getBrowserForTab(ctx.tab2);
+
+ await BrowserTestUtils.browserLoaded(ctx.tab2Browser);
+
+ // Step 3.
+ is(
+ testPath + "bug343515_pg2.html",
+ ctx.tab2Browser.currentURI.spec,
+ "Got expected tab 2 url in step 3"
+ );
+
+ // Tab 0 should be inactive, Tab 1 should be active
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active");
+
+ // Tab 2's window _and_ its iframes should be inactive
+ ok(!ctx.tab2Browser.docShellIsActive, "Tab 2 should be inactive");
+
+ await SpecialPowers.spawn(ctx.tab2Browser, [], async function () {
+ Assert.equal(content.frames.length, 2, "Tab 2 should have 2 iframes");
+ for (var i = 0; i < content.frames.length; i++) {
+ info("step 3, frame " + i + " info: " + content.frames[i].location);
+ let bc = content.frames[i].browsingContext;
+ Assert.ok(!bc.isActive, `Tab2 iframe ${i} should be inactive`);
+ }
+ });
+
+ // Navigate tab 2 to a different page
+ BrowserTestUtils.startLoadingURIString(
+ ctx.tab2Browser,
+ testPath + "bug343515_pg3.html"
+ );
+
+ await BrowserTestUtils.browserLoaded(ctx.tab2Browser);
+
+ // Step 4.
+
+ async function checkTab2Active(outerExpected) {
+ await SpecialPowers.spawn(
+ ctx.tab2Browser,
+ [outerExpected],
+ async function (expected) {
+ function isActive(aWindow) {
+ var docshell = aWindow.docShell;
+ info(`checking ${docshell.browsingContext.id}`);
+ return docshell.browsingContext.isActive;
+ }
+
+ let active = expected ? "active" : "inactive";
+ Assert.equal(content.frames.length, 2, "Tab 2 should have 2 iframes");
+ for (var i = 0; i < content.frames.length; i++) {
+ info("step 4, frame " + i + " info: " + content.frames[i].location);
+ }
+ Assert.equal(
+ content.frames[0].frames.length,
+ 1,
+ "Tab 2 iframe 0 should have 1 iframes"
+ );
+ Assert.equal(
+ isActive(content.frames[0]),
+ expected,
+ `Tab2 iframe 0 should be ${active}`
+ );
+ Assert.equal(
+ isActive(content.frames[0].frames[0]),
+ expected,
+ `Tab2 iframe 0 subiframe 0 should be ${active}`
+ );
+ Assert.equal(
+ isActive(content.frames[1]),
+ expected,
+ `Tab2 iframe 1 should be ${active}`
+ );
+ }
+ );
+ }
+
+ is(
+ testPath + "bug343515_pg3.html",
+ ctx.tab2Browser.currentURI.spec,
+ "Got expected tab 2 url in step 4"
+ );
+
+ // Tab 0 should be inactive, Tab 1 should be active
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active");
+
+ // Tab2 and all descendants should be inactive
+ await checkTab2Active(false);
+
+ // Switch to Tab 2
+ await BrowserTestUtils.switchTab(gBrowser, ctx.tab2);
+
+ // Check everything
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(!ctx.tab1Browser.docShellIsActive, "Tab 1 should be inactive");
+ ok(ctx.tab2Browser.docShellIsActive, "Tab 2 should be active");
+
+ await checkTab2Active(true);
+
+ // Go back
+ let backDone = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testPath + "bug343515_pg2.html"
+ );
+ ctx.tab2Browser.goBack();
+ await backDone;
+
+ // Step 5.
+
+ // Check everything
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(!ctx.tab1Browser.docShellIsActive, "Tab 1 should be inactive");
+ ok(ctx.tab2Browser.docShellIsActive, "Tab 2 should be active");
+ is(
+ testPath + "bug343515_pg2.html",
+ ctx.tab2Browser.currentURI.spec,
+ "Got expected tab 2 url in step 5"
+ );
+
+ await SpecialPowers.spawn(ctx.tab2Browser, [], async function () {
+ for (var i = 0; i < content.frames.length; i++) {
+ let bc = content.frames[i].browsingContext;
+ Assert.ok(bc.isActive, `Tab2 iframe ${i} should be active`);
+ }
+ });
+
+ // Switch to tab 1
+ await BrowserTestUtils.switchTab(gBrowser, ctx.tab1);
+
+ // Navigate to page 3
+ BrowserTestUtils.startLoadingURIString(
+ ctx.tab1Browser,
+ testPath + "bug343515_pg3.html"
+ );
+
+ await BrowserTestUtils.browserLoaded(ctx.tab1Browser);
+
+ // Step 6.
+
+ // Check everything
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active");
+ is(
+ testPath + "bug343515_pg3.html",
+ ctx.tab1Browser.currentURI.spec,
+ "Got expected tab 1 url in step 6"
+ );
+
+ await SpecialPowers.spawn(ctx.tab1Browser, [], async function () {
+ function isActive(aWindow) {
+ var docshell = aWindow.docShell;
+ info(`checking ${docshell.browsingContext.id}`);
+ return docshell.browsingContext.isActive;
+ }
+
+ Assert.ok(isActive(content.frames[0]), "Tab1 iframe 0 should be active");
+ Assert.ok(
+ isActive(content.frames[0].frames[0]),
+ "Tab1 iframe 0 subiframe 0 should be active"
+ );
+ Assert.ok(isActive(content.frames[1]), "Tab1 iframe 1 should be active");
+ });
+
+ ok(!ctx.tab2Browser.docShellIsActive, "Tab 2 should be inactive");
+
+ await SpecialPowers.spawn(ctx.tab2Browser, [], async function () {
+ for (var i = 0; i < content.frames.length; i++) {
+ let bc = content.frames[i].browsingContext;
+ Assert.ok(!bc.isActive, `Tab2 iframe ${i} should be inactive`);
+ }
+ });
+
+ // Go forward on tab 2
+ let forwardDone = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testPath + "bug343515_pg3.html"
+ );
+ ctx.tab2Browser.goForward();
+ await forwardDone;
+
+ // Step 7.
+
+ async function checkBrowser(browser, outerTabNum, outerActive) {
+ let data = { tabNum: outerTabNum, active: outerActive };
+ await SpecialPowers.spawn(
+ browser,
+ [data],
+ async function ({ tabNum, active }) {
+ function isActive(aWindow) {
+ var docshell = aWindow.docShell;
+ info(`checking ${docshell.browsingContext.id}`);
+ return docshell.browsingContext.isActive;
+ }
+
+ let activestr = active ? "active" : "inactive";
+ Assert.equal(
+ isActive(content.frames[0]),
+ active,
+ `Tab${tabNum} iframe 0 should be ${activestr}`
+ );
+ Assert.equal(
+ isActive(content.frames[0].frames[0]),
+ active,
+ `Tab${tabNum} iframe 0 subiframe 0 should be ${activestr}`
+ );
+ Assert.equal(
+ isActive(content.frames[1]),
+ active,
+ `Tab${tabNum} iframe 1 should be ${activestr}`
+ );
+ }
+ );
+ }
+
+ // Check everything
+ ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive");
+ ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active");
+ is(
+ testPath + "bug343515_pg3.html",
+ ctx.tab2Browser.currentURI.spec,
+ "Got expected tab 2 url in step 7"
+ );
+
+ await checkBrowser(ctx.tab1Browser, 1, true);
+
+ ok(!ctx.tab2Browser.docShellIsActive, "Tab 2 should be inactive");
+ await checkBrowser(ctx.tab2Browser, 2, false);
+
+ // Close the tabs we made
+ BrowserTestUtils.removeTab(ctx.tab1);
+ BrowserTestUtils.removeTab(ctx.tab2);
+});
diff --git a/docshell/test/navigation/browser_ghistorymaxsize_is_0.js b/docshell/test/navigation/browser_ghistorymaxsize_is_0.js
new file mode 100644
index 0000000000..87331b9bb9
--- /dev/null
+++ b/docshell/test/navigation/browser_ghistorymaxsize_is_0.js
@@ -0,0 +1,82 @@
+add_task(async function () {
+ // The urls don't really matter as long as they are of the same origin
+ var URL =
+ "http://mochi.test:8888/browser/docshell/test/navigation/bug343515_pg1.html";
+ var URL2 =
+ "http://mochi.test:8888/browser/docshell/test/navigation/bug343515_pg3_1.html";
+
+ // We want to test a specific code path that leads to this call
+ // https://searchfox.org/mozilla-central/rev/e7c61f4a68b974d5fecd216dc7407b631a24eb8f/docshell/base/nsDocShell.cpp#10795
+ // when gHistoryMaxSize is 0 and mIndex and mRequestedIndex are -1
+
+ // 1. Navigate to URL
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ // At this point, we haven't set gHistoryMaxSize to 0, and it is still 50 (default value).
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let sh = browser.browsingContext.sessionHistory;
+ is(
+ sh.count,
+ 1,
+ "We should have entry in session history because we haven't changed gHistoryMaxSize to be 0 yet"
+ );
+ is(
+ sh.index,
+ 0,
+ "Shistory's current index should be 0 because we haven't purged history yet"
+ );
+ } else {
+ await ContentTask.spawn(browser, null, () => {
+ var sh = content.window.docShell.QueryInterface(Ci.nsIWebNavigation)
+ .sessionHistory.legacySHistory;
+ is(
+ sh.count,
+ 1,
+ "We should have entry in session history because we haven't changed gHistoryMaxSize to be 0 yet"
+ );
+ is(
+ sh.index,
+ 0,
+ "Shistory's current index should be 0 because we haven't purged history yet"
+ );
+ });
+ }
+
+ var loadPromise = BrowserTestUtils.browserLoaded(browser, false, URL2);
+ // If we set the pref at the beginning of this page, then when we launch a child process
+ // to navigate to URL in Step 1, because of
+ // https://searchfox.org/mozilla-central/rev/e7c61f4a68b974d5fecd216dc7407b631a24eb8f/docshell/shistory/nsSHistory.cpp#308-312
+ // this pref will be set to the default value (currently 50). Setting this pref after the child process launches
+ // is a robust way to make sure it stays 0
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionhistory.max_entries", 0]],
+ });
+ // 2. Navigate to URL2
+ // We are navigating to a page with the same origin so that we will stay in the same process
+ BrowserTestUtils.startLoadingURIString(browser, URL2);
+ await loadPromise;
+
+ // 3. Reload the browser with specific flags so that we end up here
+ // https://searchfox.org/mozilla-central/rev/e7c61f4a68b974d5fecd216dc7407b631a24eb8f/docshell/base/nsDocShell.cpp#10795
+ var promise = BrowserTestUtils.browserLoaded(browser);
+ browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ await promise;
+
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let sh = browser.browsingContext.sessionHistory;
+ is(sh.count, 0, "We should not save any entries in session history");
+ is(sh.index, -1);
+ is(sh.requestedIndex, -1);
+ } else {
+ await ContentTask.spawn(browser, null, () => {
+ var sh = content.window.docShell.QueryInterface(Ci.nsIWebNavigation)
+ .sessionHistory.legacySHistory;
+ is(sh.count, 0, "We should not save any entries in session history");
+ is(sh.index, -1);
+ is(sh.requestedIndex, -1);
+ });
+ }
+ }
+ );
+});
diff --git a/docshell/test/navigation/browser_test-content-chromeflags.js b/docshell/test/navigation/browser_test-content-chromeflags.js
new file mode 100644
index 0000000000..ab1ae41c0c
--- /dev/null
+++ b/docshell/test/navigation/browser_test-content-chromeflags.js
@@ -0,0 +1,54 @@
+const TEST_PAGE = `data:text/html,<html><body><a href="about:blank" target="_blank">Test</a></body></html>`;
+const { CHROME_ALL, CHROME_REMOTE_WINDOW, CHROME_FISSION_WINDOW } =
+ Ci.nsIWebBrowserChrome;
+
+/**
+ * Tests that when we open new browser windows from content they
+ * get the full browser chrome.
+ */
+add_task(async function () {
+ // Make sure that the window.open call will open a new
+ // window instead of a new tab.
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [["browser.link.open_newwindow", 2]],
+ },
+ resolve
+ );
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async function (browser) {
+ let openedPromise = BrowserTestUtils.waitForNewWindow();
+ BrowserTestUtils.synthesizeMouse("a", 0, 0, {}, browser);
+ let win = await openedPromise;
+
+ let chromeFlags = win.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags;
+
+ let expected = CHROME_ALL;
+
+ // In the multi-process tab case, the new window will have the
+ // CHROME_REMOTE_WINDOW flag set.
+ if (gMultiProcessBrowser) {
+ expected |= CHROME_REMOTE_WINDOW;
+ }
+
+ // In the multi-process subframe case, the new window will have the
+ // CHROME_FISSION_WINDOW flag set.
+ if (gFissionBrowser) {
+ expected |= CHROME_FISSION_WINDOW;
+ }
+
+ is(chromeFlags, expected, "Window should have opened with all chrome");
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+ );
+});
diff --git a/docshell/test/navigation/browser_test_bfcache_eviction.js b/docshell/test/navigation/browser_test_bfcache_eviction.js
new file mode 100644
index 0000000000..5aac175ba4
--- /dev/null
+++ b/docshell/test/navigation/browser_test_bfcache_eviction.js
@@ -0,0 +1,102 @@
+add_task(async function () {
+ // We don't want the number of total viewers to be calculated by the available size
+ // for this test case. Instead, fix the number of viewers.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sessionhistory.max_total_viewers", 3],
+ ["docshell.shistory.testing.bfevict", true],
+ // If Fission is disabled, the pref is no-op.
+ ["fission.bfcacheInParent", true],
+ ],
+ });
+
+ // 1. Open a tab
+ var testPage =
+ "data:text/html,<html id='html1'><body id='body1'>First tab ever opened</body></html>";
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: testPage },
+ async function (browser) {
+ let testDone = {};
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ // 2. Add a promise that will be resolved when the 'content viewer evicted' event goes off
+ testDone.promise = SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ let webNavigation = content.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ let { legacySHistory } = webNavigation.sessionHistory;
+ // 3. Register a session history listener to listen for a 'content viewer evicted' event.
+ let historyListener = {
+ OnDocumentViewerEvicted() {
+ ok(
+ true,
+ "History listener got called after a content viewer was evicted"
+ );
+ legacySHistory.removeSHistoryListener(historyListener);
+ // 6. Resolve the promise when we got our 'content viewer evicted' event
+ resolve();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ legacySHistory.addSHistoryListener(historyListener);
+ // Keep the weak shistory listener alive
+ content._testListener = historyListener;
+ });
+ });
+ } else {
+ // 2. Add a promise that will be resolved when the 'content viewer evicted' event goes off
+ testDone.promise = new Promise(resolve => {
+ testDone.resolve = resolve;
+ });
+ let shistory = browser.browsingContext.sessionHistory;
+ // 3. Register a session history listener to listen for a 'content viewer evicted' event.
+ let historyListener = {
+ OnDocumentViewerEvicted() {
+ ok(
+ true,
+ "History listener got called after a content viewer was evicted"
+ );
+ shistory.removeSHistoryListener(historyListener);
+ delete window._testListener;
+ // 6. Resolve the promise when we got our 'content viewer evicted' event
+ testDone.resolve();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ shistory.addSHistoryListener(historyListener);
+ // Keep the weak shistory listener alive
+ window._testListener = historyListener;
+ }
+
+ // 4. Open a second tab
+ testPage = `data:text/html,<html id='html1'><body id='body1'>I am a second tab!</body></html>`;
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ testPage
+ );
+
+ // 5. Navigate the first tab to 4 different pages.
+ // We should get 1 content viewer evicted because it will be outside of the range.
+ // If we have the following pages in our session history: P1 P2 P3 P4 P5
+ // and we are currently at P5, then P1 is outside of the range
+ // (it is more than 3 entries away from current entry) and thus will be evicted.
+ for (var i = 0; i < 4; i++) {
+ testPage = `data:text/html,<html id='html1'><body id='body1'>${i}</body></html>`;
+ let pagePromise = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, testPage);
+ await pagePromise;
+ }
+ // 7. Wait for 'content viewer evicted' event to go off
+ await testDone.promise;
+
+ // 8. Close the second tab
+ BrowserTestUtils.removeTab(tab2);
+ }
+ );
+});
diff --git a/docshell/test/navigation/browser_test_shentry_wireframe.js b/docshell/test/navigation/browser_test_shentry_wireframe.js
new file mode 100644
index 0000000000..49a74ff1e9
--- /dev/null
+++ b/docshell/test/navigation/browser_test_shentry_wireframe.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BUILDER = "http://mochi.test:8888/document-builder.sjs?html=";
+const PAGE_1 = BUILDER + encodeURIComponent(`<html><body>Page 1</body></html>`);
+const PAGE_2 = BUILDER + encodeURIComponent(`<html><body>Page 2</body></html>`);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.history.collectWireframes", true]],
+ });
+});
+
+/**
+ * Test that capturing wireframes on nsISHEntriy's in the parent process
+ * happens at the right times.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(PAGE_1, async browser => {
+ let sh = browser.browsingContext.sessionHistory;
+ Assert.equal(
+ sh.count,
+ 1,
+ "Got the right SessionHistory entry count after initial tab load."
+ );
+ Assert.ok(
+ !sh.getEntryAtIndex(0).wireframe,
+ "No wireframe for the loaded entry after initial tab load."
+ );
+
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, PAGE_2);
+ BrowserTestUtils.startLoadingURIString(browser, PAGE_2);
+ await loaded;
+
+ Assert.equal(
+ sh.count,
+ 2,
+ "Got the right SessionHistory entry count after loading page 2."
+ );
+ Assert.ok(
+ sh.getEntryAtIndex(0).wireframe,
+ "A wireframe was captured for the first entry after loading page 2."
+ );
+ Assert.ok(
+ !sh.getEntryAtIndex(1).wireframe,
+ "No wireframe for the loaded entry after loading page 2."
+ );
+
+ // Now go back
+ loaded = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ browser.goBack();
+ await loaded;
+ Assert.ok(
+ sh.getEntryAtIndex(1).wireframe,
+ "A wireframe was captured for the second entry after going back."
+ );
+ Assert.ok(
+ !sh.getEntryAtIndex(0).wireframe,
+ "No wireframe for the loaded entry after going back."
+ );
+
+ // Now forward again
+ loaded = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ browser.goForward();
+ await loaded;
+
+ Assert.equal(
+ sh.count,
+ 2,
+ "Got the right SessionHistory entry count after going forward again."
+ );
+ Assert.ok(
+ sh.getEntryAtIndex(0).wireframe,
+ "A wireframe was captured for the first entry after going forward again."
+ );
+ Assert.ok(
+ !sh.getEntryAtIndex(1).wireframe,
+ "No wireframe for the loaded entry after going forward again."
+ );
+
+ // And using pushState
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.history.pushState({}, "", "nothing-1.html");
+ content.history.pushState({}, "", "nothing-2.html");
+ });
+
+ Assert.equal(
+ sh.count,
+ 4,
+ "Got the right SessionHistory entry count after using pushState."
+ );
+ Assert.ok(
+ sh.getEntryAtIndex(0).wireframe,
+ "A wireframe was captured for the first entry after using pushState."
+ );
+ Assert.ok(
+ sh.getEntryAtIndex(1).wireframe,
+ "A wireframe was captured for the second entry after using pushState."
+ );
+ Assert.ok(
+ sh.getEntryAtIndex(2).wireframe,
+ "A wireframe was captured for the third entry after using pushState."
+ );
+ Assert.ok(
+ !sh.getEntryAtIndex(3).wireframe,
+ "No wireframe for the loaded entry after using pushState."
+ );
+
+ // Now check that wireframes can be written to in case we're restoring
+ // an nsISHEntry from serialization.
+ let wireframe = sh.getEntryAtIndex(2).wireframe;
+ sh.getEntryAtIndex(2).wireframe = null;
+ Assert.equal(
+ sh.getEntryAtIndex(2).wireframe,
+ null,
+ "Successfully cleared wireframe."
+ );
+
+ sh.getEntryAtIndex(3).wireframe = wireframe;
+ Assert.deepEqual(
+ sh.getEntryAtIndex(3).wireframe,
+ wireframe,
+ "Successfully wrote a wireframe to an nsISHEntry."
+ );
+ });
+});
diff --git a/docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js b/docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js
new file mode 100644
index 0000000000..c6b13afd5e
--- /dev/null
+++ b/docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_normal_and_history_loads() {
+ // This test is for the case when session history lives in the parent process.
+ // BFCache is disabled to get more asynchronousness.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ let testPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ ) + "blank.html";
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: testPage },
+ async function (browser) {
+ for (let i = 0; i < 2; ++i) {
+ BrowserTestUtils.startLoadingURIString(browser, testPage + "?" + i);
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+
+ let sh = browser.browsingContext.sessionHistory;
+ is(sh.count, 3, "Should have 3 entries in the session history.");
+ is(sh.index, 2, "index should be 2");
+ is(sh.requestedIndex, -1, "requestedIndex should be -1");
+
+ // The following part is racy by definition. It is testing that
+ // eventually requestedIndex should become -1 again.
+ browser.browsingContext.goToIndex(1);
+ let historyLoad = BrowserTestUtils.browserLoaded(browser);
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(r => {
+ setTimeout(r, 10);
+ });
+ browser.browsingContext.loadURI(
+ Services.io.newURI(testPage + "?newload"),
+ {
+ triggeringPrincipal: browser.nodePrincipal,
+ }
+ );
+ let newLoad = BrowserTestUtils.browserLoaded(browser);
+ // Note, the loads are racy.
+ await historyLoad;
+ await newLoad;
+ is(sh.requestedIndex, -1, "requestedIndex should be -1");
+
+ browser.browsingContext.goBack();
+ await BrowserTestUtils.browserLoaded(browser);
+ is(sh.requestedIndex, -1, "requestedIndex should be -1");
+ }
+ );
+});
diff --git a/docshell/test/navigation/bug343515_pg1.html b/docshell/test/navigation/bug343515_pg1.html
new file mode 100644
index 0000000000..a8337c7f70
--- /dev/null
+++ b/docshell/test/navigation/bug343515_pg1.html
@@ -0,0 +1,5 @@
+<html>
+ <head><meta charset="UTF-8"/></head>
+ <body>Page 1
+ </body>
+</html>
diff --git a/docshell/test/navigation/bug343515_pg2.html b/docshell/test/navigation/bug343515_pg2.html
new file mode 100644
index 0000000000..c5f5665de5
--- /dev/null
+++ b/docshell/test/navigation/bug343515_pg2.html
@@ -0,0 +1,7 @@
+<html>
+ <head><meta charset="UTF-8"/></head>
+ <body>Page 2
+ <iframe src="data:text/html;charset=UTF8,<html><head></head><body>pg2 iframe 0</body></html>"></iframe>
+ <iframe src="data:text/html;charset=UTF8,<html><head></head><body>pg2 iframe 1</body></html>"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/bug343515_pg3.html b/docshell/test/navigation/bug343515_pg3.html
new file mode 100644
index 0000000000..fdc79fbf7a
--- /dev/null
+++ b/docshell/test/navigation/bug343515_pg3.html
@@ -0,0 +1,7 @@
+<html>
+ <head><meta charset="UTF-8"/></head>
+ <body>Page 3
+ <iframe src="bug343515_pg3_1.html"></iframe>
+ <iframe src="bug343515_pg3_2.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/bug343515_pg3_1.html b/docshell/test/navigation/bug343515_pg3_1.html
new file mode 100644
index 0000000000..254164c9f0
--- /dev/null
+++ b/docshell/test/navigation/bug343515_pg3_1.html
@@ -0,0 +1,6 @@
+<html>
+ <head><meta charset="UTF-8"/></head>
+ <body>pg3 - iframe 0
+ <iframe src="bug343515_pg3_1_1.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/bug343515_pg3_1_1.html b/docshell/test/navigation/bug343515_pg3_1_1.html
new file mode 100644
index 0000000000..be05b74888
--- /dev/null
+++ b/docshell/test/navigation/bug343515_pg3_1_1.html
@@ -0,0 +1 @@
+<html><head><meta charset="UTF-8"/></head><body>How far does the rabbit hole go?</body></html>
diff --git a/docshell/test/navigation/bug343515_pg3_2.html b/docshell/test/navigation/bug343515_pg3_2.html
new file mode 100644
index 0000000000..7655eb526d
--- /dev/null
+++ b/docshell/test/navigation/bug343515_pg3_2.html
@@ -0,0 +1 @@
+<html><head><meta charset="UTF-8"/></head><body>pg3 iframe 1</body></html>
diff --git a/docshell/test/navigation/cache_control_max_age_3600.sjs b/docshell/test/navigation/cache_control_max_age_3600.sjs
new file mode 100644
index 0000000000..49b6439c9f
--- /dev/null
+++ b/docshell/test/navigation/cache_control_max_age_3600.sjs
@@ -0,0 +1,20 @@
+function handleRequest(request, response) {
+ let query = request.queryString;
+ let action =
+ query == "initial"
+ ? "cache_control_max_age_3600.sjs?second"
+ : "goback.html";
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "max-age=3600");
+ response.write(
+ "<html><head><script>window.blockBFCache = new RTCPeerConnection();</script></head>" +
+ '<body onload=\'opener.postMessage("loaded", "*")\'>' +
+ "<div id='content'>" +
+ new Date().getTime() +
+ "</div>" +
+ "<form action='" +
+ action +
+ "' method='POST'></form>" +
+ "</body></html>"
+ );
+}
diff --git a/docshell/test/navigation/file_beforeunload_and_bfcache.html b/docshell/test/navigation/file_beforeunload_and_bfcache.html
new file mode 100644
index 0000000000..5c5aea2918
--- /dev/null
+++ b/docshell/test/navigation/file_beforeunload_and_bfcache.html
@@ -0,0 +1,31 @@
+<html>
+ <head>
+ <script>
+ onpageshow = function(pageShowEvent) {
+ var bc = new BroadcastChannel("beforeunload_and_bfcache");
+ bc.onmessage = function(event) {
+ if (event.data == "nextpage") {
+ bc.close();
+ location.href += "?nextpage";
+ } else if (event.data == "back") {
+ bc.close();
+ history.back();
+ } else if (event.data == "forward") {
+ onbeforeunload = function() {
+ bc.postMessage("beforeunload");
+ bc.close();
+ }
+ history.forward();
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ };
+ bc.postMessage({type: pageShowEvent.type, persisted: pageShowEvent.persisted});
+ };
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_blockBFCache.html b/docshell/test/navigation/file_blockBFCache.html
new file mode 100644
index 0000000000..dc743cdc67
--- /dev/null
+++ b/docshell/test/navigation/file_blockBFCache.html
@@ -0,0 +1,33 @@
+<script>
+let keepAlive;
+window.onpageshow = (pageShow) => {
+ let bc = new BroadcastChannel("bfcache_blocking");
+
+ bc.onmessage = async function(m) {
+ switch(m.data.message) {
+ case "load":
+ bc.close();
+ location.href = m.data.url;
+ break;
+ case "runScript":
+ let test = new Function(`return ${m.data.fun};`)();
+ keepAlive = await test.call(window);
+ bc.postMessage({ type: "runScriptDone" });
+ break;
+ case "back":
+ bc.close();
+ history.back();
+ break;
+ case "forward":
+ bc.close();
+ history.forward();
+ break;
+ case "close":
+ window.close();
+ break;
+ }
+ };
+
+ bc.postMessage({ type: pageShow.type, persisted: pageShow.persisted })
+};
+</script>
diff --git a/docshell/test/navigation/file_bug1300461.html b/docshell/test/navigation/file_bug1300461.html
new file mode 100644
index 0000000000..d7abe8be90
--- /dev/null
+++ b/docshell/test/navigation/file_bug1300461.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>Bug 1300461</title>
+ </head>
+ <body onload="test();">
+ <script>
+ /**
+ * Bug 1300461 identifies that if a history entry was not bfcached, and
+ * a http redirection happens when navigating to that entry, the history
+ * index would mess up.
+ *
+ * The test case emulates the circumstance by the following steps
+ * 1) Navigate to file_bug1300461_back.html which is not bf-cachable.
+ * 2) In file_bug1300461_back.html, replace its own history state to
+ * file_bug1300461_redirect.html.
+ * 3) Back, and then forward. Since the document is not in bfcache, it
+ * tries to load file_bug1300461_redirect.html directly.
+ * 4) file_bug1300461_redirect.html redirects UA to
+ * file_bug1300461_back.html through HTTP 301 header.
+ *
+ * We verify the history index, canGoBack, canGoForward, etc. keep correct
+ * in this process.
+ */
+ let Ci = SpecialPowers.Ci;
+ let webNav = SpecialPowers.wrap(window)
+ .docShell
+ .QueryInterface(Ci.nsIWebNavigation);
+ let shistory = webNav.sessionHistory;
+ let testSteps = [
+ function() {
+ opener.is(shistory.count, 1, "check history length");
+ opener.is(shistory.index, 0, "check history index");
+ opener.ok(!webNav.canGoForward, "check canGoForward");
+ setTimeout(() => window.location = "file_bug1300461_back.html", 0);
+ },
+ function() {
+ opener.is(shistory.count, 2, "check history length");
+ opener.is(shistory.index, 0, "check history index");
+ opener.ok(webNav.canGoForward, "check canGoForward");
+ window.history.forward();
+ },
+ function() {
+ opener.is(shistory.count, 2, "check history length");
+ opener.is(shistory.index, 0, "check history index");
+ opener.ok(webNav.canGoForward, "check canGoForward");
+ opener.info("file_bug1300461.html tests finished");
+ opener.finishTest();
+ },
+ ];
+
+ function test() {
+ if (opener) {
+ opener.info("file_bug1300461.html test " + opener.testCount);
+ testSteps[opener.testCount++]();
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1300461_back.html b/docshell/test/navigation/file_bug1300461_back.html
new file mode 100644
index 0000000000..3ec431c7c1
--- /dev/null
+++ b/docshell/test/navigation/file_bug1300461_back.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>Bug 1300461</title>
+ </head>
+ <!-- The empty unload handler is to prevent bfcache. -->
+ <body onload="test();" onunload="">
+ <script>
+ let Ci = SpecialPowers.Ci;
+ let webNav = SpecialPowers.wrap(window)
+ .docShell
+ .QueryInterface(Ci.nsIWebNavigation);
+ let shistory = webNav.sessionHistory;
+ async function test() {
+ if (opener) {
+ opener.info("file_bug1300461_back.html");
+ opener.is(shistory.count, 2, "check history length");
+ opener.is(shistory.index, 1, "check history index");
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ opener.is(shistory.legacySHistory.requestedIndex, -1, "check requestedIndex");
+ } else {
+ let index = await opener.getSHRequestedIndex(SpecialPowers.wrap(window).browsingContext.id);
+ opener.is(index, -1, "check requestedIndex");
+ }
+
+ opener.ok(webNav.canGoBack, "check canGoBack");
+ if (opener.testCount == 1) {
+ opener.info("replaceState to redirect.html");
+ window.history.replaceState({}, "", "file_bug1300461_redirect.html");
+ }
+ window.history.back();
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1300461_redirect.html b/docshell/test/navigation/file_bug1300461_redirect.html
new file mode 100644
index 0000000000..979530c5cf
--- /dev/null
+++ b/docshell/test/navigation/file_bug1300461_redirect.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>Bug 1300461</title>
+ </head>
+ <body>
+ Redirect to file_bug1300461_back.html.
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1300461_redirect.html^headers^ b/docshell/test/navigation/file_bug1300461_redirect.html^headers^
new file mode 100644
index 0000000000..241b891826
--- /dev/null
+++ b/docshell/test/navigation/file_bug1300461_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: file_bug1300461_back.html
diff --git a/docshell/test/navigation/file_bug1326251.html b/docshell/test/navigation/file_bug1326251.html
new file mode 100644
index 0000000000..57a81f46f1
--- /dev/null
+++ b/docshell/test/navigation/file_bug1326251.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Bug 1326251</title>
+ <script>
+ var bc = new BroadcastChannel("file_bug1326251");
+ bc.onmessage = function(event) {
+ if ("nextTest" in event.data) {
+ testSteps[event.data.nextTest]();
+ }
+ }
+
+ function is(val1, val2, msg) {
+ bc.postMessage({type: "is", value1: val1, value2: val2, message: msg});
+ }
+
+ function ok(val, msg) {
+ bc.postMessage({type: "ok", value: val, message: msg});
+ }
+
+ const BASE_URL = "http://mochi.test:8888/tests/docshell/test/navigation/";
+ let testSteps = [
+ async function() {
+ // Test 1: Create dynamic iframe with bfcache enabled.
+ // Navigate static / dynamic iframes, then navigate top level window
+ // and navigate back. Both iframes should still exist with history
+ // entries preserved.
+ window.onunload = null; // enable bfcache
+ await createDynamicFrame(document);
+ await loadUriInFrame(document.getElementById("staticFrame"), "frame1.html");
+ await loadUriInFrame(document.getElementById("dynamicFrame"), "frame1.html");
+ await loadUriInFrame(document.getElementById("staticFrame"), "frame2.html");
+ await loadUriInFrame(document.getElementById("dynamicFrame"), "frame2.html");
+ is(history.length, 5, "history.length");
+ window.location = "goback.html";
+ },
+ async function() {
+ let webNav = SpecialPowers.wrap(window)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsIWebNavigation);
+ let shistory = webNav.sessionHistory;
+ is(webNav.canGoForward, true, "canGoForward");
+ is(shistory.index, 4, "shistory.index");
+ is(history.length, 6, "history.length");
+ is(document.getElementById("staticFrame").contentWindow.location.href, BASE_URL + "frame2.html", "staticFrame location");
+ is(document.getElementById("dynamicFrame").contentWindow.location.href, BASE_URL + "frame2.html", "dynamicFrame location");
+
+ // Test 2: Load another page in dynamic iframe, canGoForward should be
+ // false.
+ await loadUriInFrame(document.getElementById("dynamicFrame"), "frame3.html");
+ is(webNav.canGoForward, false, "canGoForward");
+ is(shistory.index, 5, "shistory.index");
+ is(history.length, 6, "history.length");
+
+ // Test 3: Navigate to antoher page with bfcache disabled, all dynamic
+ // iframe entries should be removed.
+ window.onunload = function() {}; // disable bfcache
+ window.location = "goback.html";
+ },
+ async function() {
+ let windowWrap = SpecialPowers.wrap(window);
+ let docShell = windowWrap.docShell;
+ let shistory = docShell.QueryInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .sessionHistory;
+ // Now staticFrame has frame0 -> frame1 -> frame2.
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ // *EntryIndex attributes aren't meaningful when the session history
+ // lives in the parent process.
+ is(docShell.previousEntryIndex, 3, "docShell.previousEntryIndex");
+ is(docShell.loadedEntryIndex, 2, "docShell.loadedEntryIndex");
+ }
+ is(shistory.index, 2, "shistory.index");
+ is(history.length, 4, "history.length");
+ is(document.getElementById("staticFrame").contentWindow.location.href, BASE_URL + "frame2.html", "staticFrame location");
+ ok(!document.getElementById("dynamicFrame"), "dynamicFrame should not exist");
+
+ // Test 4: Load a nested frame in the static frame, navigate the inner
+ // static frame, add a inner dynamic frame and navigate the dynamic
+ // frame. Then navigate the outer static frame and go back. The inner
+ // iframe should show the last entry of inner static frame.
+ let staticFrame = document.getElementById("staticFrame");
+ staticFrame.width = "320px";
+ staticFrame.height = "360px";
+ await loadUriInFrame(staticFrame, "iframe_static.html");
+ let innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame");
+ await loadUriInFrame(innerStaticFrame, "frame1.html");
+ let innerDynamicFrame = await createDynamicFrame(staticFrame.contentDocument, "frame2.html");
+ await loadUriInFrame(innerDynamicFrame, "frame3.html");
+ // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static
+ // innerStaticFrame: frame0 -> frame1
+ // innerDynamicFrame: frame2 -> frame3
+ is(shistory.index, 5, "shistory.index");
+ is(history.length, 6, "history.length");
+
+ // Wait for 2 load events - navigation and goback.
+ let onloadPromise = awaitOnload(staticFrame, 2);
+ await loadUriInFrame(staticFrame, "goback.html");
+ await onloadPromise;
+ // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static -> goback
+ // innerStaticFrame: frame0 -> frame1
+ is(shistory.index, 4, "shistory.index");
+ is(history.length, 6, "history.length");
+ innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame");
+ is(innerStaticFrame.contentDocument.location.href, BASE_URL + "frame1.html", "innerStaticFrame location");
+ ok(!staticFrame.contentDocument.getElementById("dynamicFrame"), "innerDynamicFrame should not exist");
+
+ // Test 5: Insert and navigate inner dynamic frame again with bfcache
+ // enabled, and navigate top level window to a special page which will
+ // evict bfcache then goback. Verify that dynamic entries are correctly
+ // removed in this case.
+ window.onunload = null; // enable bfcache
+ staticFrame.width = "320px";
+ staticFrame.height = "360px";
+ innerDynamicFrame = await createDynamicFrame(staticFrame.contentDocument, "frame2.html");
+ await loadUriInFrame(innerDynamicFrame, "frame3.html");
+ // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static
+ // innerStaticFrame: frame0 -> frame1
+ // innerDynamicFrame: frame2 -> frame3
+ is(shistory.index, 5, "shistory.index");
+ is(history.length, 6, "history.length");
+ window.location = "file_bug1326251_evict_cache.html";
+ },
+ async function() {
+ let windowWrap = SpecialPowers.wrap(window);
+ let docShell = windowWrap.docShell;
+ let shistory = docShell.QueryInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .sessionHistory;
+ // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static
+ // innerStaticFrame: frame0 -> frame1
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ // *EntryIndex attributes aren't meaningful when the session history
+ // lives in the parent process.
+ is(docShell.previousEntryIndex, 5, "docShell.previousEntryIndex");
+ is(docShell.loadedEntryIndex, 4, "docShell.loadedEntryIndex");
+ }
+ is(shistory.index, 4, "shistory.index");
+ is(history.length, 6, "history.length");
+ let staticFrame = document.getElementById("staticFrame");
+ let innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame");
+ is(innerStaticFrame.contentDocument.location.href, BASE_URL + "frame1.html", "innerStaticFrame location");
+ ok(!staticFrame.contentDocument.getElementById("dynamicFrame"), "innerDynamicFrame should not exist");
+
+ // Test 6: Insert and navigate inner dynamic frame and then reload outer
+ // frame. Verify that inner dynamic frame entries are all removed.
+ staticFrame.width = "320px";
+ staticFrame.height = "360px";
+ let innerDynamicFrame = await createDynamicFrame(staticFrame.contentDocument, "frame2.html");
+ await loadUriInFrame(innerDynamicFrame, "frame3.html");
+ // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static
+ // innerStaticFrame: frame0 -> frame1
+ // innerDynamicFrame: frame2 -> frame3
+ is(shistory.index, 5, "shistory.index");
+ is(history.length, 6, "history.length");
+ let staticFrameLoadPromise = new Promise(resolve => {
+ staticFrame.onload = resolve;
+ });
+ staticFrame.contentWindow.location.reload();
+ await staticFrameLoadPromise;
+ // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static
+ // innerStaticFrame: frame0 -> frame1
+ is(shistory.index, 4, "shistory.index");
+ is(history.length, 5, "history.length");
+ innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame");
+ is(innerStaticFrame.contentDocument.location.href, BASE_URL + "frame1.html", "innerStaticFrame location");
+ ok(!staticFrame.contentDocument.getElementById("dynamicFrame"), "innerDynamicFrame should not exist");
+ bc.postMessage("finishTest");
+ bc.close();
+ window.close();
+ },
+ ];
+
+ function awaitOnload(frame, occurances = 1) {
+ return new Promise(function(resolve, reject) {
+ let count = 0;
+ frame.addEventListener("load", function listener(e) {
+ if (++count == occurances) {
+ frame.removeEventListener("load", listener);
+ setTimeout(resolve, 0);
+ }
+ });
+ });
+ }
+
+ async function createDynamicFrame(targetDocument, frameSrc = "frame0.html") {
+ let dynamicFrame = targetDocument.createElement("iframe");
+ let onloadPromise = awaitOnload(dynamicFrame);
+ dynamicFrame.id = "dynamicFrame";
+ dynamicFrame.src = frameSrc;
+ let container = targetDocument.getElementById("frameContainer");
+ container.appendChild(dynamicFrame);
+ await onloadPromise;
+ return dynamicFrame;
+ }
+
+ async function loadUriInFrame(frame, uri) {
+ let onloadPromise = awaitOnload(frame);
+ frame.src = uri;
+ return onloadPromise;
+ }
+
+ function test() {
+ bc.postMessage("requestNextTest");
+ }
+ </script>
+ </head>
+ <body onpageshow="test();">
+ <div id="frameContainer">
+ <iframe id="staticFrame" src="frame0.html"></iframe>
+ </div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1326251_evict_cache.html b/docshell/test/navigation/file_bug1326251_evict_cache.html
new file mode 100644
index 0000000000..d81f948d89
--- /dev/null
+++ b/docshell/test/navigation/file_bug1326251_evict_cache.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Bug 1326251</title>
+ <script>
+ // Evict bfcache and then go back.
+ async function evictCache() {
+ await SpecialPowers.evictAllDocumentViewers();
+
+ history.back();
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(evictCache, 0);">
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1364364-1.html b/docshell/test/navigation/file_bug1364364-1.html
new file mode 100644
index 0000000000..d4ecc42ad4
--- /dev/null
+++ b/docshell/test/navigation/file_bug1364364-1.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>title</title>
+ </head>
+ <body onload="loadFramesAndNavigate();">
+ <p id="content"></p>
+ <div id="frameContainer">
+ </div>
+ <script type="application/javascript">
+ function waitForLoad(frame) {
+ return new Promise(r => frame.onload = () => setTimeout(r, 0));
+ }
+
+ async function loadFramesAndNavigate() {
+ let dynamicFrame = document.createElement("iframe");
+ dynamicFrame.src = "data:text/html,iframe1";
+ document.querySelector("#frameContainer").appendChild(dynamicFrame);
+ await waitForLoad(dynamicFrame);
+ dynamicFrame.src = "data:text/html,iframe2";
+ await waitForLoad(dynamicFrame);
+ dynamicFrame.src = "data:text/html,iframe3";
+ await waitForLoad(dynamicFrame);
+ dynamicFrame.src = "data:text/html,iframe4";
+ await waitForLoad(dynamicFrame);
+ dynamicFrame.src = "data:text/html,iframe5";
+ await waitForLoad(dynamicFrame);
+ location.href = "file_bug1364364-2.html";
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1364364-2.html b/docshell/test/navigation/file_bug1364364-2.html
new file mode 100644
index 0000000000..6e52ecaaa9
--- /dev/null
+++ b/docshell/test/navigation/file_bug1364364-2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>title</title>
+ </head>
+ <body onload="notifyOpener();">
+ <script type="application/javascript">
+ function notifyOpener() {
+ opener.postMessage("navigation-done", "*");
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1375833-frame1.html b/docshell/test/navigation/file_bug1375833-frame1.html
new file mode 100644
index 0000000000..ea38326479
--- /dev/null
+++ b/docshell/test/navigation/file_bug1375833-frame1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>iframe test page 1</title>
+ </head>
+ <body>iframe test page 1</body>
+</html>
diff --git a/docshell/test/navigation/file_bug1375833-frame2.html b/docshell/test/navigation/file_bug1375833-frame2.html
new file mode 100644
index 0000000000..6e76ab7e47
--- /dev/null
+++ b/docshell/test/navigation/file_bug1375833-frame2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>iframe test page 2</title>
+ </head>
+ <body>iframe test page 2</body>
+</html>
diff --git a/docshell/test/navigation/file_bug1375833.html b/docshell/test/navigation/file_bug1375833.html
new file mode 100644
index 0000000000..373a7fe08e
--- /dev/null
+++ b/docshell/test/navigation/file_bug1375833.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Test for bug 1375833</title>
+ </head>
+ <body onload="test();">
+ <iframe id="testFrame" src="file_bug1375833-frame1.html"></iframe>
+ <script type="application/javascript">
+ function test() {
+ let iframe = document.querySelector("#testFrame");
+ setTimeout(function() { iframe.src = "file_bug1375833-frame1.html"; }, 0);
+ iframe.onload = function(e) {
+ setTimeout(function() { iframe.src = "file_bug1375833-frame2.html"; }, 0);
+ iframe.onload = function() {
+ opener.postMessage(iframe.contentWindow.location.href, "*");
+ };
+ };
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1379762-1.html b/docshell/test/navigation/file_bug1379762-1.html
new file mode 100644
index 0000000000..c8cd666667
--- /dev/null
+++ b/docshell/test/navigation/file_bug1379762-1.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Bug 1379762</title>
+ </head>
+ <img srcset> <!-- This tries to add load blockers during bfcache restoration -->
+ <script>
+ onunload = null; // enable bfcache
+ var bc = new BroadcastChannel('bug1379762');
+ bc.postMessage("init");
+ onpageshow = function() {
+ bc.onmessage = (messageEvent) => {
+ let message = messageEvent.data;
+ if (message == "forward_back") {
+ // Navigate forward and then back.
+ // eslint-disable-next-line no-global-assign
+ setTimeout(function() { location = "goback.html"; }, 0);
+ } else if (message == "finish_test") {
+ // Do this async so our load event gets a chance to fire if it plans to
+ // do it.
+ setTimeout(function() {
+ bc.postMessage("finished");
+ bc.close();
+ window.close();
+ });
+ }
+ }
+ bc.postMessage("increment_testCount");
+ };
+ onload = function() {
+ bc.postMessage("increment_loadCount");
+ };
+ </script>
+</html>
diff --git a/docshell/test/navigation/file_bug1536471.html b/docshell/test/navigation/file_bug1536471.html
new file mode 100644
index 0000000000..53012257ee
--- /dev/null
+++ b/docshell/test/navigation/file_bug1536471.html
@@ -0,0 +1,8 @@
+<html>
+ <body onload="opener.bodyOnLoad()">
+ Nested Frame
+ <div id="frameContainer">
+ <iframe id="staticFrame" src="frame0.html"></iframe>
+ </div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1583110.html b/docshell/test/navigation/file_bug1583110.html
new file mode 100644
index 0000000000..5b08f54d21
--- /dev/null
+++ b/docshell/test/navigation/file_bug1583110.html
@@ -0,0 +1,26 @@
+<html>
+ <head>
+ <script>
+ var bc;
+ window.onpageshow = function(pageshow) {
+ bc = new BroadcastChannel("bug1583110");
+ bc.onmessage = function(event) {
+ bc.close();
+ if (event.data == "loadnext") {
+ location.search = "?nextpage";
+ } else if (event.data == "back") {
+ history.back();
+ }
+ }
+ bc.postMessage({type: "pageshow", persisted: pageshow.persisted });
+ if (pageshow.persisted) {
+ document.body.appendChild(document.createElement("iframe"));
+ bc.close();
+ window.close();
+ }
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1609475.html b/docshell/test/navigation/file_bug1609475.html
new file mode 100644
index 0000000000..7699d46b08
--- /dev/null
+++ b/docshell/test/navigation/file_bug1609475.html
@@ -0,0 +1,51 @@
+<html>
+ <head>
+ <script>
+
+ var loadCount = 0;
+ function loadListener(event) {
+ ++loadCount;
+ if (loadCount == 2) {
+ // Use a timer to ensure we don't get extra load events.
+ setTimeout(function() {
+ var doc1URI = document.getElementById("i1").contentDocument.documentURI;
+ opener.ok(doc1URI.includes("frame1.html"),
+ "Should have loaded the initial page to the first iframe. Got " + doc1URI);
+ var doc2URI = document.getElementById("i2").contentDocument.documentURI;
+ opener.ok(doc2URI.includes("frame1.html"),
+ "Should have loaded the initial page to the second iframe. Got " + doc2URI);
+ opener.finishTest();
+ }, 1000);
+ } else if (loadCount > 2) {
+ opener.ok(false, "Too many load events");
+ }
+ // if we don't get enough load events, the test will time out.
+ }
+
+ function setupIframe(id) {
+ var ifr = document.getElementById(id);
+ return new Promise(function(resolve) {
+ ifr.onload = function() {
+ // Replace load listener to catch page loads from the session history.
+ ifr.onload = loadListener;
+ // Need to use setTimeout, because triggering loads inside
+ // load event listener has special behavior since at the moment
+ // the docshell keeps track of whether it is executing a load handler or not.
+ setTimeout(resolve);
+ }
+ ifr.contentWindow.location.href = "frame2.html";
+ });
+ }
+
+ async function test() {
+ await setupIframe("i1");
+ await setupIframe("i2");
+ history.go(-2);
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(test)">
+ <iframe id="i1" src="frame1.html"></iframe>
+ <iframe id="i2" src="frame1.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/file_bug1706090.html b/docshell/test/navigation/file_bug1706090.html
new file mode 100644
index 0000000000..9c5bc025d3
--- /dev/null
+++ b/docshell/test/navigation/file_bug1706090.html
@@ -0,0 +1,40 @@
+<html>
+ <head>
+ <script>
+ onpageshow = function(pageShowEvent) {
+ if (location.hostname == "example.com" ||
+ location.hostname == "test1.mochi.test") {
+ // BroadcastChannel doesn't work across domains, so just go to the
+ // previous page explicitly.
+ history.back();
+ return;
+ }
+
+ var bc = new BroadcastChannel("bug1706090");
+ bc.onmessage = function(event) {
+ if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ } else if (event.data == "sameOrigin") {
+ bc.close();
+ location.href = location.href + "?foo"
+ } else if (event.data == "back") {
+ history.back();
+ } else if (event.data == "crossOrigin") {
+ bc.close();
+ location.href = "https://example.com:443" + location.pathname;
+ } else if (event.data == "sameSite") {
+ bc.close();
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ location.href = "http://test1.mochi.test:8888" + location.pathname;
+ }
+ }
+
+ bc.postMessage({type: pageShowEvent.type, persisted: pageShowEvent.persisted});
+ }
+ </script>
+ </head>
+ <body onunload="/* dummy listener*/">
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug1745638.html b/docshell/test/navigation/file_bug1745638.html
new file mode 100644
index 0000000000..b98c8de91f
--- /dev/null
+++ b/docshell/test/navigation/file_bug1745638.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script>
+ function go() {
+ var doc = document.getElementById("testFrame").contentWindow.document;
+ doc.open();
+ doc.close();
+ doc.body.innerText = "passed";
+ }
+</script>
+<body onload="setTimeout(opener.pageLoaded);">
+The iframe below should not be blank after a reload.<br>
+<iframe src="about:blank" id="testFrame"></iframe>
+<script>
+ go();
+</script>
diff --git a/docshell/test/navigation/file_bug1750973.html b/docshell/test/navigation/file_bug1750973.html
new file mode 100644
index 0000000000..28b2f995ae
--- /dev/null
+++ b/docshell/test/navigation/file_bug1750973.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script>
+ function test() {
+ history.scrollRestoration = "manual";
+ document.getElementById("initialfocus").focus();
+ history.pushState('data', '', '');
+ history.back();
+ }
+
+ window.onpopstate = function() {
+ window.onscroll = function() {
+ window.onscroll = null;
+ document.documentElement.style.display = "none";
+ // focus() triggers recreation of the nsIFrames without a reflow.
+ document.getElementById("focustarget").focus();
+ document.documentElement.style.display = "";
+ // Flush the layout.
+ document.documentElement.getBoundingClientRect();
+ opener.isnot(window.scrollY, 0, "The page should have been scrolled down(1)");
+ requestAnimationFrame(
+ function() {
+ // Extra timeout to ensure we're called after rAF has triggered a
+ // reflow.
+ setTimeout(function() {
+ opener.isnot(window.scrollY, 0, "The page should have been scrolled down(2)");
+ window.close();
+ opener.SimpleTest.finish();
+ });
+ });
+ }
+ window.scrollTo(0, 1000);
+ }
+ </script>
+</head>
+<body onload="setTimeout(test)">
+<div style="position: fixed;">
+ <input type="button" value="" id="initialfocus">
+ <input type="button" value="" id="focustarget">
+</div>
+<div style="border: 1px solid black; width: 100px; height: 9000px;"></div>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_bug1758664.html b/docshell/test/navigation/file_bug1758664.html
new file mode 100644
index 0000000000..07798dfddd
--- /dev/null
+++ b/docshell/test/navigation/file_bug1758664.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+var onIframeOnload = function() {
+ var iframe = window.document.getElementById('applicationIframe');
+ opener.is(iframe.contentWindow.location.search, "?iframe", "Should have loaded the iframe");
+ window.close();
+ opener.SimpleTest.finish();
+}
+
+var onPageOnload = function() {
+ if (location.search == "?iframe") {
+ return;
+ }
+ if(!window.name) {
+ window.name = 'file_bug1758664.html';
+ window.location.reload();
+ return;
+ }
+ var iframe = window.document.getElementById('applicationIframe');
+ iframe.addEventListener('load', onIframeOnload);
+ iframe.src = "file_bug1758664.html?iframe";
+}
+window.document.addEventListener("DOMContentLoaded", onPageOnload);
+
+</script>
+</head>
+<body>
+ <iframe id="applicationIframe"></iframe>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_bug386782_contenteditable.html b/docshell/test/navigation/file_bug386782_contenteditable.html
new file mode 100644
index 0000000000..4515d015d9
--- /dev/null
+++ b/docshell/test/navigation/file_bug386782_contenteditable.html
@@ -0,0 +1 @@
+<html><head><meta charset="utf-8"><script>window.addEventListener("pageshow", function(event) { window.opener.postMessage({persisted: event.persisted}, "*"); });</script></head><body contentEditable="true"><p>contentEditable</p></body></html> \ No newline at end of file
diff --git a/docshell/test/navigation/file_bug386782_designmode.html b/docshell/test/navigation/file_bug386782_designmode.html
new file mode 100644
index 0000000000..faa063cbae
--- /dev/null
+++ b/docshell/test/navigation/file_bug386782_designmode.html
@@ -0,0 +1 @@
+<html><head><meta charset="utf-8"><script>window.addEventListener("pageshow", function(event) { window.opener.postMessage({persisted: event.persisted}, "*"); });</script></head><body><p>designModeDocument</p></body></html> \ No newline at end of file
diff --git a/docshell/test/navigation/file_bug462076_1.html b/docshell/test/navigation/file_bug462076_1.html
new file mode 100644
index 0000000000..5050e79fdc
--- /dev/null
+++ b/docshell/test/navigation/file_bug462076_1.html
@@ -0,0 +1,55 @@
+<html>
+ <head>
+ <title>Bug 462076</title>
+ <script>
+ var srcs = [ "frame0.html",
+ "frame1.html",
+ "frame2.html",
+ "frame3.html" ];
+
+ var checkCount = 0;
+
+ function makeFrame(index) {
+ var ifr = document.createElement("iframe");
+ ifr.src = srcs[index];
+ ifr.onload = checkFrame;
+ document.getElementById("container" + index).appendChild(ifr);
+ }
+
+ function runTest() {
+ var randomNumber = Math.floor(Math.random() * 4);
+ for (var i = randomNumber; i < 4; ++i) {
+ makeFrame(i);
+ }
+ for (var k = 0; k < randomNumber; ++k) {
+ makeFrame(k);
+ }
+ }
+
+ function checkFrame(evt) {
+ var ifr = evt.target;
+ opener.ok(String(ifr.contentWindow.location).includes(ifr.src),
+ "Wrong document loaded (" + ifr.src + ", " +
+ ifr.contentWindow.location + ")!");
+
+ if (++checkCount == 4) {
+ if (++opener.testCount == 10) {
+ opener.nextTest();
+ window.close();
+ } else {
+ window.location.reload();
+ }
+ }
+ }
+ </script>
+ </head>
+ <body>
+ <div id="container0"></div>
+ <div id="container1"></div>
+ <div id="container2"></div>
+ <div id="container3"></div>
+ <script>
+ runTest();
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug462076_2.html b/docshell/test/navigation/file_bug462076_2.html
new file mode 100644
index 0000000000..63cf3de3f9
--- /dev/null
+++ b/docshell/test/navigation/file_bug462076_2.html
@@ -0,0 +1,52 @@
+<html>
+ <head>
+ <title>Bug 462076</title>
+ <script>
+ var srcs = [ "frame0.html",
+ "frame1.html",
+ "frame2.html",
+ "frame3.html" ];
+
+ var checkCount = 0;
+
+ function makeFrame(index) {
+ var ifr = document.createElement("iframe");
+ ifr.src = srcs[index];
+ ifr.onload = checkFrame;
+ document.getElementById("container" + index).appendChild(ifr);
+ }
+
+ function runTest() {
+ var randomNumber = Math.floor(Math.random() * 4);
+ for (var i = randomNumber; i < 4; ++i) {
+ makeFrame(i);
+ }
+ for (var k = 0; k < randomNumber; ++k) {
+ makeFrame(k);
+ }
+ }
+
+ function checkFrame(evt) {
+ var ifr = evt.target;
+ opener.ok(String(ifr.contentWindow.location).includes(ifr.src),
+ "Wrong document loaded (" + ifr.src + ", " +
+ ifr.contentWindow.location + ")!");
+
+ if (++checkCount == 4) {
+ if (++opener.testCount == 10) {
+ opener.nextTest();
+ window.close();
+ } else {
+ window.location.reload();
+ }
+ }
+ }
+ </script>
+ </head>
+ <body onload="runTest();">
+ <div id="container0"></div>
+ <div id="container1"></div>
+ <div id="container2"></div>
+ <div id="container3"></div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug462076_3.html b/docshell/test/navigation/file_bug462076_3.html
new file mode 100644
index 0000000000..5c779d2f49
--- /dev/null
+++ b/docshell/test/navigation/file_bug462076_3.html
@@ -0,0 +1,52 @@
+<html>
+ <head>
+ <title>Bug 462076</title>
+ <script>
+ var srcs = [ "frame0.html",
+ "frame1.html",
+ "frame2.html",
+ "frame3.html" ];
+
+ var checkCount = 0;
+
+ function makeFrame(index) {
+ var ifr = document.createElement("iframe");
+ ifr.src = srcs[index];
+ ifr.onload = checkFrame;
+ document.getElementById("container" + index).appendChild(ifr);
+ }
+
+ function runTest() {
+ var randomNumber = Math.floor(Math.random() * 4);
+ for (var i = randomNumber; i < 4; ++i) {
+ makeFrame(i);
+ }
+ for (var k = 0; k < randomNumber; ++k) {
+ makeFrame(k);
+ }
+ }
+
+ function checkFrame(evt) {
+ var ifr = evt.target;
+ opener.ok(String(ifr.contentWindow.location).includes(ifr.src),
+ "Wrong document loaded (" + ifr.src + ", " +
+ ifr.contentWindow.location + ")!");
+
+ if (++checkCount == 4) {
+ if (++opener.testCount == 10) {
+ opener.nextTest();
+ window.close();
+ } else {
+ window.location.reload();
+ }
+ }
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(runTest, 0);">
+ <div id="container0"></div>
+ <div id="container1"></div>
+ <div id="container2"></div>
+ <div id="container3"></div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_bug508537_1.html b/docshell/test/navigation/file_bug508537_1.html
new file mode 100644
index 0000000000..182c085670
--- /dev/null
+++ b/docshell/test/navigation/file_bug508537_1.html
@@ -0,0 +1,33 @@
+<html>
+ <head>
+ <script>
+ function dynFrameLoad() {
+ var ifrs = document.getElementsByTagName("iframe");
+ opener.ok(String(ifrs[0].contentWindow.location).includes(ifrs[0].src),
+ "Wrong document loaded (1)\n");
+ opener.ok(String(ifrs[1].contentWindow.location).includes(ifrs[1].src),
+ "Wrong document loaded (2)\n");
+ if (opener && ++opener.testCount == 1) {
+ window.location = "goback.html";
+ } else {
+ opener.finishTest();
+ }
+ }
+
+ window.addEventListener("load",
+ function() {
+ var container = document.getElementById("t1");
+ container.addEventListener("load", dynFrameLoad, true);
+ container.appendChild(container.appendChild(document.getElementById("i1")));
+ });
+ </script>
+ </head>
+ <body>
+ <h5>Container:</h5>
+ <div id="t1"></div>
+ <h5>Original frames:</h5>
+ <iframe id="i1" src="frame0.html"></iframe>
+ <iframe src="frame1.html"></iframe>
+ </body>
+</html>
+
diff --git a/docshell/test/navigation/file_bug534178.html b/docshell/test/navigation/file_bug534178.html
new file mode 100644
index 0000000000..4d77dd824b
--- /dev/null
+++ b/docshell/test/navigation/file_bug534178.html
@@ -0,0 +1,30 @@
+<html>
+ <head>
+ <script>
+
+ function testDone() {
+ document.body.firstChild.remove();
+ var isOK = false;
+ try {
+ isOK = history.previous != location;
+ } catch (ex) {
+ // history.previous should throw if this is the first page in shistory.
+ isOK = true;
+ }
+ document.body.textContent = isOK ? "PASSED" : "FAILED";
+ opener.ok(isOK, "Duplicate session history entries should have been removed!");
+ opener.finishTest();
+ }
+ function ifrload() {
+ setTimeout(testDone, 0);
+ }
+ function test() {
+ var ifr = document.getElementsByTagName("iframe")[0];
+ ifr.onload = ifrload;
+ ifr.src = "data:text/html,doc2";
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(test, 0)"><iframe src="data:text/html,doc1"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_contentpolicy_block_window.html b/docshell/test/navigation/file_contentpolicy_block_window.html
new file mode 100644
index 0000000000..c51e574e5e
--- /dev/null
+++ b/docshell/test/navigation/file_contentpolicy_block_window.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This window should never be openend!
+</body>
+</html>
diff --git a/docshell/test/navigation/file_docshell_gotoindex.html b/docshell/test/navigation/file_docshell_gotoindex.html
new file mode 100644
index 0000000000..f3e8919822
--- /dev/null
+++ b/docshell/test/navigation/file_docshell_gotoindex.html
@@ -0,0 +1,42 @@
+<html>
+ <head>
+ <script>
+ function loaded() {
+ if (location.search == "") {
+ if (opener.loadedInitialPage) {
+ opener.ok(true, "got back to the initial page.");
+ opener.setTimeout("SimpleTest.finish();");
+ window.close();
+ return;
+ }
+ opener.loadedInitialPage = true;
+ opener.info("Loaded initial page.");
+ // Load another page (which is this same document, but different URL.)
+ location.href = location.href + "?anotherPage";
+ } else {
+ opener.info("Loaded the second page.");
+ location.hash = "1";
+ window.onhashchange = function() {
+ opener.info("hash: " + location.hash);
+ location.hash = "2";
+ window.onhashchange = function() {
+ opener.info("hash: " + location.hash);
+ var docShell = SpecialPowers.wrap(window).docShell;
+ var webNavigation =
+ SpecialPowers.do_QueryInterface(docShell, "nsIWebNavigation");
+ webNavigation.gotoIndex(history.length - 2);
+ window.onhashchange = function() {
+ opener.info("hash: " + location.hash);
+ webNavigation.gotoIndex(history.length - 4);
+ }
+ }
+ }
+ }
+ }
+ </script>
+ </head>
+ <body onpageshow="setTimeout(loaded)">
+ <a href="#1" name="1">1</a>
+ <a href="#2" name="2">2</a>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/file_document_write_1.html b/docshell/test/navigation/file_document_write_1.html
new file mode 100644
index 0000000000..be52b60231
--- /dev/null
+++ b/docshell/test/navigation/file_document_write_1.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <script>
+ function start() {
+ var length = history.length;
+ document.open();
+ document.write("<h5 id='dynamic'>document.written content</h5>");
+ document.close();
+ opener.is(history.length, length,
+ "document.open/close should not change history");
+ opener.finishTest();
+ }
+ </script>
+ </head>
+ <body onload="start();">
+ <h5>static content</h5>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_evict_from_bfcache.html b/docshell/test/navigation/file_evict_from_bfcache.html
new file mode 100644
index 0000000000..9f50533543
--- /dev/null
+++ b/docshell/test/navigation/file_evict_from_bfcache.html
@@ -0,0 +1,29 @@
+<html>
+ <head>
+ <script>
+ onpageshow = function(pageShowEvent) {
+ var bc = new BroadcastChannel("evict_from_bfcache");
+ bc.onmessage = function(event) {
+ if (event.data == "nextpage") {
+ bc.close();
+ location.href += "?nextpage";
+ } else if (event.data == "back") {
+ bc.close();
+ history.back();
+ } else if (event.data == "forward") {
+ // Note, we don't close BroadcastChannel
+ history.forward();
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ }
+
+ bc.postMessage({ type: "pageshow", persisted: pageShowEvent.persisted});
+ };
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_fragment_handling_during_load.html b/docshell/test/navigation/file_fragment_handling_during_load.html
new file mode 100644
index 0000000000..a7f468c32d
--- /dev/null
+++ b/docshell/test/navigation/file_fragment_handling_during_load.html
@@ -0,0 +1,27 @@
+<html>
+ <head>
+ <script>
+ function checkHaveLoadedNewDoc() {
+ let l = document.body.firstChild.contentWindow.location.href;
+ if (!l.endsWith("file_fragment_handling_during_load_frame2.sjs")) {
+ opener.ok(true, "Fine. We will check later.");
+ setTimeout(checkHaveLoadedNewDoc, 500);
+ return;
+ }
+ opener.ok(true, "Have loaded a new document.");
+ opener.finishTest();
+ }
+ function test() {
+ // Test that executing back() before load has started doesn't interrupt
+ // the load.
+ var ifr = document.getElementsByTagName("iframe")[0];
+ ifr.onload = checkHaveLoadedNewDoc;
+ ifr.contentWindow.location.hash = "b";
+ ifr.contentWindow.location.href = "file_fragment_handling_during_load_frame2.sjs";
+ history.back();
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(test, 0)"><iframe src="file_fragment_handling_during_load_frame1.html#a"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_fragment_handling_during_load_frame1.html b/docshell/test/navigation/file_fragment_handling_during_load_frame1.html
new file mode 100644
index 0000000000..c03ba2bda6
--- /dev/null
+++ b/docshell/test/navigation/file_fragment_handling_during_load_frame1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+foo
+</body>
+</html>
diff --git a/docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs b/docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs
new file mode 100644
index 0000000000..77abe5949e
--- /dev/null
+++ b/docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs
@@ -0,0 +1,20 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ // Wait a bit.
+ var s = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - s < 1000) {}
+
+ response.write(`<!DOCTYPE HTML>
+ <html>
+ <body>
+ bar
+ </body>
+ </html>
+ `);
+}
diff --git a/docshell/test/navigation/file_load_history_entry_page_with_one_link.html b/docshell/test/navigation/file_load_history_entry_page_with_one_link.html
new file mode 100644
index 0000000000..a4d1b62176
--- /dev/null
+++ b/docshell/test/navigation/file_load_history_entry_page_with_one_link.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body onpageshow="opener.bodyOnLoad()">
+<a id="link1" href="#1">Link 1</a>
+<a name="1">link 1</a>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_load_history_entry_page_with_two_links.html b/docshell/test/navigation/file_load_history_entry_page_with_two_links.html
new file mode 100644
index 0000000000..4be2ea6f4e
--- /dev/null
+++ b/docshell/test/navigation/file_load_history_entry_page_with_two_links.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body onpageshow="opener.bodyOnLoad()">
+<a id="link1" href="#1">Link 1</a>
+<a id="link2" href="#2">Link 2</a>
+<a name="1">link 1</a>
+<a name="2">link 2</a>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_meta_refresh.html b/docshell/test/navigation/file_meta_refresh.html
new file mode 100644
index 0000000000..274084b03b
--- /dev/null
+++ b/docshell/test/navigation/file_meta_refresh.html
@@ -0,0 +1,39 @@
+<html>
+ <head>
+ </head>
+ <body>
+ <script>
+ function addMetaRefresh() {
+ document.head.innerHTML = "<meta http-equiv='refresh' content='5; url=" +
+ location.href.replace("?initial", "?refresh") + "'>";
+ }
+
+ onpageshow = function() {
+ let bc = new BroadcastChannel("test_meta_refresh");
+ bc.onmessage = function(event) {
+ if (event.data == "loadnext") {
+ bc.close();
+ addMetaRefresh();
+ location.href = location.href.replace("?initial", "?nextpage");
+ } else if (event.data == "back") {
+ bc.close();
+ history.back();
+ } else if (event.data == "ensuremetarefresh") {
+ bc.close();
+ // This test is designed to work with and without bfcache, but
+ // if bfcache is enabled, meta refresh should be suspended/resumed.
+ if (document.head.firstChild.localName != "meta") {
+ addMetaRefresh();
+ }
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ };
+
+ bc.postMessage({ load: location.search.substr(1) });
+ }
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_navigation_type.html b/docshell/test/navigation/file_navigation_type.html
new file mode 100644
index 0000000000..bb538eefec
--- /dev/null
+++ b/docshell/test/navigation/file_navigation_type.html
@@ -0,0 +1,25 @@
+<html>
+ <head>
+ <script>
+ onpageshow = function() {
+ let bc = new BroadcastChannel("navigation_type");
+ bc.onmessage = function(event) {
+ if (event.data == "loadNewPage") {
+ bc.close();
+ location.href = location.href + "?next";
+ } else if (event.data == "back") {
+ bc.close();
+ history.back();
+ } else if (event.data == "close") {
+ window.close();
+ bc.postMessage("closed");
+ bc.close();
+ }
+ }
+ bc.postMessage({ navigationType: performance.navigation.type });
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/file_nested_frames.html b/docshell/test/navigation/file_nested_frames.html
new file mode 100644
index 0000000000..6ec286aa3e
--- /dev/null
+++ b/docshell/test/navigation/file_nested_frames.html
@@ -0,0 +1,27 @@
+<html>
+ <head>
+ <script>
+ function nestedIframeLoaded() {
+ var tf = document.getElementById("testframe");
+ var innerf = tf.contentDocument.getElementsByTagName("iframe")[0];
+ if (!innerf.contentDocument.documentURI.includes("frame0")) {
+ innerf.contentWindow.location.href = "http://mochi.test:8888/tests/docshell/test/navigation/frame0.html";
+ return;
+ }
+ innerf.onload = null;
+ innerf.src = "about:blank";
+ var d = innerf.contentDocument;
+ d.open();
+ d.write("test");
+ d.close();
+ opener.is(window.history.length, 1, "Unexpected history length");
+ opener.finishTest();
+ }
+ </script>
+ </head>
+ <body>
+ <iframe id="testframe" src="file_nested_frames_innerframe.html" onload="frameLoaded()"></iframe>
+ <script>
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_nested_frames_innerframe.html b/docshell/test/navigation/file_nested_frames_innerframe.html
new file mode 100644
index 0000000000..e25b6a4f6a
--- /dev/null
+++ b/docshell/test/navigation/file_nested_frames_innerframe.html
@@ -0,0 +1 @@
+<iframe onload='parent.nestedIframeLoaded();'></iframe>
diff --git a/docshell/test/navigation/file_nested_srcdoc.html b/docshell/test/navigation/file_nested_srcdoc.html
new file mode 100644
index 0000000000..ae6d656f27
--- /dev/null
+++ b/docshell/test/navigation/file_nested_srcdoc.html
@@ -0,0 +1,3 @@
+
+iframe loaded inside of a srcdoc
+<iframe id="static" srcdoc="Second nested srcdoc<iframe id='static' srcdoc='Third nested srcdoc'&gt;</iframe&gt;"></iframe>
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html
new file mode 100644
index 0000000000..2f9a41e1d1
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html
@@ -0,0 +1,5 @@
+<script>
+ onload = function() {
+ opener.postMessage("load");
+ }
+</script>
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^ b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html
new file mode 100644
index 0000000000..de456f8f1c
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html
@@ -0,0 +1,10 @@
+<script>
+ onload = function() {
+ opener.postMessage("load");
+ }
+
+ onbeforeunload = function() {
+ opener.postMessage("beforeunload");
+ history.pushState("data1", "", "?push1");
+ }
+</script>
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^ b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html
new file mode 100644
index 0000000000..2237e3e367
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html
@@ -0,0 +1,22 @@
+<script>
+ window.onpageshow = function(e) {
+ if (location.search == "?v2") {
+ onbeforeunload = function() {
+ history.pushState("data1", "", "?push1");
+ }
+ }
+
+ let bc = new BroadcastChannel("new_shentry_during_history_navigation");
+ bc.onmessage = function(event) {
+ bc.close();
+ if (event.data == "loadnext") {
+ location.href = "file_new_shentry_during_history_navigation_4.html";
+ } else if (event.data == "back") {
+ history.back();
+ } else if (event.data == "close") {
+ window.close();
+ }
+ }
+ bc.postMessage({page: location.href, type: e.type, persisted: e.persisted});
+ }
+</script>
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^ b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_4.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_4.html
new file mode 100644
index 0000000000..d5c3f61794
--- /dev/null
+++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_4.html
@@ -0,0 +1,16 @@
+<script>
+ window.onpageshow = function(e) {
+ let bc = new BroadcastChannel("new_shentry_during_history_navigation");
+ bc.onmessage = function(event) {
+ bc.close();
+ if (event.data == "loadnext") {
+ location.href = "file_new_shentry_during_history_navigation_3.html?v2";
+ } else if (event.data == "forward") {
+ history.forward();
+ } else if (event.data == "close") {
+ window.close();
+ }
+ }
+ bc.postMessage({page: location.href, type: e.type, persisted: e.persisted});
+ }
+</script>
diff --git a/docshell/test/navigation/file_online_offline_bfcache.html b/docshell/test/navigation/file_online_offline_bfcache.html
new file mode 100644
index 0000000000..7f8138e758
--- /dev/null
+++ b/docshell/test/navigation/file_online_offline_bfcache.html
@@ -0,0 +1,41 @@
+<html>
+ <head>
+ <script>
+ onpageshow = function(pageshowEvent) {
+ let bc = new BroadcastChannel("online_offline_bfcache");
+ bc.onmessage = function(event) {
+ if (event.data == "nextpage") {
+ bc.close();
+ location.href = location.href + "?nextpage";
+ } else if (event.data == "back") {
+ bc.close();
+ history.back();
+ } else if (event.data == "forward") {
+ bc.close();
+ history.forward();
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ };
+ bc.postMessage({ event: pageshowEvent.type, persisted: pageshowEvent.persisted });
+ }
+
+ onoffline = function(event) {
+ let bc = new BroadcastChannel("online_offline_bfcache");
+ bc.postMessage(event.type);
+ bc.close();
+ }
+
+ ononline = function(event) {
+ let bc = new BroadcastChannel("online_offline_bfcache");
+ bc.postMessage(event.type);
+ bc.close();
+ }
+
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_reload.html b/docshell/test/navigation/file_reload.html
new file mode 100644
index 0000000000..f0cb1c2d52
--- /dev/null
+++ b/docshell/test/navigation/file_reload.html
@@ -0,0 +1,23 @@
+<html>
+ <head>
+ <script>
+ window.onpageshow = function() {
+ let bc = new BroadcastChannel("test_reload");
+ bc.onmessage = function(event) {
+ if (event.data == "reload") {
+ bc.close();
+ location.reload(true);
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ }
+
+ bc.postMessage("pageshow");
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_reload_large_postdata.sjs b/docshell/test/navigation/file_reload_large_postdata.sjs
new file mode 100644
index 0000000000..29a1264fac
--- /dev/null
+++ b/docshell/test/navigation/file_reload_large_postdata.sjs
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function readStream(inputStream) {
+ let available = 0;
+ let result = [];
+ while ((available = inputStream.available()) > 0) {
+ result.push(inputStream.readBytes(available));
+ }
+ return result.join("");
+}
+
+function handleRequest(request, response) {
+ let rv = (() => {
+ try {
+ if (request.method != "POST") {
+ return "ERROR: not a POST request";
+ }
+
+ let body = new URLSearchParams(
+ readStream(new BinaryInputStream(request.bodyInputStream))
+ );
+ return body.get("payload").length;
+ } catch (e) {
+ return "ERROR: Exception: " + e;
+ }
+ })();
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(`<!DOCTYPE HTML>
+<script>
+let rv = (${JSON.stringify(rv)});
+opener.postMessage(rv, "*");
+</script>
+ `);
+}
diff --git a/docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs b/docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs
new file mode 100644
index 0000000000..e1ef75efa0
--- /dev/null
+++ b/docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs
@@ -0,0 +1,27 @@
+const createPage = function (msg) {
+ return `
+<html>
+<script>
+ onpageshow = function() {
+ opener.postMessage(document.body.firstChild.contentDocument.body.textContent);
+ }
+</script>
+<body><iframe srcdoc="${msg}"></iframe><body>
+</html>
+`;
+};
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-store");
+ response.setHeader("Content-Type", "text/html");
+
+ let currentState = getState("reload_nonbfcached_srcdoc");
+ let srcdoc = "pageload:" + currentState;
+ if (currentState != "second") {
+ setState("reload_nonbfcached_srcdoc", "second");
+ } else {
+ setState("reload_nonbfcached_srcdoc", "");
+ }
+
+ response.write(createPage(srcdoc));
+}
diff --git a/docshell/test/navigation/file_same_url.html b/docshell/test/navigation/file_same_url.html
new file mode 100644
index 0000000000..72a1dd2564
--- /dev/null
+++ b/docshell/test/navigation/file_same_url.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+let bc = new BroadcastChannel("test_same_url");
+let listener = e => {
+ switch (e.data) {
+ case "linkClick":
+ var link = document.getElementById("link1");
+ link.click();
+ break;
+ case "closeWin":
+ self.close();
+ break;
+ }
+};
+bc.addEventListener("message", listener);
+</script>
+</head>
+<body onpageshow="bc.postMessage({bodyOnLoad: history.length})">
+<a id="link1" href="file_same_url.html">Link 1</a>
+<a name="1">link 1</a>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html
new file mode 100644
index 0000000000..fec51f821d
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html
@@ -0,0 +1,30 @@
+<html>
+ <head>
+ <script>
+ // Ensure layout is flushed before doing anything with scrolling.
+ function flushAndExecute(callback) {
+ window.requestAnimationFrame(function() {
+ setTimeout(callback);
+ });
+ }
+
+ var bc = new BroadcastChannel("bfcached");
+ bc.onmessage = (msgEvent) => {
+ if (msgEvent.data == "loadNext") {
+ flushAndExecute(() => {
+ location.href = "file_scrollRestoration_bfcache_and_nobfcache_part2.html";
+ });
+ } else if (msgEvent.data == "forward") {
+ flushAndExecute(() => {
+ history.forward();
+ });
+ }
+ };
+ window.onpageshow = (event) => {
+ bc.postMessage({command: "pageshow", persisted: event.persisted});
+ };
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html
new file mode 100644
index 0000000000..40e0578515
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html
@@ -0,0 +1,35 @@
+<html>
+ <head>
+ <script>
+ // Note, this page does not enter bfcache because of an HTTP header.
+
+ // Ensure layout is flushed before doing anything with scrolling.
+ function flushAndExecute(callback) {
+ window.requestAnimationFrame(function() {
+ setTimeout(callback);
+ });
+ }
+
+ var bc = new BroadcastChannel("notbfcached");
+ bc.onmessage = (msgEvent) => {
+ if (msgEvent.data == "scroll") {
+ flushAndExecute(() => { window.scrollTo(0, 4000); });
+ } else if (msgEvent.data == "getScrollY") {
+ flushAndExecute(() => { bc.postMessage({ scrollY: window.scrollY}); });
+ } else if (msgEvent.data == "back") {
+ flushAndExecute(() => { bc.close(); history.back(); });
+ } else if (msgEvent.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ }
+ };
+ window.onpageshow = (event) => {
+ bc.postMessage({command: "pageshow", persisted: event.persisted});
+ };
+ </script>
+ </head>
+ <body>
+ <div style="height: 5000px; border: 1px solid black;">content</div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/navigation/file_scrollRestoration_navigate.html b/docshell/test/navigation/file_scrollRestoration_navigate.html
new file mode 100644
index 0000000000..ac78f0abbe
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_navigate.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<script>
+ var bc = new BroadcastChannel("navigate");
+ window.onload = () => {
+ bc.onmessage = (event) => {
+ if (event.data.command == "navigate") {
+ window.location = event.data.location;
+ bc.close();
+ }
+ if (event.data.command == "back") {
+ history.back();
+ bc.close();
+ }
+ }
+ bc.postMessage({command: "loaded", scrollRestoration: history.scrollRestoration});
+ }
+</script> \ No newline at end of file
diff --git a/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html
new file mode 100644
index 0000000000..1c94899ac2
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html
@@ -0,0 +1,63 @@
+<html>
+ <head>
+ <script>
+ var oldHistoryObject = null;
+ var bc = new BroadcastChannel("bug1155730_part1");
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "test") {
+ var currentCase = msg.currentCase;
+ test(currentCase);
+ }
+ }
+
+ function test(currentCase) {
+ var assertIs = [];
+ var assertOk = [];
+ var assertIsNot = [];
+ switch (currentCase) {
+ case 1: {
+ assertOk.push([history.scrollRestoration, "History object has scrollRestoration property."]);
+ assertIs.push([history.scrollRestoration, "auto", "history.scrollRestoration's default value should be 'auto'."]);
+ history.scrollRestoration = "foobar";
+ assertIs.push([history.scrollRestoration, "auto", "Invalid enum value should not change the value of an attribute."]);
+ history.scrollRestoration = "manual";
+ assertIs.push([history.scrollRestoration, "manual", "Valid enum value should change the value of an attribute."]);
+ history.scrollRestoration = "auto";
+ assertIs.push([history.scrollRestoration, "auto", "Valid enum value should change the value of an attribute."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertOk});
+ document.getElementById("bottom").scrollIntoView();
+ window.location.reload(false);
+ break;
+ }
+ case 2: {
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have restored scrolling."]);
+ assertIs.push([history.scrollRestoration, "auto", "Should have the same scrollRestoration as before reload."]);
+ history.scrollRestoration = "manual";
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot});
+ window.location.reload(false);
+ break;
+ }
+ case 3: {
+ assertIs.push([window.scrollY, 0, "Should not have restored scrolling."]);
+ assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration as before reload."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs});
+ bc.close();
+ window.close();
+ break;
+ }
+ }
+ }
+ window.onpageshow = (event) => {
+ bc.postMessage({command: "pageshow", persisted: event.persisted});
+ }
+ </script>
+ </head>
+ <body>
+ <div style="border: 1px solid black; height: 5000px;">
+ &nbsp;</div>
+ <div id="bottom">Hello world</div>
+ <a href="#hash" name="hash">hash</a>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^ b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^
new file mode 100644
index 0000000000..f944e3806d
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store \ No newline at end of file
diff --git a/docshell/test/navigation/file_scrollRestoration_part2_bfcache.html b/docshell/test/navigation/file_scrollRestoration_part2_bfcache.html
new file mode 100644
index 0000000000..2776e42a6e
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_part2_bfcache.html
@@ -0,0 +1,57 @@
+<html>
+ <head>
+ <script>
+ var bc = new BroadcastChannel("bug1155730_part2");
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "test") {
+ var currentCase = msg.currentCase;
+ test(currentCase);
+ }
+ }
+
+ function test(currentCase) {
+ var assertIs = [];
+ var assertIsNot = [];
+ switch (currentCase) {
+ case 1: {
+ history.scrollRestoration = "manual";
+ document.getElementById("bottom").scrollIntoView();
+ window.location.href = "file_scrollRestoration_navigate.html";
+ break;
+ }
+ case 2: {
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have kept the old scroll position."]);
+ assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration as before reload."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot, assert2: "assert2"});
+ window.scrollTo(0, 0);
+ window.location.hash = "hash";
+ bc.postMessage({command: "nextCase"});
+ requestAnimationFrame(() => {
+ test(currentCase + 1);
+ });
+ break;
+ }
+ case 3: {
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled to #hash."]);
+ assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot});
+ bc.close();
+ window.close();
+ break;
+ }
+ }
+ }
+ window.onpageshow = (event) => {
+ bc.postMessage({command: "pageshow", persisted: event.persisted});
+ }
+ </script>
+ </head>
+ <body>
+ <div style="border: 1px solid black; height: 5000px;">
+ &nbsp;</div>
+ <div id="bottom">Hello world</div>
+ <a href="#hash" name="hash">hash</a>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html
new file mode 100644
index 0000000000..ffc68d6ccc
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html
@@ -0,0 +1,157 @@
+<html>
+ <head>
+ <script>
+ var oldHistoryObject = null;
+ var currCaseForIframe = 0;
+ var bc = new BroadcastChannel("bug1155730_part3");
+ bc.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "test") {
+ var currentCase = msg.currentCase;
+ test(currentCase);
+ }
+ }
+
+ // If onpopstate event takes place, check if we need to call 'test()'
+ var callTest = false;
+ var nextCase = 0;
+ window.onpopstate = (e) => {
+ if (callTest) {
+ callTest = false;
+ setTimeout(() => {
+ test(nextCase);
+ });
+ }
+ }
+
+ function test(currentCase) {
+ var assertIs = [];
+ var assertOk = [];
+ var assertIsNot = [];
+ switch (currentCase) {
+ case 1: {
+ history.scrollRestoration = "manual";
+ window.location.hash = "hash";
+ bc.postMessage({command: "nextCase"});
+ requestAnimationFrame(() => {
+ test(currentCase + 1);
+ });
+ break;
+ }
+ case 2: {
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled to #hash."]);
+ assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot});
+ window.location.href = "file_scrollRestoration_navigate.html";
+ break;
+ }
+ case 3: {
+ assertIs.push([window.scrollY, 0, "Shouldn't have kept the old scroll position."]);
+ assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs});
+ history.scrollRestoration = "auto";
+ document.getElementById("bottom").scrollIntoView();
+ history.pushState({ state: "state1" }, "state1");
+ history.pushState({ state: "state2" }, "state2");
+ window.scrollTo(0, 0);
+ bc.postMessage({command: "nextCase"});
+ callTest = true;
+ nextCase = currentCase + 1;
+ history.back(); // go back to state 1
+ break;
+ }
+ case 4: {
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled back to the state1's position"]);
+ assertIs.push([history.state.state, "state1", "Unexpected state."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot});
+
+ history.scrollRestoration = "manual";
+ document.getElementById("bottom").scrollIntoView();
+ history.pushState({ state: "state3" }, "state3");
+ history.pushState({ state: "state4" }, "state4");
+ window.scrollTo(0, 0);
+ bc.postMessage({command: "nextCase"});
+ callTest = true;
+ nextCase = currentCase + 1;
+ history.back(); // go back to state 3
+ break;
+ }
+ case 5: {
+ assertIs.push([Math.round(window.scrollY), 0, "Shouldn't have scrolled back to the state3's position"]);
+ assertIs.push([history.state.state, "state3", "Unexpected state."]);
+
+ history.pushState({ state: "state5" }, "state5");
+ history.scrollRestoration = "auto";
+ document.getElementById("bottom").scrollIntoView();
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled to 'bottom'."]);
+ bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot});
+ bc.postMessage({command: "nextCase"});
+ callTest = true;
+ nextCase = currentCase + 1;
+ // go back to state 3 (state 4 was removed when state 5 was pushed)
+ history.back();
+ break;
+ }
+ case 6: {
+ window.scrollTo(0, 0);
+ bc.postMessage({command: "nextCase"});
+ callTest = true;
+ nextCase = currentCase + 1;
+ history.forward();
+ break;
+ }
+ case 7: {
+ assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled back to the state5's position"]);
+ bc.postMessage({command: "asserts", currentCase, assertIsNot});
+
+ var ifr = document.createElement("iframe");
+ ifr.src = "data:text/html,";
+ document.body.appendChild(ifr);
+ bc.postMessage({command: "nextCase"});
+ currCaseForIframe = currentCase + 1;
+ ifr.onload = () => {
+ test(currCaseForIframe);
+ };
+ break;
+ }
+ case 8: {
+ oldHistoryObject = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]).contentWindow.history;
+ bc.postMessage({command: "nextCase"});
+ currCaseForIframe++;
+ document.getElementsByTagName("iframe")[0].src = "about:blank";
+ break;
+ }
+ case 9: {
+ try {
+ oldHistoryObject.scrollRestoration;
+ assertOk.push([false, "Should have thrown an exception."]);
+ } catch (ex) {
+ assertOk.push([ex != null, "Did get an exception"]);
+ }
+ try {
+ oldHistoryObject.scrollRestoration = "auto";
+ assertOk.push([false, "Should have thrown an exception."]);
+ } catch (ex) {
+ assertOk.push([ex != null, "Did get an exception"]);
+ }
+ bc.postMessage({command: "asserts", currentCase, assertOk});
+ bc.postMessage({command: "finishing"});
+ bc.close();
+ window.close();
+ break;
+ }
+ }
+ }
+ window.onpageshow = (event) => {
+ bc.postMessage({command: "pageshow", persisted: event.persisted});
+ }
+ </script>
+ </head>
+ <body>
+ <div style="border: 1px solid black; height: 5000px;">
+ &nbsp;</div>
+ <div id="bottom">Hello world</div>
+ <a href="#hash" name="hash">hash</a>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^ b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^
new file mode 100644
index 0000000000..f944e3806d
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store \ No newline at end of file
diff --git a/docshell/test/navigation/file_session_history_on_redirect.html b/docshell/test/navigation/file_session_history_on_redirect.html
new file mode 100644
index 0000000000..df7e99a1dd
--- /dev/null
+++ b/docshell/test/navigation/file_session_history_on_redirect.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script>
+ window.onpagehide = function(event) {
+ opener.is(event.persisted, false, "Should have disabled bfcache for this test.");
+ }
+
+ window.onpageshow = function(event) {
+ opener.pageshow();
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_session_history_on_redirect.html^headers^ b/docshell/test/navigation/file_session_history_on_redirect.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/navigation/file_session_history_on_redirect.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/navigation/file_session_history_on_redirect_2.html b/docshell/test/navigation/file_session_history_on_redirect_2.html
new file mode 100644
index 0000000000..df7e99a1dd
--- /dev/null
+++ b/docshell/test/navigation/file_session_history_on_redirect_2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script>
+ window.onpagehide = function(event) {
+ opener.is(event.persisted, false, "Should have disabled bfcache for this test.");
+ }
+
+ window.onpageshow = function(event) {
+ opener.pageshow();
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_session_history_on_redirect_2.html^headers^ b/docshell/test/navigation/file_session_history_on_redirect_2.html^headers^
new file mode 100644
index 0000000000..59ba296103
--- /dev/null
+++ b/docshell/test/navigation/file_session_history_on_redirect_2.html^headers^
@@ -0,0 +1 @@
+Cache-control: no-store
diff --git a/docshell/test/navigation/file_sessionhistory_iframe_removal.html b/docshell/test/navigation/file_sessionhistory_iframe_removal.html
new file mode 100644
index 0000000000..f18e849942
--- /dev/null
+++ b/docshell/test/navigation/file_sessionhistory_iframe_removal.html
@@ -0,0 +1,37 @@
+<html>
+ <head>
+ <script>
+ async function wait() {
+ return opener.SpecialPowers.spawnChrome([], function() { /*dummy */ });
+ }
+ async function run() {
+ var counter = 0;
+ while(true) {
+ // Push new history entries until we hit the limit and start removing
+ // entries from the front.
+ var len = history.length;
+ document.getElementById("ifr1").contentWindow.history.pushState(++counter, null, null);
+ await wait();
+ if (len == history.length) {
+ opener.ok(true, "Found max length " + len);
+ document.getElementById("ifr2").contentWindow.history.pushState(++counter, null, null);
+ await wait();
+ document.getElementById("ifr1").contentWindow.history.pushState(++counter, null, null);
+ await wait();
+ opener.is(len, history.length, "Adding session history entries in different iframe should behave the same way");
+ // This should remove all the history entries for ifr1, leaving just
+ // the ones for ifr2.
+ document.getElementById("ifr1").remove();
+ opener.is(history.length, 2, "Should have entries for the initial load and the pushState for ifr2");
+ opener.finishTest();
+ break;
+ }
+ }
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(run)">
+ <iframe src="blank.html" id="ifr1"></iframe>
+ <iframe src="blank.html" id="ifr2"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_sessionstorage_across_coop.html b/docshell/test/navigation/file_sessionstorage_across_coop.html
new file mode 100644
index 0000000000..44489b9e2f
--- /dev/null
+++ b/docshell/test/navigation/file_sessionstorage_across_coop.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script>
+ addEventListener("load", () => {
+ let bc = new BroadcastChannel("testChannel");
+ bc.postMessage(sessionStorage.getItem("testItem"));
+ window.close();
+ });
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_sessionstorage_across_coop.html^headers^ b/docshell/test/navigation/file_sessionstorage_across_coop.html^headers^
new file mode 100644
index 0000000000..46ad58d83b
--- /dev/null
+++ b/docshell/test/navigation/file_sessionstorage_across_coop.html^headers^
@@ -0,0 +1 @@
+Cross-Origin-Opener-Policy: same-origin
diff --git a/docshell/test/navigation/file_shiftReload_and_pushState.html b/docshell/test/navigation/file_shiftReload_and_pushState.html
new file mode 100644
index 0000000000..7882143c83
--- /dev/null
+++ b/docshell/test/navigation/file_shiftReload_and_pushState.html
@@ -0,0 +1,28 @@
+<html>
+ <head>
+ <script>
+ function test() {
+ try {
+ frames[0].history.pushState({}, "state", "?pushed");
+ } catch (ex) {
+ opener.ok(false, "history.pushState shouldn't throw");
+ }
+
+ if (!opener.shiftReloadPushStateFirstRound) {
+ opener.shiftReloadPushStateFirstRound = true;
+ window.location.reload(true);
+ } else {
+ opener.ok(true, "Did run history.push");
+ opener.finishTest();
+ }
+ }
+
+ window.addEventListener("load", function() { setTimeout(test, 0); });
+ </script>
+ </head>
+ <body>
+ <iframe src="frame0.html"></iframe>
+ <script>
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_ship_beforeunload_fired.html b/docshell/test/navigation/file_ship_beforeunload_fired.html
new file mode 100644
index 0000000000..a1f416f959
--- /dev/null
+++ b/docshell/test/navigation/file_ship_beforeunload_fired.html
@@ -0,0 +1,37 @@
+<html>
+ <script>
+ onpageshow = function(pageShowEvent) {
+ var bc = new BroadcastChannel("ship_beforeunload");
+ bc.onmessage = function(event) {
+ if (event.data.action == "register_beforeunload") {
+ onbeforeunload = function() {
+ bc.postMessage("beforeunload_fired");
+ bc.close();
+ }
+ if (event.data.loadNextPageFromSessionHistory) {
+ history.back();
+ } else {
+ location.href += "?differentpage";
+ }
+ } else if (event.data.action == "navigate_to_page_b") {
+ bc.close();
+ if (event.data.blockBFCache) {
+ window.blockBFCache = new RTCPeerConnection();
+ }
+ location.href += "?pageb";
+ } else if (event.data.action == "back_to_page_b") {
+ if (event.data.forwardNavigateToPageB) {
+ history.forward();
+ } else {
+ history.back();
+ }
+ bc.close();
+ } else if (event.data.action == "close") {
+ bc.close();
+ window.close();
+ }
+ }
+ bc.postMessage({type: pageShowEvent.type, persisted: pageShowEvent.persisted});
+ }
+ </script>
+</html>
diff --git a/docshell/test/navigation/file_static_and_dynamic_1.html b/docshell/test/navigation/file_static_and_dynamic_1.html
new file mode 100644
index 0000000000..e66216c41e
--- /dev/null
+++ b/docshell/test/navigation/file_static_and_dynamic_1.html
@@ -0,0 +1,31 @@
+<html>
+ <head>
+ <script>
+ function test() {
+ var ifr = document.createElement("iframe");
+ ifr.src = "frame0.html";
+ document.getElementById("dynamic").appendChild(ifr);
+ var staticFrame = document.getElementById("staticframe");
+ staticFrame.onload = window.location = "goback.html";
+ staticFrame.contentWindow.location = "frame1.html";
+ }
+
+ function start() {
+ if (++opener.testCount == 1) {
+ test();
+ } else {
+ var staticFrame = document.getElementById("staticframe");
+ opener.ok(String(staticFrame.contentWindow.location).includes(staticFrame.src),
+ "Wrong document loaded!");
+ opener.finishTest();
+ }
+ }
+ </script>
+ </head>
+ <body onload="setTimeout('start()', 0)">
+ <h5>Dynamic</h5>
+ <div id="dynamic"></div>
+ <h5>Static</h5>
+ <div id="static"><iframe id="staticframe" src="frame0.html"></iframe></div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/file_tell_opener.html b/docshell/test/navigation/file_tell_opener.html
new file mode 100644
index 0000000000..bd45c275e6
--- /dev/null
+++ b/docshell/test/navigation/file_tell_opener.html
@@ -0,0 +1,8 @@
+<html>
+ <body onload="bodyLoaded()">Frame 1</body>
+ <script>
+ function bodyLoaded() {
+ opener.postMessage("body-loaded", "*");
+ }
+ </script>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_frame_1.html b/docshell/test/navigation/file_triggeringprincipal_frame_1.html
new file mode 100644
index 0000000000..528437f892
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_frame_1.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset="utf-8"></head>
+<body>
+<b>Frame 1</b><br/>
+<a href="#"" id="testlink" onclick="parent.frames[1].frames[0].location='http://test2.mochi.test:8888/tests/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html'">click me</a>
+
+<script type="application/javascript">
+ // make sure to set document.domain to the same domain as the subframe
+ window.onload = function() {
+ document.domain = "mochi.test";
+ };
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ // make sure to get the right start command, otherwise
+ // let the parent know and fail the test
+ if (event.data.start !== "startTest") {
+ window.removeEventListener("message", receiveMessage);
+ window.parent.postMessage({triggeringPrincipalURI: "false"}, "*");
+ }
+ // click the link to navigate the subframe
+ document.getElementById("testlink").click();
+ }
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_frame_2.html b/docshell/test/navigation/file_triggeringprincipal_frame_2.html
new file mode 100644
index 0000000000..ef7cdfc178
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_frame_2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset="utf-8"></head>
+<body>
+<b>Frame 2</b><br/>
+<iframe src="http://test2.mochi.test:8888/tests/docshell/test/navigation/file_triggeringprincipal_subframe.html"></iframe>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html
new file mode 100644
index 0000000000..75b2933c1b
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+Frame A
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html
new file mode 100644
index 0000000000..0479f5e1e5
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+Frame A navigated by Frame B
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html
new file mode 100644
index 0000000000..e5d40b267a
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<body>
+Frame B navigating Frame A
+
+<script type="text/javascript">
+
+window.open("file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html", "framea");
+
+</script>
+
+</body>
+</html>
+
+
diff --git a/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html
new file mode 100644
index 0000000000..caa6b275b9
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+base test frame
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html
new file mode 100644
index 0000000000..f4a4d0e631
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+navigated by window.open()
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_subframe.html b/docshell/test/navigation/file_triggeringprincipal_subframe.html
new file mode 100644
index 0000000000..ba6b6dc09a
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_subframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset='utf-8'></head>
+<body>
+<b>Sub Frame 2</b><br/>
+<script type='application/javascript'>
+ // make sure to set document.domain to same domain as frame 1
+ window.onload = function() {
+ document.domain = "mochi.test";
+ // let Frame 1 know that we are ready to run the test
+ window.parent.parent.frames[0].postMessage({start: "startTest"}, "*");
+ };
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html b/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html
new file mode 100644
index 0000000000..582181c00d
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset="utf-8"></head>
+<body onload="checkResults()">
+<b>Sub Frame 2 Navigated</b><br/>
+
+<script type='application/javascript'>
+ function checkResults() {
+ // query the uri of the loadingPrincipal and the TriggeringPrincipal and pass
+ // that information on to the parent for verification.
+ var channel = SpecialPowers.wrap(window).docShell.currentDocumentChannel;
+ var triggeringPrincipalURI = channel.loadInfo.triggeringPrincipal.asciiSpec;
+ var loadingPrincipalURI = channel.loadInfo.loadingPrincipal.asciiSpec;
+ var referrerURI = document.referrer;
+ window.parent.parent.postMessage({triggeringPrincipalURI,
+ loadingPrincipalURI,
+ referrerURI}, "*");
+ }
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html b/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html
new file mode 100644
index 0000000000..c84e216ae8
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head><meta charset="utf-8"></head>
+<body onload="checkResults()">
+<b>SubFrame Same-Origin Navigated</b><br/>
+
+<script type='application/javascript'>
+ function checkResults() {
+ // query the uri of the loadingPrincipal and the TriggeringPrincipal and pass
+ // that information on to the parent for verification.
+ var channel = SpecialPowers.wrap(window).docShell.currentDocumentChannel;
+ var triggeringPrincipalURI = channel.loadInfo.triggeringPrincipal.asciiSpec;
+ var loadingPrincipalURI = channel.loadInfo.loadingPrincipal.asciiSpec;
+
+ window.parent.postMessage({triggeringPrincipalURI,
+ loadingPrincipalURI}, "*");
+ }
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/file_triggeringprincipal_window_open.html b/docshell/test/navigation/file_triggeringprincipal_window_open.html
new file mode 100644
index 0000000000..d0644a4d5c
--- /dev/null
+++ b/docshell/test/navigation/file_triggeringprincipal_window_open.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+http
+</body>
+</html>
diff --git a/docshell/test/navigation/frame0.html b/docshell/test/navigation/frame0.html
new file mode 100644
index 0000000000..93d1c9c822
--- /dev/null
+++ b/docshell/test/navigation/frame0.html
@@ -0,0 +1,3 @@
+<html>
+ <body>Frame 0</body>
+</html>
diff --git a/docshell/test/navigation/frame1.html b/docshell/test/navigation/frame1.html
new file mode 100644
index 0000000000..4d06c09d1c
--- /dev/null
+++ b/docshell/test/navigation/frame1.html
@@ -0,0 +1,3 @@
+<html>
+ <body>Frame 1</body>
+</html>
diff --git a/docshell/test/navigation/frame2.html b/docshell/test/navigation/frame2.html
new file mode 100644
index 0000000000..7a3b5e0b9b
--- /dev/null
+++ b/docshell/test/navigation/frame2.html
@@ -0,0 +1,3 @@
+<html>
+ <body>Frame 2</body>
+</html>
diff --git a/docshell/test/navigation/frame3.html b/docshell/test/navigation/frame3.html
new file mode 100644
index 0000000000..fd24293873
--- /dev/null
+++ b/docshell/test/navigation/frame3.html
@@ -0,0 +1,3 @@
+<html>
+ <body>Frame 3</body>
+</html>
diff --git a/docshell/test/navigation/frame_1_out_of_6.html b/docshell/test/navigation/frame_1_out_of_6.html
new file mode 100644
index 0000000000..93547cd1c4
--- /dev/null
+++ b/docshell/test/navigation/frame_1_out_of_6.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 1
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_2_out_of_6.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/frame_2_out_of_6.html b/docshell/test/navigation/frame_2_out_of_6.html
new file mode 100644
index 0000000000..02056acce8
--- /dev/null
+++ b/docshell/test/navigation/frame_2_out_of_6.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 2
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_3_out_of_6.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/frame_3_out_of_6.html b/docshell/test/navigation/frame_3_out_of_6.html
new file mode 100644
index 0000000000..e9dc308f6e
--- /dev/null
+++ b/docshell/test/navigation/frame_3_out_of_6.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 3
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/frame_4_out_of_6.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/frame_4_out_of_6.html b/docshell/test/navigation/frame_4_out_of_6.html
new file mode 100644
index 0000000000..66a5083e4f
--- /dev/null
+++ b/docshell/test/navigation/frame_4_out_of_6.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 4
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://test2.mochi.test:8888/tests/docshell/test/navigation/frame_5_out_of_6.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/frame_5_out_of_6.html b/docshell/test/navigation/frame_5_out_of_6.html
new file mode 100644
index 0000000000..0121f0f749
--- /dev/null
+++ b/docshell/test/navigation/frame_5_out_of_6.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 5
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://example.org:80/tests/docshell/test/navigation/frame_6_out_of_6.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/frame_6_out_of_6.html b/docshell/test/navigation/frame_6_out_of_6.html
new file mode 100644
index 0000000000..c9827ccaae
--- /dev/null
+++ b/docshell/test/navigation/frame_6_out_of_6.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 6
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html"></iframe>
+ </body>
+</html> \ No newline at end of file
diff --git a/docshell/test/navigation/frame_load_as_example_com.html b/docshell/test/navigation/frame_load_as_example_com.html
new file mode 100644
index 0000000000..a1a4e7110a
--- /dev/null
+++ b/docshell/test/navigation/frame_load_as_example_com.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ example.com
+ <iframe style="height: 100vh; width:100%;" id="static" src="http://example.org/tests/docshell/test/navigation/frame_load_as_example_org.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/frame_load_as_example_org.html b/docshell/test/navigation/frame_load_as_example_org.html
new file mode 100644
index 0000000000..2fbb8038c9
--- /dev/null
+++ b/docshell/test/navigation/frame_load_as_example_org.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ example.org
+ <iframe style="height: 100vh; width:100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/frame_load_as_host1.html b/docshell/test/navigation/frame_load_as_host1.html
new file mode 100644
index 0000000000..eb006b21a1
--- /dev/null
+++ b/docshell/test/navigation/frame_load_as_host1.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 1
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_load_as_host2.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/frame_load_as_host2.html b/docshell/test/navigation/frame_load_as_host2.html
new file mode 100644
index 0000000000..5457c17e9b
--- /dev/null
+++ b/docshell/test/navigation/frame_load_as_host2.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 2
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host3.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/frame_load_as_host3.html b/docshell/test/navigation/frame_load_as_host3.html
new file mode 100644
index 0000000000..a9064ec867
--- /dev/null
+++ b/docshell/test/navigation/frame_load_as_host3.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ Page 3
+ <iframe style="height: 100vh; width: 100%;" id="static" src="http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/frame_recursive.html b/docshell/test/navigation/frame_recursive.html
new file mode 100644
index 0000000000..835d9d63a2
--- /dev/null
+++ b/docshell/test/navigation/frame_recursive.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ example.com
+ <iframe style="height: 100vh; width:100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_recursive.html"></iframe>
+ </body>
+</html>
diff --git a/docshell/test/navigation/goback.html b/docshell/test/navigation/goback.html
new file mode 100644
index 0000000000..ce2968374e
--- /dev/null
+++ b/docshell/test/navigation/goback.html
@@ -0,0 +1,5 @@
+<html>
+ <body onload="setTimeout('window.history.go(-1)', 1000);">
+ window.history.go(-1);
+ </body>
+</html>
diff --git a/docshell/test/navigation/iframe.html b/docshell/test/navigation/iframe.html
new file mode 100644
index 0000000000..4685fea7b7
--- /dev/null
+++ b/docshell/test/navigation/iframe.html
@@ -0,0 +1,8 @@
+<html>
+<body>
+<script>
+var src = window.location.hash.substring(1);
+document.write('<iframe src="' + src + '"></iframe>');
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/iframe_slow_onload.html b/docshell/test/navigation/iframe_slow_onload.html
new file mode 100644
index 0000000000..e8555699bb
--- /dev/null
+++ b/docshell/test/navigation/iframe_slow_onload.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ <iframe id="inner" src="iframe_slow_onload_inner.html">
+ </body>
+</html>
diff --git a/docshell/test/navigation/iframe_slow_onload_inner.html b/docshell/test/navigation/iframe_slow_onload_inner.html
new file mode 100644
index 0000000000..ad39eba795
--- /dev/null
+++ b/docshell/test/navigation/iframe_slow_onload_inner.html
@@ -0,0 +1,19 @@
+<html>
+ <head>
+ <script>
+ function load() {
+ // Let the test page know that it can try to navigate.
+ top.postMessage("onload", "*");
+ // We're starting an infinite loop, but we need to time out after a
+ // while, or the loop will keep running until shutdown.
+ let t0 = performance.now();
+ while (performance.now() - t0 < 5000) {
+ document.getElementById("output").innerText = Math.random();
+ }
+ }
+ </script>
+ </head>
+ <body onload="load()">
+ <p id="output"></p>
+ </body>
+</html>
diff --git a/docshell/test/navigation/iframe_static.html b/docshell/test/navigation/iframe_static.html
new file mode 100644
index 0000000000..1bdd1437c1
--- /dev/null
+++ b/docshell/test/navigation/iframe_static.html
@@ -0,0 +1,8 @@
+<html>
+ <body>
+ Nested Frame
+ <div id="frameContainer">
+ <iframe id="staticFrame" src="frame0.html"></iframe>
+ </div>
+ </body>
+</html>
diff --git a/docshell/test/navigation/mochitest.toml b/docshell/test/navigation/mochitest.toml
new file mode 100644
index 0000000000..ce0fa0d18f
--- /dev/null
+++ b/docshell/test/navigation/mochitest.toml
@@ -0,0 +1,359 @@
+[DEFAULT]
+support-files = [
+ "NavigationUtils.js",
+ "navigation_target_url.html",
+ "navigation_target_popup_url.html",
+ "blank.html",
+ "file_bug386782_contenteditable.html",
+ "file_bug386782_designmode.html",
+ "redbox_bug430723.html",
+ "bluebox_bug430723.html",
+ "file_bug462076_1.html",
+ "file_bug462076_2.html",
+ "file_bug462076_3.html",
+ "file_bug508537_1.html",
+ "file_bug534178.html",
+ "file_document_write_1.html",
+ "file_fragment_handling_during_load.html",
+ "file_fragment_handling_during_load_frame1.html",
+ "file_fragment_handling_during_load_frame2.sjs",
+ "file_nested_frames.html",
+ "file_nested_frames_innerframe.html",
+ "file_shiftReload_and_pushState.html",
+ "file_static_and_dynamic_1.html",
+ "frame0.html",
+ "frame1.html",
+ "frame2.html",
+ "frame3.html",
+ "goback.html",
+ "iframe.html",
+ "iframe_static.html",
+ "navigate.html",
+ "open.html",
+ "parent.html",
+ "file_tell_opener.html",
+ "file_triggeringprincipal_frame_1.html",
+ "file_triggeringprincipal_frame_2.html",
+ "file_triggeringprincipal_subframe.html",
+ "file_triggeringprincipal_subframe_nav.html",
+ "file_triggeringprincipal_subframe_same_origin_nav.html",
+ "file_triggeringprincipal_window_open.html",
+ "file_triggeringprincipal_parent_iframe_window_open_base.html",
+ "file_triggeringprincipal_parent_iframe_window_open_nav.html",
+ "file_triggeringprincipal_iframe_iframe_window_open_frame_a.html",
+ "file_triggeringprincipal_iframe_iframe_window_open_frame_b.html",
+ "file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html",
+ "file_load_history_entry_page_with_one_link.html",
+ "file_load_history_entry_page_with_two_links.html",
+ "file_bug1300461.html",
+ "file_bug1300461_redirect.html",
+ "file_bug1300461_redirect.html^headers^",
+ "file_bug1300461_back.html",
+ "file_contentpolicy_block_window.html",
+ "file_bug1326251.html",
+ "file_bug1326251_evict_cache.html",
+ "file_bug1364364-1.html",
+ "file_bug1364364-2.html",
+ "file_bug1375833.html",
+ "file_bug1375833-frame1.html",
+ "file_bug1375833-frame2.html",
+ "test_bug145971.html",
+ "file_bug1609475.html",
+ "file_bug1379762-1.html",
+ "file_scrollRestoration_bfcache_and_nobfcache.html",
+ "file_scrollRestoration_bfcache_and_nobfcache_part2.html",
+ "file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^",
+ "file_scrollRestoration_navigate.html",
+ "file_scrollRestoration_part1_nobfcache.html",
+ "file_scrollRestoration_part1_nobfcache.html^headers^",
+ "file_scrollRestoration_part2_bfcache.html",
+ "file_scrollRestoration_part3_nobfcache.html",
+ "file_scrollRestoration_part3_nobfcache.html^headers^",
+ "file_session_history_on_redirect.html",
+ "file_session_history_on_redirect.html^headers^",
+ "file_session_history_on_redirect_2.html",
+ "file_session_history_on_redirect_2.html^headers^",
+ "redirect_handlers.sjs",
+ "frame_load_as_example_com.html",
+ "frame_load_as_example_org.html",
+ "frame_load_as_host1.html",
+ "frame_load_as_host2.html",
+ "frame_load_as_host3.html",
+ "frame_1_out_of_6.html",
+ "frame_2_out_of_6.html",
+ "frame_3_out_of_6.html",
+ "frame_4_out_of_6.html",
+ "frame_5_out_of_6.html",
+ "frame_6_out_of_6.html",
+ "frame_recursive.html",
+ "object_recursive_load.html",
+ "file_nested_srcdoc.html",
+]
+
+["test_aboutblank_change_process.html"]
+
+["test_beforeunload_and_bfcache.html"]
+support-files = ["file_beforeunload_and_bfcache.html"]
+
+["test_blockBFCache.html"]
+support-files = [
+ "file_blockBFCache.html",
+ "slow.sjs",
+ "iframe_slow_onload.html",
+ "iframe_slow_onload_inner.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug13871.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug270414.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug278916.html"]
+
+["test_bug279495.html"]
+
+["test_bug344861.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug386782.html"]
+
+["test_bug430624.html"]
+
+["test_bug430723.html"]
+skip-if = [
+ "!debug && (os == 'mac' || os == 'win')", # Bug 874423
+ "http3",
+ "http2",
+]
+
+["test_bug1300461.html"]
+
+["test_bug1326251.html"]
+skip-if = [
+ "os == 'android'",
+ "sessionHistoryInParent", # It relies on the bfcache
+]
+
+["test_bug1364364.html"]
+skip-if = ["os == 'android'"] # Bug 1560378
+
+["test_bug1375833.html"]
+
+["test_bug1379762.html"]
+
+["test_bug1536471.html"]
+support-files = ["file_bug1536471.html"]
+
+["test_bug1583110.html"]
+support-files = ["file_bug1583110.html"]
+
+["test_bug1609475.html"]
+
+["test_bug1699721.html"]
+skip-if = ["!fission"] # tests fission-only process switching behaviour
+
+["test_bug1706090.html"]
+support-files = ["file_bug1706090.html"]
+skip-if = ["sessionHistoryInParent"] # The test is currently for the old bfcache implementation
+
+["test_bug1745638.html"]
+support-files = ["file_bug1745638.html"]
+
+["test_bug1747019.html"]
+support-files = [
+ "goback.html",
+ "cache_control_max_age_3600.sjs",
+]
+
+["test_bug1750973.html"]
+support-files = ["file_bug1750973.html"]
+
+["test_bug1758664.html"]
+support-files = ["file_bug1758664.html"]
+skip-if = ["!sessionHistoryInParent"] # the old implementation behaves inconsistently
+
+["test_child.html"]
+
+["test_contentpolicy_block_window.html"]
+
+["test_docshell_gotoindex.html"]
+support-files = ["file_docshell_gotoindex.html"]
+
+["test_dynamic_frame_forward_back.html"]
+
+["test_evict_from_bfcache.html"]
+support-files = ["file_evict_from_bfcache.html"]
+
+["test_fragment_handling_during_load.html"]
+
+["test_grandchild.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_load_history_entry.html"]
+
+["test_meta_refresh.html"]
+support-files = ["file_meta_refresh.html"]
+
+["test_navigation_type.html"]
+support-files = ["file_navigation_type.html"]
+
+["test_nested_frames.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_new_shentry_during_history_navigation.html"]
+support-files = [
+ "file_new_shentry_during_history_navigation_1.html",
+ "file_new_shentry_during_history_navigation_1.html^headers^",
+ "file_new_shentry_during_history_navigation_2.html",
+ "file_new_shentry_during_history_navigation_2.html^headers^",
+ "file_new_shentry_during_history_navigation_3.html",
+ "file_new_shentry_during_history_navigation_3.html^headers^",
+ "file_new_shentry_during_history_navigation_4.html",
+]
+
+["test_not-opener.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_online_offline_bfcache.html"]
+support-files = ["file_online_offline_bfcache.html"]
+
+["test_open_javascript_noopener.html"]
+
+["test_opener.html"]
+skip-if = ["true"] # Bug 1572299, Bug 1716402, Bug 1797751, Bug 1781601
+
+["test_performance_navigation.html"]
+
+["test_popup-navigates-children.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_rate_limit_location_change.html"]
+
+["test_recursive_frames.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_reload.html"]
+support-files = ["file_reload.html"]
+
+["test_reload_large_postdata.html"]
+support-files = ["file_reload_large_postdata.sjs"]
+
+["test_reload_nonbfcached_srcdoc.html"]
+support-files = ["file_reload_nonbfcached_srcdoc.sjs"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_reserved.html"]
+skip-if = ["debug"] # bug 1263213
+
+["test_same_url.html"]
+support-files = ["file_same_url.html"]
+
+["test_scrollRestoration.html"]
+
+["test_session_history_entry_cleanup.html"]
+
+["test_session_history_on_redirect.html"]
+
+["test_sessionhistory.html"]
+skip-if = ["verify && os == 'mac' && debug"] # Hit MOZ_CRASH(Shutdown too long, probably frozen, causing a crash.) bug 1677545
+
+["test_sessionhistory_document_write.html"]
+
+["test_sessionhistory_iframe_removal.html"]
+support-files = ["file_sessionhistory_iframe_removal.html"]
+
+["test_sessionstorage_across_coop.html"]
+support-files = [
+ "file_sessionstorage_across_coop.html",
+ "file_sessionstorage_across_coop.html^headers^",
+]
+
+["test_shiftReload_and_pushState.html"]
+
+["test_ship_beforeunload_fired.html"]
+support-files = ["file_ship_beforeunload_fired.html"]
+skip-if = [
+ "!sessionHistoryInParent",
+ "http3",
+ "http2",
+]
+
+["test_ship_beforeunload_fired_2.html"]
+support-files = ["file_ship_beforeunload_fired.html"]
+skip-if = ["!sessionHistoryInParent"]
+
+["test_ship_beforeunload_fired_3.html"]
+support-files = ["file_ship_beforeunload_fired.html"]
+skip-if = ["!sessionHistoryInParent"]
+
+["test_sibling-matching-parent.html"]
+
+["test_sibling-off-domain.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_state_size.html"]
+
+["test_static_and_dynamic.html"]
+skip-if = ["true"] # This was disabled for a few years now anyway, bug 1677544
+
+["test_triggeringprincipal_frame_nav.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_triggeringprincipal_frame_same_origin_nav.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_triggeringprincipal_iframe_iframe_window_open.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_triggeringprincipal_parent_iframe_window_open.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_triggeringprincipal_window_open.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
diff --git a/docshell/test/navigation/navigate.html b/docshell/test/navigation/navigate.html
new file mode 100644
index 0000000000..eed1eb1c18
--- /dev/null
+++ b/docshell/test/navigation/navigate.html
@@ -0,0 +1,37 @@
+<html>
+<head>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="NavigationUtils.js"></script>
+ <script>
+ function navigate() {
+ var args = window.location.hash.substring(1).split(",");
+ var target = args[0];
+ var mechanism = args[1];
+
+ switch (mechanism) {
+ case "location":
+ // eslint-disable-next-line no-eval
+ navigateByLocation(eval(target));
+ break;
+ case "open":
+ navigateByOpen(target);
+ break;
+ case "form":
+ navigateByForm(target);
+ break;
+ case "hyperlink":
+ navigateByHyperlink(target);
+ break;
+ }
+ }
+ </script>
+</head>
+<body onload="navigate();">
+<script>
+var args = window.location.hash.substring(1).split(",");
+var target = args[0];
+var mechanism = args[1];
+document.write("target=" + target + " mechanism=" + mechanism);
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/navigation_target_popup_url.html b/docshell/test/navigation/navigation_target_popup_url.html
new file mode 100644
index 0000000000..cfe6de009d
--- /dev/null
+++ b/docshell/test/navigation/navigation_target_popup_url.html
@@ -0,0 +1 @@
+<html><body>This is a popup</body></html>
diff --git a/docshell/test/navigation/navigation_target_url.html b/docshell/test/navigation/navigation_target_url.html
new file mode 100644
index 0000000000..a485e8133f
--- /dev/null
+++ b/docshell/test/navigation/navigation_target_url.html
@@ -0,0 +1 @@
+<html><body>This frame was navigated.</body></html>
diff --git a/docshell/test/navigation/object_recursive_load.html b/docshell/test/navigation/object_recursive_load.html
new file mode 100644
index 0000000000..3ae9521e63
--- /dev/null
+++ b/docshell/test/navigation/object_recursive_load.html
@@ -0,0 +1,6 @@
+<html>
+ <body width="400" height="300">
+ Frame 0
+ <object id="static" width="400" height="300" data="http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html"></object>
+ </body>
+</html>
diff --git a/docshell/test/navigation/open.html b/docshell/test/navigation/open.html
new file mode 100644
index 0000000000..97eb9b76e1
--- /dev/null
+++ b/docshell/test/navigation/open.html
@@ -0,0 +1,9 @@
+<html>
+<body>
+<script>
+var target = window.location.hash.substring(1);
+document.write("target=" + target);
+window.open("navigation_target_popup_url.html", target, "width=10,height=10");
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/parent.html b/docshell/test/navigation/parent.html
new file mode 100644
index 0000000000..74722b8bdf
--- /dev/null
+++ b/docshell/test/navigation/parent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body>
+This document contains a frame.
+<div><iframe src="blank.html"></iframe></div>
+<script>
+frames[0].name = window.name + "_child0";
+window.onload = function() {
+ opener.postMessage("ready", "*");
+};
+</script>
+</body>
+</html>
+
diff --git a/docshell/test/navigation/redbox_bug430723.html b/docshell/test/navigation/redbox_bug430723.html
new file mode 100644
index 0000000000..c2d1f98092
--- /dev/null
+++ b/docshell/test/navigation/redbox_bug430723.html
@@ -0,0 +1,6 @@
+<html><head>
+<script> window.addEventListener("pageshow", function() { opener.nextTest(); }); </script>
+</head><body>
+<div style="position:absolute; left:0px; top:0px; width:50%; height:150%; background-color:red">
+<p>This is a very tall red box.</p>
+</div></body></html>
diff --git a/docshell/test/navigation/redirect_handlers.sjs b/docshell/test/navigation/redirect_handlers.sjs
new file mode 100644
index 0000000000..c2b39e61c9
--- /dev/null
+++ b/docshell/test/navigation/redirect_handlers.sjs
@@ -0,0 +1,29 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Cache-Control", "no-store", false);
+
+ let state = getState("sessionhistory_do_redirect");
+ if (state != "doredirect") {
+ response.setHeader("Content-Type", "text/html");
+ const contents = `
+ <script>
+ window.onpageshow = function(event) {
+ opener.pageshow();
+ }
+ </script>
+ `;
+ response.write(contents);
+
+ // The next load should do a redirect.
+ setState("sessionhistory_do_redirect", "doredirect");
+ } else {
+ setState("sessionhistory_do_redirect", "");
+
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ "file_session_history_on_redirect_2.html",
+ false
+ );
+ }
+}
diff --git a/docshell/test/navigation/redirect_to_blank.sjs b/docshell/test/navigation/redirect_to_blank.sjs
new file mode 100644
index 0000000000..b1668401ea
--- /dev/null
+++ b/docshell/test/navigation/redirect_to_blank.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Cache-Control", "no-store", false);
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", "blank.html", false);
+}
diff --git a/docshell/test/navigation/slow.sjs b/docshell/test/navigation/slow.sjs
new file mode 100644
index 0000000000..9720f807f6
--- /dev/null
+++ b/docshell/test/navigation/slow.sjs
@@ -0,0 +1,16 @@
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ function () {
+ response.finish();
+ },
+ 5000,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+
+ response.setStatusLine(null, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("Start of the content.");
+}
diff --git a/docshell/test/navigation/test_aboutblank_change_process.html b/docshell/test/navigation/test_aboutblank_change_process.html
new file mode 100644
index 0000000000..61325570f3
--- /dev/null
+++ b/docshell/test/navigation/test_aboutblank_change_process.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<script>
+// Open a window and navigate it from https://example.com to about:blank
+// With fission, we should switch processes and about:blank should load in
+// the same process as this test page.
+// This is a crash test.
+add_task(async function test_aboutblank_change_process() {
+ let exampleLoaded = new Promise(resolve => {
+ function onMessage(event) {
+ if (event.data == "body-loaded") {
+ window.removeEventListener("message", onMessage);
+ resolve();
+ }
+ }
+ window.addEventListener("message", onMessage);
+ });
+ let win = window.open();
+ win.location = "https://example.com/tests/docshell/test/navigation/file_tell_opener.html";
+ await exampleLoaded;
+
+ win.location = "about:blank";
+
+ // A crash happens somewhere here when about:blank does not go via
+ // DocumentChannel with fission enabled
+
+ // Wait for about:blank to load in this process
+ await SimpleTest.promiseWaitForCondition(() => {
+ try {
+ return win.location.href == "about:blank";
+ } catch (e) {
+ // While the `win` still has example.com page loaded, `win.location` will
+ // be a cross origin object and querying win.location.href will throw a
+ // SecurityError. Return false as long as this is the case.
+ return false;
+ }
+ })
+
+ ok(true, "We did not crash");
+ win.close();
+});
+</script>
diff --git a/docshell/test/navigation/test_beforeunload_and_bfcache.html b/docshell/test/navigation/test_beforeunload_and_bfcache.html
new file mode 100644
index 0000000000..6bb958c6c6
--- /dev/null
+++ b/docshell/test/navigation/test_beforeunload_and_bfcache.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Loading a page from BFCache and firing beforeunload on the current page</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ /*
+ * This is a simple test to ensure beforeunload is fired on the current page
+ * when restoring a page from bfcache.
+ * (1) The controller page opens a new window. Another page is loaded there
+ * and session history is navigated back to check whether bfcache is
+ * enabled. If not, close message is sent and the opened window closes
+ * and the test ends.
+ * (2) beforeunload event listener is added to the page and history.forward()
+ * is called. The event listener should send a message to the controller
+ * page.
+ * (3) Close message is sent to close the opened window and the test finishes.
+ */
+
+ var pageshowCount = 0;
+ var gotBeforeUnload = false;
+ var bfcacheDisabled = false;
+
+
+ function executeTest() {
+ var bc = new BroadcastChannel("beforeunload_and_bfcache");
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ bc.postMessage("nextpage");
+ } else if (pageshowCount == 2) {
+ bc.postMessage("back");
+ } else if (pageshowCount == 3) {
+ if (!event.data.persisted) {
+ ok(true, "BFCache not enabled");
+ bfcacheDisabled = true;
+ bc.postMessage("close");
+ return;
+ }
+ bc.postMessage("forward");
+ } else if (pageshowCount == 4) {
+ ok(event.data.persisted, "Should have loaded a page from bfcache.");
+ bc.postMessage("close");
+ }
+ } else if (event.data == "beforeunload") {
+ gotBeforeUnload = true;
+ } else if (event.data == "closed") {
+ isnot(bfcacheDisabled, gotBeforeUnload,
+ "Either BFCache shouldn't be enabled or a beforeunload event should have been fired.");
+ bc.close();
+ SimpleTest.finish();
+ }
+ };
+
+ function runTest() {
+ SpecialPowers.pushPrefEnv({"set": [["docshell.shistory.bfcache.allow_unload_listeners", false]]},
+ function() {
+ window.open("file_beforeunload_and_bfcache.html", "", "noopener");
+ }
+ );
+ }
+ runTest();
+ }
+
+ if (isXOrigin) {
+ // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+ // Acquire storage access permission here so that the BroadcastChannel used to
+ // communicate with the opened windows works in xorigin tests. Otherwise,
+ // the iframe containing this page is isolated from first-party storage access,
+ // which isolates BroadcastChannel communication.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => {
+ SpecialPowers.wrap(document).requestStorageAccess().then(() => {
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]]
+ }).then(() => {
+ executeTest();
+ });
+ });
+ });
+ } else {
+ executeTest();
+ }
+
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_blockBFCache.html b/docshell/test/navigation/test_blockBFCache.html
new file mode 100644
index 0000000000..5cfbdb38f7
--- /dev/null
+++ b/docshell/test/navigation/test_blockBFCache.html
@@ -0,0 +1,294 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Blocking pages from entering BFCache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="">
+<script>
+
+const getUserMediaPrefs = {
+ set: [
+ ["media.devices.insecure.enabled", true],
+ ["media.getusermedia.insecure.enabled", true],
+ ["media.navigator.permission.disabled", true],
+ ],
+};
+const msePrefs = {
+ set: [
+ ["media.mediasource.enabled", true],
+ ["media.audio-max-decode-error", 0],
+ ["media.video-max-decode-error", 0],
+ ]
+};
+
+const blockBFCacheTests = [
+ {
+ name: "Request",
+ test: () => {
+ return new Promise((resolve) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "slow.sjs");
+ xhr.addEventListener("progress", () => { resolve(xhr); }, { once: true });
+ xhr.send();
+ });
+ },
+ },
+ {
+ name: "Background request",
+ test: () => {
+ return new Promise((resolve) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "slow.sjs");
+ xhr.addEventListener("readystatechange", () => { if (xhr.readyState == xhr.HEADERS_RECEIVED) resolve(xhr); });
+ xhr.send();
+ });
+ },
+ },
+ {
+ name: "getUserMedia",
+ prefs: getUserMediaPrefs,
+ test: () => {
+ return navigator.mediaDevices.getUserMedia({ audio: true, fake: true });
+ },
+ },
+ {
+ name: "RTCPeerConnection",
+ test: () => {
+ let pc = new RTCPeerConnection();
+ return pc.createOffer();
+ },
+ },
+ {
+ name: "MSE",
+ prefs: msePrefs,
+ test: () => {
+ const ms = new MediaSource();
+ const el = document.createElement("video");
+ el.src = URL.createObjectURL(ms);
+ el.preload = "auto";
+ return el;
+ },
+ },
+ {
+ name: "WebSpeech",
+ test: () => {
+ return new Promise((resolve) => {
+ const utterance = new SpeechSynthesisUtterance('bfcache');
+ utterance.lang = 'it-IT-noend';
+ utterance.addEventListener('start', () => { resolve(utterance); })
+ speechSynthesis.speak(utterance);
+ });
+ },
+ },
+ {
+ name: "WebVR",
+ prefs: {
+ set: [
+ ["dom.vr.test.enabled", true],
+ ["dom.vr.puppet.enabled", true],
+ ["dom.vr.require-gesture", false],
+ ],
+ },
+ test: () => {
+ return navigator.requestVRServiceTest();
+ }
+ },
+];
+
+if (SpecialPowers.effectiveIsolationStrategy() == SpecialPowers.ISOLATION_STRATEGY.IsolateEverything) {
+ blockBFCacheTests.push({
+ name: "Loading OOP iframe",
+ test: () => {
+ return new Promise((resolve) => {
+ const el = document.body.appendChild(document.createElement("iframe"));
+ el.id = "frame";
+ addEventListener("message", ({ data }) => {
+ if (data == "onload") {
+ resolve();
+ }
+ });
+ el.src = "https://example.com/tests/docshell/test/navigation/iframe_slow_onload.html";
+ });
+ },
+ waitForDone: () => {
+ SimpleTest.requestFlakyTimeout("Test has a loop in an onload handler that runs for 5000ms, we need to make sure the loop is done before moving to the next test.");
+ return new Promise(resolve => {
+ setTimeout(resolve, 5000);
+ });
+ },
+ });
+}
+
+const dontBlockBFCacheTests = [
+ {
+ name: "getUserMedia",
+ prefs: getUserMediaPrefs,
+ test: () => {
+ return navigator.mediaDevices.getUserMedia({ video: true, fake: true }).then(stream => {
+ stream.getTracks().forEach(track => track.stop());
+ return stream;
+ });
+ },
+ },
+/*
+ Disabled because MediaKeys rely on being destroyed by the CC before they
+ notify their window, so the test would intermittently fail depending on
+ when the CC runs.
+
+ {
+ name: "MSE",
+ prefs: msePrefs,
+ test: () => {
+ return new Promise((resolve) => {
+ const ms = new MediaSource();
+ const el = document.createElement("video");
+ ms.addEventListener("sourceopen", () => { resolve(el) }, { once: true });
+ el.src = URL.createObjectURL(ms);
+ el.preload = "auto";
+ }).then(el => {
+ el.src = "";
+ return el;
+ });
+ },
+ },
+*/
+];
+
+
+
+function executeTest() {
+
+ let bc = new BroadcastChannel("bfcache_blocking");
+
+ function promiseMessage(type) {
+ return new Promise((resolve, reject) => {
+ bc.addEventListener("message", (e) => {
+ if (e.data.type == type) {
+ resolve(e.data);
+ }
+ }, { once: true });
+ });
+ }
+
+ function promisePageShow(shouldBePersisted) {
+ return promiseMessage("pageshow").then(data => data.persisted == shouldBePersisted);
+ }
+
+ function promisePageShowFromBFCache(e) {
+ return promisePageShow(true);
+ }
+
+ function promisePageShowNotFromBFCache(e) {
+ return promisePageShow(false);
+ }
+
+ function runTests(testArray, shouldBlockBFCache) {
+ for (const { name, prefs = {}, test, waitForDone } of testArray) {
+ add_task(async function() {
+ await SpecialPowers.pushPrefEnv(prefs, async function() {
+ // Load a mostly blank page that we can communicate with over
+ // BroadcastChannel (though it will close the BroadcastChannel after
+ // receiving the next "load" message, to avoid blocking BFCache).
+ let loaded = promisePageShowNotFromBFCache();
+ window.open("file_blockBFCache.html", "", "noopener");
+ await loaded;
+
+ // Load the same page with a different URL.
+ loaded = promisePageShowNotFromBFCache();
+ bc.postMessage({ message: "load", url: `file_blockBFCache.html?${name}_${shouldBlockBFCache}` });
+ await loaded;
+
+ // Run test script in the second page.
+ bc.postMessage({ message: "runScript", fun: test.toString() });
+ await promiseMessage("runScriptDone");
+
+ // Go back to the first page (this should just come from the BFCache).
+ let goneBack = promisePageShowFromBFCache();
+ bc.postMessage({ message: "back" });
+ await goneBack;
+
+ // Go forward again to the second page and check that it does/doesn't come
+ // from the BFCache.
+ let goneForward = promisePageShow(!shouldBlockBFCache);
+ bc.postMessage({ message: "forward" });
+ let result = await goneForward;
+ ok(result, `Page ${shouldBlockBFCache ? "should" : "should not"} have been blocked from going into the BFCache (${name})`);
+
+ // If the test will keep running after navigation, then we need to make
+ // sure it's completely done before moving to the next test, to avoid
+ // interfering with any following tests. If waitForDone is defined then
+ // it'll return a Promise that we can use to wait for the end of the
+ // test.
+ if (waitForDone) {
+ await waitForDone();
+ }
+
+ // Do a similar test, but replace the bfcache test page with a new page,
+ // not a page coming from the session history.
+
+ // Load the same page with a different URL.
+ loaded = promisePageShowNotFromBFCache();
+ bc.postMessage({ message: "load", url: `file_blockBFCache.html?p2_${name}_${shouldBlockBFCache}` });
+ await loaded;
+
+ // Run the test script.
+ bc.postMessage({ message: "runScript", fun: test.toString() });
+ await promiseMessage("runScriptDone");
+
+ // Load a new page.
+ loaded = promisePageShowNotFromBFCache();
+ bc.postMessage({ message: "load", url: "file_blockBFCache.html" });
+ await loaded;
+
+ // Go back to the previous page and check that it does/doesn't come
+ // from the BFCache.
+ goneBack = promisePageShow(!shouldBlockBFCache);
+ bc.postMessage({ message: "back" });
+ result = await goneBack;
+ ok(result, `Page ${shouldBlockBFCache ? "should" : "should not"} have been blocked from going into the BFCache (${name})`);
+
+ if (waitForDone) {
+ await waitForDone();
+ }
+
+ bc.postMessage({ message: "close" });
+
+ SpecialPowers.popPrefEnv();
+ });
+ });
+ }
+ }
+
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ runTests(blockBFCacheTests, true);
+ runTests(dontBlockBFCacheTests, false);
+ });
+}
+
+if (isXOrigin) {
+ // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+ // Acquire storage access permission here so that the BroadcastChannel used to
+ // communicate with the opened windows works in xorigin tests. Otherwise,
+ // the iframe containing this page is isolated from first-party storage access,
+ // which isolates BroadcastChannel communication.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => {
+ SpecialPowers.wrap(document).requestStorageAccess().then(() => {
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]]
+ }).then(() => {
+ executeTest();
+ });
+ });
+ });
+} else {
+ executeTest();
+}
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1300461.html b/docshell/test/navigation/test_bug1300461.html
new file mode 100644
index 0000000000..22783c07c2
--- /dev/null
+++ b/docshell/test/navigation/test_bug1300461.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1300461</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1300461">Mozilla Bug 1300461</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+
+ let chromeScript = null;
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ function doSend(message, fn) {
+ try {
+ sendAsyncMessage(message, {success: true, value: fn()});
+ } catch(_) {
+ sendAsyncMessage(message, {success: false});
+ }
+ }
+
+ addMessageListener("requestedIndex", (id) => {
+ doSend("requestedIndex", () => {
+ let shistory = BrowsingContext.get(id).top.sessionHistory;
+ return shistory.requestedIndex;
+ })
+ });
+ });
+ }
+
+ async function getSHRequestedIndex(browsingContextId) {
+ let p = chromeScript.promiseOneMessage("requestedIndex");
+ chromeScript.sendAsyncMessage("requestedIndex", browsingContextId);
+ let result = await p;
+ ok(result.success, "Got requested index from parent");
+ return result.value;
+ }
+
+ var testCount = 0;
+
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_bug1300461.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ if (chromeScript) {
+ chromeScript.destroy();
+ }
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1326251.html b/docshell/test/navigation/test_bug1326251.html
new file mode 100644
index 0000000000..3c951729e6
--- /dev/null
+++ b/docshell/test/navigation/test_bug1326251.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1326251</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1326251">Mozilla Bug 1326251</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+
+ var testCount = 0;
+
+ var bc = new BroadcastChannel("file_bug1326251");
+ bc.onmessage = function(event) {
+ if (event.data == "requestNextTest") {
+ bc.postMessage({ nextTest: testCount++ });
+ } else if (event.data.type == "is") {
+ is(event.data.value1, event.data.value2, event.data.message);
+ } else if (event.data.type == "ok") {
+ ok(event.data.value, event.data.message);
+ } else if (event.data == "finishTest") {
+ SimpleTest.finish();
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ window.open("file_bug1326251.html", "", "width=360,height=480,noopener");
+ });
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1364364.html b/docshell/test/navigation/test_bug1364364.html
new file mode 100644
index 0000000000..90d2009df4
--- /dev/null
+++ b/docshell/test/navigation/test_bug1364364.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1364364
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1364364</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1364364 */
+ let testWin, testDoc;
+ async function test() {
+ SimpleTest.waitForExplicitFinish();
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ // This test relies on the possibility to modify the bfcached document.
+ // The new implementation is more restricted and thus works only
+ // when the bfcached browsing context is the only top level one
+ // in the browsing context group.
+ ok(true, "This test is for the old bfcache implementation only.");
+ SimpleTest.finish();
+ return;
+ }
+ testWin = window.open("file_bug1364364-1.html");
+ await waitForLoad(testWin);
+ testDoc = testWin.document;
+
+ // file_bug1364364-1.html will load a few dynamic iframes and then navigate
+ // top browsing context to file_bug1364364-2.html, which will postMessage
+ // back.
+ }
+
+ function waitForLoad(win) {
+ return new Promise(r => win.addEventListener("load", r, { once: true}));
+ }
+
+ window.addEventListener("message", async function(msg) {
+ if (msg.data == "navigation-done") {
+ is(testWin.history.length, 6, "check history.length");
+
+ // Modify a document in bfcache should cause the cache being dropped tho
+ // RemoveFromBFCacheAsync.
+ testDoc.querySelector("#content").textContent = "modified";
+ await new Promise(r => setTimeout(r, 0));
+
+ is(testWin.history.length, 2, "check history.length after bfcache dropped");
+ testWin.close();
+ SimpleTest.finish();
+ }
+ });
+
+ </script>
+</head>
+<body onload="test();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1364364">Mozilla Bug 1364364</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1375833.html b/docshell/test/navigation/test_bug1375833.html
new file mode 100644
index 0000000000..c2a7750a4e
--- /dev/null
+++ b/docshell/test/navigation/test_bug1375833.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1375833
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1375833</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ /**
+ * Test for Bug 1375833. It tests for 2 things in a normal reload -
+ * 1. Static frame history should not be dropped.
+ * 2. In a reload, docshell would parse the reloaded root document and
+ * genearate new child docshells, and then use the child offset
+ */
+
+ let testWin = window.open("file_bug1375833.html");
+ let count = 0;
+ let webNav, shistory;
+ let frameDocShellId;
+ let chromeScript = null;
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ function doSend(message, fn) {
+ try {
+ sendAsyncMessage(message, {success: true, value: fn()});
+ } catch(_) {
+ sendAsyncMessage(message, {success: false});
+ }
+ }
+
+ addMessageListener("test1", id => {
+ doSend("test1", () => {
+ let sessionHistory = BrowsingContext.get(id).top.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(sessionHistory.index);
+ let frameEntry = entry.GetChildAt(0);
+ return String(frameEntry.docshellID);
+ })
+ });
+ });
+ }
+
+ window.addEventListener("message", async e => {
+ switch (count++) {
+ case 0:
+ ok(e.data.endsWith("file_bug1375833-frame2.html"), "check location");
+
+ webNav = SpecialPowers.wrap(testWin)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsIWebNavigation);
+ shistory = webNav.sessionHistory;
+ is(shistory.count, 2, "check history length");
+ is(shistory.index, 1, "check history index");
+
+ frameDocShellId = String(getFrameDocShell().historyID);
+ ok(frameDocShellId, "sanity check for docshell ID");
+
+ testWin.location.reload();
+ break;
+ case 1:
+ ok(e.data.endsWith("file_bug1375833-frame2.html"), "check location");
+ is(shistory.count, 4, "check history length");
+ is(shistory.index, 3, "check history index");
+
+ let newFrameDocShellId = String(getFrameDocShell().historyID);
+ ok(newFrameDocShellId, "sanity check for docshell ID");
+ is(newFrameDocShellId, frameDocShellId, "check docshell ID remains after reload");
+
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let entry = shistory.legacySHistory.getEntryAtIndex(shistory.index);
+ let frameEntry = entry.GetChildAt(0);
+ is(String(frameEntry.docshellID), frameDocShellId, "check newly added shentry uses the same docshell ID");
+ } else {
+ let p = chromeScript.promiseOneMessage("test1");
+ chromeScript.sendAsyncMessage("test1", SpecialPowers.wrap(testWin).browsingContext.id);
+ let result = await p;
+ ok(result.success, "legacySHistory worked around ok");
+ is(result.value, frameDocShellId, "check newly added shentry uses the same docshell ID");
+ }
+
+ webNav.goBack();
+ break;
+ case 2:
+ ok(e.data.endsWith("file_bug1375833-frame1.html"), "check location");
+ is(shistory.count, 4, "check history length");
+ is(shistory.index, 2, "check history index");
+
+ webNav.goBack();
+ break;
+ case 3:
+ ok(e.data.endsWith("file_bug1375833-frame2.html"), "check location");
+ is(shistory.count, 4, "check history length");
+ is(shistory.index, 1, "check history index");
+
+ webNav.goBack();
+ break;
+ case 4:
+ ok(e.data.endsWith("file_bug1375833-frame1.html"), "check location");
+ is(shistory.count, 4, "check history length");
+ is(shistory.index, 0, "check history index");
+
+ if (chromeScript) {
+ chromeScript.destroy();
+ }
+ testWin.close();
+ SimpleTest.finish();
+ }
+ });
+
+ function getFrameDocShell() {
+ return SpecialPowers.wrap(testWin.window[0]).docShell;
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1375833">Mozilla Bug 1375833</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1379762.html b/docshell/test/navigation/test_bug1379762.html
new file mode 100644
index 0000000000..eda3b539a5
--- /dev/null
+++ b/docshell/test/navigation/test_bug1379762.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1379762</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1379762">Mozilla Bug 1379762</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ /**
+ * - This page opens new window
+ * - new window sends 'init' msg
+ * - onload() in new window sends 'increment_loadCount' msg
+ * - onpageshow() in new window sends 'increment_testCount' msg
+ * - This page sends 'forward_back' msg
+ * - onpageshow() in new window 'increment_testCount'
+ * - This page sends 'finish_test' msg
+ * - onpageshow() in new window sends 'finished' msg
+ */
+ var testCount = 0; // Used by the test files.
+ var loadCount = 0;
+ var goneBack = false;
+ var bc = new BroadcastChannel("bug1379762");
+ bc.onmessage = (messageEvent) => {
+ let message = messageEvent.data;
+ if (message == "init") {
+ is(testCount, 0, "new window should only be loaded once; otherwise the loadCount variable makes no sense");
+ } else if (message == "increment_loadCount") {
+ loadCount++;
+ is(loadCount, 1, "Should only get one load")
+ } else if (message == 'increment_testCount') {
+ testCount++;
+ if (testCount == 1) {
+ bc.postMessage("forward_back");
+ goneBack = true;
+ } else if (testCount == 2) {
+ ok(goneBack, "We had a chance to navigate backwards and forwards in the new window to test BFCache");
+ bc.postMessage("finish_test");
+ }
+ } else if (message == "finished") {
+ bc.close();
+ SimpleTest.finish();
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ window.open("file_bug1379762-1.html", "", "width=360,height=480,noopener");
+ });
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug13871.html b/docshell/test/navigation/test_bug13871.html
new file mode 100644
index 0000000000..0532bc7b56
--- /dev/null
+++ b/docshell/test/navigation/test_bug13871.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+async function runTest() {
+ navigateByLocation(window0.frames[0]);
+ navigateByOpen("window1_child0");
+ navigateByForm("window2_child0");
+ navigateByHyperlink("window3_child0");
+
+ await waitForFinishedFrames(4);
+
+ isInaccessible(window0.frames[0], "Should not be able to navigate off-domain frame by setting location.");
+ isInaccessible(window1.frames[0], "Should not be able to navigate off-domain frame by calling window.open.");
+ isInaccessible(window2.frames[0], "Should not be able to navigate off-domain frame by submitting form.");
+ isInaccessible(window3.frames[0], "Should not be able to navigate off-domain frame by targeted hyperlink.");
+
+ window0.close();
+ window1.close();
+ window2.close();
+ window3.close();
+
+ await cleanupWindows();
+ SimpleTest.finish();
+}
+
+// Because our open()'d windows are cross-origin, we can't wait for onload.
+// We instead wait for a postMessage from parent.html.
+var windows = new Map();
+addEventListener("message", function windowLoaded(evt) {
+ // Because window.open spins the event loop in order to open new windows,
+ // we might receive the "ready" message before we call waitForLoad.
+ // In that case, windows won't contain evt.source and we just note that the
+ // window is ready. Otherwise, windows contains the "resolve" function for
+ // that window's promise and we just have to call it.
+ if (windows.has(evt.source)) {
+ windows.get(evt.source)();
+ } else {
+ windows.set(evt.source, true);
+ }
+});
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window0 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window0", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window1 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window1", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window2 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window2", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window3 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window3", "width=10,height=10");
+
+function waitForLoad(w) {
+ return new Promise(function(resolve, reject) {
+ // If we already got the "ready" message, resolve immediately.
+ if (windows.has(w)) {
+ resolve();
+ } else {
+ windows.set(w, resolve);
+ }
+ });
+}
+
+Promise.all([ waitForLoad(window0),
+ waitForLoad(window1),
+ waitForLoad(window2),
+ waitForLoad(window3) ])
+ .then(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=13871">Mozilla Bug 13871</a>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug145971.html b/docshell/test/navigation/test_bug145971.html
new file mode 100644
index 0000000000..ffad27a9c3
--- /dev/null
+++ b/docshell/test/navigation/test_bug145971.html
@@ -0,0 +1,29 @@
+<html>
+ <head>
+ <script>
+ let pass = false;
+ let initialLoad = false;
+ var bc = new BroadcastChannel("bug145971");
+ function checkNavigationTypeEquals2() {
+ if (performance.navigation.type == 2) {
+ pass = true;
+ }
+ testDone();
+ }
+
+ function testDone() {
+ bc.postMessage({result: pass});
+ bc.close();
+ window.close();
+ }
+
+ function test() {
+ window.onpageshow = checkNavigationTypeEquals2;
+ window.location.href = 'goback.html';
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(test, 0);">
+ Testing bug 145971.
+ </body>
+</html>
diff --git a/docshell/test/navigation/test_bug1536471.html b/docshell/test/navigation/test_bug1536471.html
new file mode 100644
index 0000000000..f37aedba21
--- /dev/null
+++ b/docshell/test/navigation/test_bug1536471.html
@@ -0,0 +1,75 @@
+
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1536471
+ -->
+<head>
+ <title>Test for Bug 1536471</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+
+ let testWin;
+ async function test() {
+ // Open a new tab and load a document with an iframe inside
+ testWin = window.open("file_bug1536471.html");
+ await waitForLoad();
+ var iframe = testWin.document.getElementById("staticFrame");
+ is(testWin.history.length, 1, "Checking the number of session history entries when there is only one iframe");
+
+ // Navigate the iframe to different pages
+ await loadUriInFrame(iframe, "frame1.html");
+ is(testWin.history.length, 2, "Checking the number of session history entries after having navigated a single iframe 1 time");
+ await loadUriInFrame(iframe, "frame2.html");
+ is(testWin.history.length, 3, "Checking the number of session history entries after having navigated a single iframe 2 times");
+ await loadUriInFrame(iframe, "frame3.html");
+ is(testWin.history.length, 4, "Checking the number of session history entries after having navigated a single iframe 3 times");
+
+ // Reload the top document
+ testWin.location.reload(true);
+ await waitForLoad();
+ is(testWin.history.length, 1, "Checking the number of session history entries after reloading the top document");
+
+ testWin.close();
+ SimpleTest.finish();
+ }
+
+ async function waitForLoad() {
+ await new Promise(resolve => {
+ window.bodyOnLoad = function() {
+ setTimeout(resolve, 0);
+ window.bodyOnLoad = undefined;
+ };
+ });
+ }
+
+ async function iframeOnload(frame) {
+ return new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ setTimeout(resolve, 0);
+ }, {once: true});
+ });
+ }
+
+ async function loadUriInFrame(frame, uri) {
+ let onloadPromise = iframeOnload(frame);
+ frame.src = uri;
+ await onloadPromise;
+ }
+ </script>
+</head>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1536471">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<body onload="test()">
+</body>
+</html>
+
diff --git a/docshell/test/navigation/test_bug1583110.html b/docshell/test/navigation/test_bug1583110.html
new file mode 100644
index 0000000000..f1c1b65e4d
--- /dev/null
+++ b/docshell/test/navigation/test_bug1583110.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>test bug 1583110</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ var bc = new BroadcastChannel("bug1583110");
+ var pageshowCount = 0;
+ bc.onmessage = function(event) {
+ ok(event.data.type == "pageshow");
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ bc.postMessage("loadnext");
+ } else if (pageshowCount == 2) {
+ bc.postMessage("back");
+ } else {
+ ok(event.data.persisted, "Should have persisted the first page");
+ bc.close();
+ SimpleTest.finish();
+ }
+ }
+
+ function test() {
+ window.open("file_bug1583110.html", "", "noopener");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1609475.html b/docshell/test/navigation/test_bug1609475.html
new file mode 100644
index 0000000000..4dbe7d17d6
--- /dev/null
+++ b/docshell/test/navigation/test_bug1609475.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1609475</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1609475">Mozilla Bug 1609475</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_bug1609475.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1699721.html b/docshell/test/navigation/test_bug1699721.html
new file mode 100644
index 0000000000..c6ae8e88d3
--- /dev/null
+++ b/docshell/test/navigation/test_bug1699721.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script type="text/javascript">
+ add_task(async function() {
+ // Induce a process switching behavior for example.com
+ // with isolateHighValue isolation strategy
+ // (because we test specifically process switching behavior here)
+ await SpecialPowers.pushPermissions([
+ {
+ type: "highValueCOOP",
+ allow: SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION,
+ context: "https://example.com",
+ }
+ ]);
+
+ let popup = window.open("blank.html");
+
+ info("opened popup");
+ await new Promise(resolve => {
+ popup.addEventListener("load", resolve, { once: true });
+ });
+
+ info("popup blank.html loaded");
+ let tell_opener = new URL("file_tell_opener.html", location.href);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let xorigin_url = new URL(tell_opener.pathname, "https://example.com");
+
+ let resolveStartedUnload;
+ let startedUnload = new Promise(resolve => {
+ resolveStartedUnload = resolve;
+ });
+ let didFinishUnload = false;
+
+ let finishUnload = false;
+ popup.addEventListener("unload", function() {
+ resolveStartedUnload();
+ try {
+ // Spin a nested event loop in unload until we set `finishUnload`.
+ SpecialPowers.Services.tm.spinEventLoopUntil(
+ "Test(test_switch_back_nested.html)", () => finishUnload);
+ } finally {
+ info("exiting from unload nested event loop...");
+ didFinishUnload = true;
+ }
+ });
+
+ info("wait for message from popup");
+ let messagePromise = new Promise(resolve => {
+ addEventListener("message", evt => {
+ resolve();
+ }, { once: true });
+ });
+ popup.location = xorigin_url.href;
+ await messagePromise;
+
+ info("popup loaded, ensuring we're in unload");
+ await startedUnload;
+ is(didFinishUnload, false, "unload shouldn't have finished");
+
+ let switchStarted = SpecialPowers.spawnChrome([], async () => {
+ await new Promise(resolve => {
+ async function observer(subject, topic) {
+ is(topic, "http-on-examine-response");
+
+ let uri = subject.QueryInterface(Ci.nsIChannel).URI;
+ if (!uri.filePath.endsWith("file_tell_opener.html")) {
+ return;
+ }
+
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+
+ // spin the event loop a few times to ensure we resolve after the process switch
+ for (let i = 0; i < 10; ++i) {
+ await new Promise(res => Services.tm.dispatchToMainThread(res));
+ }
+
+ info("resolving!");
+ resolve();
+ }
+ Services.obs.addObserver(observer, "http-on-examine-response");
+ });
+ });
+
+ info("Navigating back to the current process");
+ await SpecialPowers.spawn(popup, [tell_opener.href], (href) => {
+ content.location.href = href;
+ });
+
+ let messagePromise2 = new Promise(resolve => {
+ addEventListener("message", evt => {
+ resolve();
+ }, { once: true });
+ });
+
+ info("Waiting for the process switch to start");
+ await switchStarted;
+
+ // Finish unloading, and wait for the unload to complete
+ is(didFinishUnload, false, "unload shouldn't be finished");
+ finishUnload = true;
+ await new Promise(resolve => setTimeout(resolve, 0));
+ is(didFinishUnload, true, "unload should be finished");
+
+ info("waiting for navigation to complete");
+ await messagePromise2;
+
+ info("closing popup");
+ popup.close();
+
+ ok(true, "Didn't crash");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1706090.html b/docshell/test/navigation/test_bug1706090.html
new file mode 100644
index 0000000000..293148b9c6
--- /dev/null
+++ b/docshell/test/navigation/test_bug1706090.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1706090</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ var bc = new BroadcastChannel("bug1706090");
+ var pageshowCount = 0;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ is(event.data.persisted, false, "Shouldn't have persisted the initial load.");
+ bc.postMessage("sameOrigin");
+ } else if (pageshowCount == 2) {
+ bc.postMessage("back");
+ } else if (pageshowCount == 3) {
+ is(event.data.persisted, false, "Shouldn't have persisted same origin load.");
+ bc.postMessage("crossOrigin");
+ } else if (pageshowCount == 4) {
+ is(event.data.persisted, true, "Should have persisted cross origin load.");
+ bc.postMessage("sameSite");
+ } else if (pageshowCount == 5) {
+ is(event.data.persisted, false, "Shouldn't have persisted same site load.");
+ bc.postMessage("close");
+ }
+ } else if (event.data == "closed") {
+ bc.close();
+ SimpleTest.finish();
+ }
+ };
+
+ function runTest() {
+ SpecialPowers.pushPrefEnv({set: [["docshell.shistory.bfcache.allow_unload_listeners", true]]}, () => {
+ window.open("file_bug1706090.html", "", "noopener");
+ });
+ }
+ </script>
+</head>
+<body onload="runTest()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1745638.html b/docshell/test/navigation/test_bug1745638.html
new file mode 100644
index 0000000000..594c464da3
--- /dev/null
+++ b/docshell/test/navigation/test_bug1745638.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>bug 1745638</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ // This test triggers an assertion in the old session history
+ // implementation.
+ SimpleTest.expectAssertions(0, 1);
+
+ SimpleTest.waitForExplicitFinish();
+ var testWindow;
+ var loadCount = 0;
+ function test() {
+ testWindow = window.open("file_bug1745638.html");
+ }
+
+ function pageLoaded() {
+ ++loadCount;
+ is(testWindow.document.getElementById('testFrame').contentDocument.body.innerHTML,
+ "passed",
+ "Iframe's textual content should be 'passed'.");
+ if (loadCount == 1) {
+ testWindow.history.go(0);
+ } else {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+ }
+
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1747019.html b/docshell/test/navigation/test_bug1747019.html
new file mode 100644
index 0000000000..c7995737df
--- /dev/null
+++ b/docshell/test/navigation/test_bug1747019.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test session history and caching</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ var win;
+ var loadCount = 0;
+ var initialContent;
+ // The test loads first a page in a new window, then using
+ // form submission loads another page and then using form submission
+ // again loads third page. That page triggers history.go(-1).
+ // The second page is loaded now again and should have the same content
+ // as it had before.
+ function test() {
+ win = window.open("cache_control_max_age_3600.sjs?initial");
+ window.onmessage = (e) => {
+ is(e.data, "loaded", "Should get load message 'loaded'");
+ ++loadCount;
+ if (loadCount == 1) {
+ win.document.forms[0].submit();
+ } else if (loadCount == 2) {
+ initialContent = win.document.body.textContent;
+ info("The initial content is [" + initialContent + "].");
+ win.document.forms[0].submit();
+ } else if (loadCount == 3) {
+ let newContent = win.document.body.textContent;
+ info("The new content is [" + newContent + "].");
+ win.close();
+ is(initialContent, newContent, "Should have loaded the page from cache.");
+ SimpleTest.finish();
+ } else {
+ ok(false, "Unexpected load count.");
+ }
+ }
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1750973.html b/docshell/test/navigation/test_bug1750973.html
new file mode 100644
index 0000000000..9f87075b90
--- /dev/null
+++ b/docshell/test/navigation/test_bug1750973.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>The layout state restoration when reframing the root element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function test() {
+ window.open("file_bug1750973.html");
+ }
+ </script>
+</head>
+<body onload="setTimeout(test)">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug1758664.html b/docshell/test/navigation/test_bug1758664.html
new file mode 100644
index 0000000000..662242e44a
--- /dev/null
+++ b/docshell/test/navigation/test_bug1758664.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1758664</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ function test() {
+ window.open("file_bug1758664.html");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug270414.html b/docshell/test/navigation/test_bug270414.html
new file mode 100644
index 0000000000..1d2829750d
--- /dev/null
+++ b/docshell/test/navigation/test_bug270414.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+/* eslint-disable no-useless-concat */
+/* global window0:true, window1:true, window2:true, window3:true */
+var headerHTML = "<html><head>" +
+ "<script src='/tests/SimpleTest/EventUtils.js'><\/script>" +
+ "<script src='NavigationUtils.js'><\/script>" +
+ "</head><body>";
+var footerHTML = "</body></html>";
+
+function testChild0() {
+ if (!window.window0) {
+ window0 = window.open("", "window0", "width=10,height=10");
+ window0.document.open();
+ window0.document.write(headerHTML);
+ window0.document.write("<script>navigateByLocation(opener.frames[0])<\/script>");
+ window0.document.write(footerHTML);
+ window0.document.close();
+ }
+}
+
+function testChild1() {
+ if (!window.window1) {
+ window1 = window.open("", "window1", "width=10,height=10");
+ window1.document.open();
+ window1.document.write(headerHTML);
+ window1.document.write("<script>navigateByOpen('child1');<\/script>");
+ window1.document.write(footerHTML);
+ window1.document.close();
+ }
+}
+
+function testChild2() {
+ if (!window.window2) {
+ window2 = window.open("", "window2", "width=10,height=10");
+ window2.document.open();
+ window2.document.write(headerHTML);
+ window2.document.write("<script>navigateByForm('child2');<\/script>");
+ window2.document.write(footerHTML);
+ window2.document.close();
+ }
+}
+
+function testChild3() {
+ if (!window.window3) {
+ window3 = window.open("", "window3", "width=10,height=10");
+ window3.document.open();
+ window3.document.write(headerHTML);
+ window3.document.write("<script>navigateByHyperlink('child3');<\/script>");
+ window3.document.write(footerHTML);
+ window3.document.close();
+ }
+}
+
+add_task(async function() {
+ await waitForFinishedFrames(4);
+
+ await isNavigated(frames[0], "Should be able to navigate on-domain opener's children by setting location.");
+ await isNavigated(frames[1], "Should be able to navigate on-domain opener's children by calling window.open.");
+ await isNavigated(frames[2], "Should be able to navigate on-domain opener's children by submitting form.");
+ await isNavigated(frames[3], "Should be able to navigate on-domain opener's children by targeted hyperlink.");
+
+ window0.close();
+ window1.close();
+ window2.close();
+ window3.close();
+
+ await cleanupWindows();
+});
+
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=270414">Mozilla Bug 270414</a>
+<div id="frames">
+<iframe onload="testChild0();" name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe onload="testChild1();" name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe onload="testChild2();" name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe onload="testChild3();" name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+</div>
+<pre id="test">
+<script type="text/javascript">
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug278916.html b/docshell/test/navigation/test_bug278916.html
new file mode 100644
index 0000000000..9e2335721e
--- /dev/null
+++ b/docshell/test/navigation/test_bug278916.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+<script>
+window.onload = async function() {
+ document.getElementById("link0").href = target_url;
+ sendMouseEvent({type: "click"}, "link0");
+
+ await waitForFinishedFrames(1);
+
+ var array_of_frames = await getFramesByName("window0");
+ is(array_of_frames.length, 1, "Should only open one window using a fancy hyperlink.");
+
+ for (var i = 0; i < array_of_frames.length; ++i)
+ array_of_frames[i].close();
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=278916">Mozilla Bug 278916</a>
+<div id="links">
+<a id="link0" target="window0" onclick="window.open('', 'window0', 'width=10,height=10');">This is a fancy hyperlink</a>
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug279495.html b/docshell/test/navigation/test_bug279495.html
new file mode 100644
index 0000000000..245ed14ed4
--- /dev/null
+++ b/docshell/test/navigation/test_bug279495.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+<script>
+window.onload = async function() {
+ document.getElementById("link0").href = target_url;
+ document.getElementById("link1").href = target_url;
+
+ sendMouseEvent({type: "click"}, "link0");
+ sendMouseEvent({type: "click"}, "link1");
+
+ await waitForFinishedFrames(2);
+ await countAndClose("window0", 1);
+ await countAndClose("window1", 1);
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+
+async function countAndClose(name, expected_count) {
+ var array_of_frames = await getFramesByName(name);
+ is(array_of_frames.length, expected_count,
+ "Should only open " + expected_count +
+ " window(s) with name " + name + " using a fancy hyperlink.");
+}
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=279495">Mozilla Bug 279495</a>
+<div id="links">
+<a id="link0" target="window0" onclick="window.open('blank.html', 'window0', 'width=10,height=10');">This is a fancy hyperlink</a>
+<a id="link1" target="window1" onclick="window.open('https://test1.example.org/tests/docshell/test/navigation/blank.html', 'window1', 'width=10,height=10');">This is a fancy hyperlink</a>
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug344861.html b/docshell/test/navigation/test_bug344861.html
new file mode 100644
index 0000000000..5ab8809d27
--- /dev/null
+++ b/docshell/test/navigation/test_bug344861.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=344861
+-->
+<head>
+ <title>Test for Bug 344861</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=344861">Mozilla Bug 344861</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 344861 */
+SimpleTest.waitForExplicitFinish();
+
+var newwindow = window.open("/", "testwindow", "width=200,height=200");
+newwindow.onload = function() {
+ is(newwindow.innerHeight, 200, "window.open has correct height dimensions");
+ is(newwindow.innerWidth, 200, "window.open has correct width dimensions");
+ SimpleTest.finish();
+ newwindow.close();
+};
+</script>
+</pre>
+</body>
+</html>
+
+
diff --git a/docshell/test/navigation/test_bug386782.html b/docshell/test/navigation/test_bug386782.html
new file mode 100644
index 0000000000..f6ad85f5a6
--- /dev/null
+++ b/docshell/test/navigation/test_bug386782.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=386782
+-->
+<head>
+ <title>Test for Bug 386782</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <script>
+
+ // This tests if we can load a document whose root is in designMode,
+ // edit it, navigate to a new page, navigate back, still edit, and still
+ // undo/redo. Note that this is different from the case where the
+ // designMode document is in a frame inside the window, as this means
+ // the editable region is not in the root docshell (a less complicated case).
+
+ var gTests = [
+ {
+ // <html><body><p>designModeDocument</p></body></html>
+ url: "file_bug386782_designmode.html",
+ name: "designModeNavigate",
+ onload(doc) { doc.designMode = "on"; },
+ expectedBodyBeforeEdit: "<p>designModeDocument</p>",
+ expectedBodyAfterEdit: "<p>EDITED designModeDocument</p>",
+ expectedBodyAfterSecondEdit: "<p>EDITED TWICE designModeDocument</p>",
+ },
+ {
+ // <html><body contentEditable="true"><p>contentEditable</p></body></html>
+ url: "file_bug386782_contenteditable.html",
+ name: "contentEditableNavigate",
+ expectedBodyBeforeEdit: "<p>contentEditable</p>",
+ expectedBodyAfterEdit: "EDITED <br><p>contentEditable</p>",
+ expectedBodyAfterSecondEdit: "EDITED TWICE <br><p>contentEditable</p>",
+ },
+ ];
+
+ var gTest = null;
+
+ add_task(async () => {
+ while (gTests.length) {
+ gTest = gTests.shift();
+ await runTest();
+ }
+ });
+
+ async function runTest() {
+ gTest.window = window.open(gTest.url, gTest.name, "width=500,height=500");
+ let e = await new Promise(r => window.onmessage = r);
+ is(e.data.persisted, false, "Initial load cannot be persisted");
+ if ("onload" in gTest) {
+ gTest.onload(gTest.window.document);
+ }
+ await SimpleTest.promiseFocus(gTest.window);
+
+ gTest.window.document.body.focus();
+
+ // WARNING: If the following test fails, give the setTimeout() in the onload()
+ // a bit longer; the doc hasn't had enough time to setup its editor.
+ is(gTest.window.document.body.innerHTML, gTest.expectedBodyBeforeEdit, "Is doc setup yet");
+ sendString("EDITED ", gTest.window);
+ is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterEdit, "Editing failed.");
+
+ gTest.window.location = "about:blank";
+ await new Promise(r => gTest.window.onpagehide = r);
+ // The active document is updated synchronously after "pagehide" (and
+ // its associated microtasks), so, after waiting for the next global
+ // task, gTest.window will be proxying the realm associated with the
+ // "about:blank" document.
+ // https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-the-session-history-with-the-new-page
+ await new Promise(r => setTimeout(r));
+ is(gTest.window.location.href, "about:blank", "location.href");
+ await SimpleTest.promiseFocus(gTest.window, true);
+
+ gTest.window.history.back();
+ e = await new Promise(r => window.onmessage = r);
+ // Skip the test if the page is not loaded from the bf-cache when going back.
+ if (e.data.persisted) {
+ checkStillEditable();
+ }
+ gTest.window.close();
+ }
+
+ function checkStillEditable() {
+ // Check that the contents are correct.
+ is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterEdit, "Edited contents still correct?");
+
+ // Check that we can undo/redo and the contents are correct.
+ gTest.window.document.execCommand("undo", false, null);
+ is(gTest.window.document.body.innerHTML, gTest.expectedBodyBeforeEdit, "Can we undo?");
+
+ gTest.window.document.execCommand("redo", false, null);
+ is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterEdit, "Can we redo?");
+
+ // Check that we can still edit the page.
+ gTest.window.document.body.focus();
+ sendString("TWICE ", gTest.window);
+ is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterSecondEdit, "Can we still edit?");
+ }
+
+ </script>
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386782">Mozilla Bug 386782</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 386782 */
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_bug430624.html b/docshell/test/navigation/test_bug430624.html
new file mode 100644
index 0000000000..49afb023b9
--- /dev/null
+++ b/docshell/test/navigation/test_bug430624.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=430624
+-->
+<head>
+ <title>Test for Bug 430624</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430624">Mozilla Bug 430624</a>
+<p id="display"></p>
+
+
+
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 430624 */
+
+function onLoad() {
+ window.frames[0].frameElement.onload = onReload;
+ // eslint-disable-next-line no-self-assign
+ window.frames[0].frameElement.srcdoc = window.frames[0].frameElement.srcdoc;
+}
+
+function onReload() {
+ var iframe = window.frames[0].frameElement;
+ SimpleTest.waitForFocus(doTest, iframe.contentWindow);
+ iframe.contentDocument.body.focus();
+}
+
+function doTest() {
+ var bodyElement = window.frames[0].frameElement.contentDocument.body;
+ bodyElement.focus();
+ sendString("Still ", window.frames[0].frameElement.contentWindow);
+
+ is(bodyElement.innerHTML, "Still contentEditable", "Check we're contentEditable after reload");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+
+<iframe onload="onLoad()" srcdoc="<body contenteditable>contentEditable</body>"></iframe>
+
+</body>
+</html>
+
diff --git a/docshell/test/navigation/test_bug430723.html b/docshell/test/navigation/test_bug430723.html
new file mode 100644
index 0000000000..dd85c7fb08
--- /dev/null
+++ b/docshell/test/navigation/test_bug430723.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=430723
+-->
+<head>
+ <title>Test for Bug 430723</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430723">Mozilla Bug 430723</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// <![CDATA[
+
+/** Test for Bug 430723 */
+
+var BASE_URI = "http://mochi.test:8888/tests/docshell/test/navigation/";
+var gTallRedBoxURI = BASE_URI + "redbox_bug430723.html";
+var gTallBlueBoxURI = BASE_URI + "bluebox_bug430723.html";
+
+window.onload = runTest;
+
+var testWindow;
+var testNum = 0;
+
+var smoothScrollPref = "general.smoothScroll";
+function runTest() {
+ SpecialPowers.pushPrefEnv({"set": [[smoothScrollPref, false]]}, function() {
+ testWindow = window.open(gTallRedBoxURI, "testWindow", "width=300,height=300,location=yes,scrollbars=yes");
+ });
+}
+
+var nextTest = function() {
+ testNum++;
+ switch (testNum) {
+ case 1: setTimeout(step1, 0); break;
+ case 2: setTimeout(step2, 0); break;
+ case 3: setTimeout(step3, 0); break;
+ }
+};
+
+var step1 = function() {
+ window.is(String(testWindow.location), gTallRedBoxURI, "Ensure red page loaded.");
+
+ // Navigate down and up.
+ is(testWindow.document.body.scrollTop, 0,
+ "Page1: Ensure the scrollpane is at the top before we start scrolling.");
+ testWindow.addEventListener("scroll", function() {
+ isnot(testWindow.document.body.scrollTop, 0,
+ "Page1: Ensure we can scroll down.");
+ SimpleTest.executeSoon(step1_2);
+ }, {capture: true, once: true});
+ sendKey("DOWN", testWindow);
+
+ function step1_2() {
+ testWindow.addEventListener("scroll", function() {
+ is(testWindow.document.body.scrollTop, 0,
+ "Page1: Ensure we can scroll up, back to the top.");
+
+ // Nav to blue box page. This should fire step2.
+ testWindow.location = gTallBlueBoxURI;
+ }, {capture: true, once: true});
+ sendKey("UP", testWindow);
+ }
+};
+
+
+var step2 = function() {
+ window.is(String(testWindow.location), gTallBlueBoxURI, "Ensure blue page loaded.");
+
+ // Scroll around a bit.
+ is(testWindow.document.body.scrollTop, 0,
+ "Page2: Ensure the scrollpane is at the top before we start scrolling.");
+
+ var scrollTest = function() {
+ if (++count < 2) {
+ SimpleTest.executeSoon(function() { sendKey("DOWN", testWindow); });
+ } else {
+ testWindow.removeEventListener("scroll", scrollTest, true);
+
+ isnot(testWindow.document.body.scrollTop, 0,
+ "Page2: Ensure we could scroll.");
+
+ // Navigate backwards. This should fire step3.
+ testWindow.history.back();
+ }
+ };
+
+ var count = 0;
+ testWindow.addEventListener("scroll", scrollTest, true);
+ sendKey("DOWN", testWindow);
+};
+
+var step3 = function() {
+ window.is(String(testWindow.location), gTallRedBoxURI,
+ "Ensure red page restored from history.");
+
+ // Check we can still scroll with the keys.
+ is(testWindow.document.body.scrollTop, 0,
+ "Page1Again: Ensure scroll pane at top before we scroll.");
+ testWindow.addEventListener("scroll", function() {
+ isnot(testWindow.document.body.scrollTop, 0,
+ "Page2Again: Ensure we can still scroll.");
+
+ testWindow.close();
+ window.SimpleTest.finish();
+ }, {capture: true, once: true});
+ sendKey("DOWN", testWindow);
+};
+
+SimpleTest.waitForExplicitFinish();
+
+// ]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_child.html b/docshell/test/navigation/test_child.html
new file mode 100644
index 0000000000..87237471cd
--- /dev/null
+++ b/docshell/test/navigation/test_child.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+if (!navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+window.onload = async function() {
+ navigateByLocation(frames[0]);
+ navigateByOpen("child1");
+ navigateByForm("child2");
+ navigateByHyperlink("child3");
+
+ await waitForFinishedFrames(4);
+ await isNavigated(frames[0], "Should be able to navigate off-domain child by setting location.");
+ await isNavigated(frames[1], "Should be able to navigate off-domain child by calling window.open.");
+ await isNavigated(frames[2], "Should be able to navigate off-domain child by submitting form.");
+ await isNavigated(frames[3], "Should be able to navigate off-domain child by targeted hyperlink.");
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<div id="frames">
+<iframe name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_contentpolicy_block_window.html b/docshell/test/navigation/test_contentpolicy_block_window.html
new file mode 100644
index 0000000000..7dc73e8574
--- /dev/null
+++ b/docshell/test/navigation/test_contentpolicy_block_window.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1329288
+-->
+<head>
+ <title>Test for Bug 1329288</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1329288">Mozilla Bug 1329288</a>
+
+
+<!-- have a testlink which we can use for the test to open a new window -->
+<a href="http://test1.example.org/tests/docshell/test/navigation/file_contentpolicy_block_window.html"
+ target="_blank"
+ id="testlink">This is a link</a>
+
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * The test tries to open a new window and makes sure that a registered contentPolicy
+ * gets called with the right (a non null) 'context' for the TYPE_DOCUMENT load.
+ */
+
+const Ci = SpecialPowers.Ci;
+
+var categoryManager = SpecialPowers.Services.catMan;
+var componentManager = SpecialPowers.Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+
+// Content policy / factory implementation for the test
+var policyID = SpecialPowers.wrap(SpecialPowers.Components).ID("{b80e19d0-878f-d41b-2654-194714a4115c}");
+var policyName = "@mozilla.org/testpolicy;1";
+var policy = {
+ // nsISupports implementation
+ QueryInterface(iid) {
+ iid = SpecialPowers.wrap(iid);
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsIContentPolicy))
+ return this;
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ // nsIFactory implementation
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ // nsIContentPolicy implementation
+ shouldLoad(contentLocation, loadInfo) {
+ let contentType = loadInfo.externalContentPolicyType;
+ let context = loadInfo.loadingContext;
+
+ if (SpecialPowers.wrap(contentLocation).spec !== document.getElementById("testlink").href) {
+ // not the URI we are looking for, allow the load
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ is(contentType, Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "needs to be type document load");
+ ok(context, "context is not allowed to be null");
+ ok(context.name.endsWith("test_contentpolicy_block_window.html"),
+ "context should be the current window");
+
+ // remove the policy and finish test.
+ categoryManager.deleteCategoryEntry("content-policy", policyName, false);
+
+ setTimeout(function() {
+ // Component must be unregistered delayed, otherwise other content
+ // policy will not be removed from the category correctly
+ componentManager.unregisterFactory(policyID, policy);
+ }, 0);
+
+ SimpleTest.finish();
+ return Ci.nsIContentPolicy.REJECT_REQUEST;
+ },
+
+ shouldProcess(contentLocation, loadInfo) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+};
+
+policy = SpecialPowers.wrapCallbackObject(policy);
+componentManager.registerFactory(policyID, "Test content policy", policyName, policy);
+categoryManager.addCategoryEntry("content-policy", policyName, policyName, false, true);
+
+SimpleTest.waitForExplicitFinish();
+
+// now everything is set up, let's start the test
+document.getElementById("testlink").click();
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_docshell_gotoindex.html b/docshell/test/navigation/test_docshell_gotoindex.html
new file mode 100644
index 0000000000..992c9c9dbe
--- /dev/null
+++ b/docshell/test/navigation/test_docshell_gotoindex.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1684310</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function test() {
+ /*
+ * This test is for nsIWebNavigation.gotoIndex.
+ *
+ * The test
+ * - opens a new window
+ * - loads a page there
+ * - loads another page
+ * - navigates to some fragments in the page
+ * - goes back to one of the fragments
+ * - tries to go back to the initial page.
+ */
+ window.open("file_docshell_gotoindex.html");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_dynamic_frame_forward_back.html b/docshell/test/navigation/test_dynamic_frame_forward_back.html
new file mode 100644
index 0000000000..f3a349e09a
--- /dev/null
+++ b/docshell/test/navigation/test_dynamic_frame_forward_back.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 508537</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=508537">Mozilla Bug 508537</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_bug508537_1.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_evict_from_bfcache.html b/docshell/test/navigation/test_evict_from_bfcache.html
new file mode 100644
index 0000000000..0b1eb2fca4
--- /dev/null
+++ b/docshell/test/navigation/test_evict_from_bfcache.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Evict a page from bfcache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ /*
+ * This test checks that a page can be evicted from bfcache. Sending a
+ * message to an open BroadcastChannel is used for this.
+ *
+ * First the test opens a window and loads a page there. Another page is then
+ * loaded and then session history is navigated back to check if bfcache is
+ * enabled. If not, close message is sent to close the opened window and this
+ * controller page will finish the test.
+ * If bfcache is enabled, session history goes forward, but the
+ * BroadcastChannel in the page isn't closed. Then sending the message to go
+ * back again should evict the bfcached page.
+ * Close message is sent and window closed and test finishes.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+ var bc = new BroadcastChannel("evict_from_bfcache");
+ var pageshowCount = 0;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ info("pageshow " + pageshowCount);
+ if (pageshowCount == 1) {
+ bc.postMessage("nextpage");
+ } else if (pageshowCount == 2) {
+ bc.postMessage("back");
+ } else if (pageshowCount == 3) {
+ if (!event.data.persisted) {
+ ok(true, "BFCache isn't enabled.");
+ bc.postMessage("close");
+ } else {
+ bc.postMessage("forward");
+ }
+ } else if (pageshowCount == 4) {
+ bc.postMessage("back");
+ } else if (pageshowCount == 5) {
+ ok(!event.data.persisted,
+ "The page should have been evicted from BFCache");
+ bc.postMessage("close");
+ }
+ } else if (event.data == "closed") {
+ SimpleTest.finish();
+ }
+ }
+
+ function runTest() {
+ window.open("file_evict_from_bfcache.html", "", "noopener");
+ }
+ </script>
+</head>
+<body onload="runTest()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_fragment_handling_during_load.html b/docshell/test/navigation/test_fragment_handling_during_load.html
new file mode 100644
index 0000000000..9c082c2ecf
--- /dev/null
+++ b/docshell/test/navigation/test_fragment_handling_during_load.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for fragment navigation during load</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=978408">Mozilla Bug 978408</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_fragment_handling_during_load.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_grandchild.html b/docshell/test/navigation/test_grandchild.html
new file mode 100644
index 0000000000..10cf610664
--- /dev/null
+++ b/docshell/test/navigation/test_grandchild.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 200px; }
+ </style>
+<script>
+if (!navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+window.onload = async function() {
+ navigateByLocation(frames[0].frames[0]);
+ navigateByOpen("child1_child0");
+ navigateByForm("child2_child0");
+ navigateByHyperlink("child3_child0");
+
+ await waitForFinishedFrames(4);
+ await isNavigated(frames[0].frames[0], "Should be able to navigate off-domain grandchild by setting location.");
+ await isNavigated(frames[1].frames[0], "Should be able to navigate off-domain grandchild by calling window.open.");
+ await isNavigated(frames[2].frames[0], "Should be able to navigate off-domain grandchild by submitting form.");
+ await isNavigated(frames[3].frames[0], "Should be able to navigate off-domain grandchild by targeted hyperlink.");
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<div id="frames">
+<iframe name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe>
+<iframe name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe>
+<iframe name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe>
+<iframe name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe>
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_load_history_entry.html b/docshell/test/navigation/test_load_history_entry.html
new file mode 100644
index 0000000000..8ca3fcb913
--- /dev/null
+++ b/docshell/test/navigation/test_load_history_entry.html
@@ -0,0 +1,196 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script>
+ <script type="application/javascript">
+ /*
+ * Perform the following steps.
+ * 1) Go to file_load_history_entry_page_with_two_links.html, which contains two links, 'link1' and 'link2'
+ * 2) Click on 'link1' to be taken to file_load_history_entry_page_with_two_links.html#1
+ * 3) Click on 'link2' to be taken to file_load_history_entry_page_with_two_links.html#2
+ * 4) Go to file_load_history_entry_page_with_one_link.html
+ * 5) Push state to go to file_load_history_entry_page_with_one_link.html#1
+ *
+ * After each step
+ * - Check the number of session history entries
+ * - Reload the document and do the above again
+ * - Navigate back and check the correct history index
+ * - Navigate forward and check the correct history index and location
+ */
+ async function test() {
+ let testWin;
+ var promise;
+ var previousLocation;
+ var numSHEntries = 0;
+
+ // Step 1. Open a new tab and load a document with two links inside
+ // Now we are at file_load_history_entry_page_with_two_links.html
+ numSHEntries++;
+ promise = waitForLoad();
+ testWin = window.open("file_load_history_entry_page_with_two_links.html");
+ await promise;
+
+ let shistory = SpecialPowers.wrap(testWin)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .sessionHistory;
+
+ // Step 2. Navigate the document by clicking on the 1st link
+ // Now we are at file_load_history_entry_page_with_two_links.html#1
+ numSHEntries++;
+ previousLocation = testWin.location.href;
+ await clickLink(testWin, "link1");
+ await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation);
+
+ // Step 3. Navigate the document by clicking the 2nd link
+ // Now we are file_load_history_entry_page_with_two_links.html#2
+ numSHEntries++;
+ previousLocation = testWin.location.href;
+ await clickLink(testWin, "link2");
+ await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation);
+
+ // Step 4. Navigate the document to a different page
+ // Now we are at file_load_history_entry_page_with_one_link.html
+ numSHEntries++;
+ previousLocation = testWin.location.href;
+ promise = waitForLoad();
+ testWin.location = "file_load_history_entry_page_with_one_link.html";
+ await promise;
+ await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation,
+ true /* isCrossDocumentLoad */, false /* hashChangeExpected */);
+
+ // Step 5. Push some state
+ // Now we are at file_load_history_entry_page_with_one_link.html#1
+ numSHEntries++;
+ previousLocation = testWin.location.href;
+ testWin.history.pushState({foo: "bar"}, "", "#1");
+ is(testWin.history.length, numSHEntries, "Session history's length is correct after pushing state");
+ is(shistory.index, numSHEntries - 1 /* we haven't switched to new history entry yet*/,
+ "Session history's index is correct after pushing state");
+ await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation);
+
+ // We are done with the test
+ testWin.close();
+ SimpleTest.finish();
+ }
+
+ /*
+ * @prevLocation
+ * if undefined, it is because there is no page to go back to
+ *
+ * @isCrossDocumentLoad
+ * did we just open a different document
+ * @hashChangeExpected
+ * Would we get a hash change event if we navigated backwards and forwards in history?
+ * This is framed with respect to the previous step, e.g. in the previous step was the
+ * hash different from the location we have navigated to just before calling this function?
+ * When we navigate forwards or backwards, we need to wait for this event
+ * because clickLink() also waits for hashchange event and
+ * if this function gets called before clickLink(), sometimes hashchange
+ * events from this function will leak to clickLink.
+ */
+ async function doAfterEachTest(testWin, shistory, expectedNumSHEntries, prevLocation,
+ isCrossDocumentLoad = false, hashChangeExpected = true) {
+ var initialLocation = testWin.location.href;
+ var initialSHIndex = shistory.index;
+ var promise;
+ is(testWin.history.length, expectedNumSHEntries, "Session history's length is correct");
+
+ // Reload the document
+ promise = waitForLoad();
+ testWin.location.reload(true);
+ await promise;
+ is(testWin.history.length, expectedNumSHEntries, "Session history's length is correct after reloading");
+
+ if (prevLocation == undefined) {
+ return;
+ }
+
+ var hashChangePromise;
+ if (hashChangeExpected) {
+ hashChangePromise = new Promise(resolve => {
+ testWin.addEventListener("hashchange", resolve, {once: true});
+ });
+ }
+ // Navigate backwards
+ if (isCrossDocumentLoad) {
+ // Current page must have been a cross document load, so we just need to wait for
+ // document load to complete after we navigate the history back
+ // because popstate event will not be fired in this case
+ promise = waitForLoad();
+ } else {
+ promise = waitForPopstate(testWin);
+ }
+ testWin.history.back();
+ await promise;
+ if (hashChangeExpected) {
+ await hashChangePromise;
+ }
+ is(testWin.location.href, prevLocation, "Window location is correct after navigating back in history");
+ is(shistory.index, initialSHIndex - 1, "Session history's index is correct after navigating back in history");
+
+ // Navigate forwards
+ if (isCrossDocumentLoad) {
+ promise = waitForLoad();
+ } else {
+ promise = waitForPopstate(testWin);
+ }
+ if (hashChangeExpected) {
+ hashChangePromise = new Promise(resolve => {
+ testWin.addEventListener("hashchange", resolve, {once: true});
+ });
+ }
+ testWin.history.forward();
+ await promise;
+ if (hashChangeExpected) {
+ await hashChangePromise;
+ }
+ is(testWin.location.href, initialLocation, "Window location is correct after navigating forward in history");
+ is(shistory.index, initialSHIndex, "Session history's index is correct after navigating forward in history");
+ }
+
+ async function waitForLoad() {
+ return new Promise(resolve => {
+ window.bodyOnLoad = function() {
+ setTimeout(resolve, 0);
+ window.bodyOnLoad = undefined;
+ };
+ });
+ }
+
+ async function waitForPopstate(win) {
+ return new Promise(resolve => {
+ win.addEventListener("popstate", (e) => {
+ setTimeout(resolve, 0);
+ }, {once: true});
+ });
+ }
+
+ async function clickLink(win, id) {
+ var link = win.document.getElementById(id);
+ let clickPromise = new Promise(resolve => {
+ win.addEventListener("hashchange", resolve, {once: true});
+ });
+ link.click();
+ await clickPromise;
+ }
+
+ </script>
+</head>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1539482">Bug 1539482</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<body onload="test()">
+</body>
+</html>
+
diff --git a/docshell/test/navigation/test_meta_refresh.html b/docshell/test/navigation/test_meta_refresh.html
new file mode 100644
index 0000000000..bda9a9fe73
--- /dev/null
+++ b/docshell/test/navigation/test_meta_refresh.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test meta refresh</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ let hasLoadedInitialOnce = false;
+ let bc = new BroadcastChannel("test_meta_refresh");
+ bc.onmessage = function(event) {
+ info(event.data.load || event.data);
+ if (event.data.load == "initial") {
+ if (!hasLoadedInitialOnce) {
+ hasLoadedInitialOnce = true;
+ bc.postMessage("loadnext");
+ } else {
+ bc.postMessage("ensuremetarefresh");
+ }
+ } else if (event.data.load == "nextpage") {
+ bc.postMessage("back");
+ } else if (event.data.load == "refresh") {
+ bc.postMessage("close");
+ } else if (event.data == "closed") {
+ ok(true, "Meta refresh page was loaded.");
+ SimpleTest.finish();
+ }
+ }
+
+ function test() {
+ window.open("file_meta_refresh.html?initial", "", "noopener");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_navigation_type.html b/docshell/test/navigation/test_navigation_type.html
new file mode 100644
index 0000000000..75ea88bcbd
--- /dev/null
+++ b/docshell/test/navigation/test_navigation_type.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>performance.navigation.type</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ let bc = new BroadcastChannel("navigation_type");
+ let pageshowCount = 0;
+ bc.onmessage = function(event) {
+ if (event.data == "closed") {
+ bc.close();
+ SimpleTest.finish();
+ return;
+ }
+
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ is(event.data.navigationType, PerformanceNavigation.TYPE_NAVIGATE,
+ "Should have navigation type TYPE_NAVIGATE.");
+ bc.postMessage("loadNewPage");
+ } else if (pageshowCount == 2) {
+ is(event.data.navigationType, PerformanceNavigation.TYPE_NAVIGATE,
+ "Should have navigation type TYPE_NAVIGATE.");
+ bc.postMessage("back");
+ } else if (pageshowCount == 3) {
+ is(event.data.navigationType, PerformanceNavigation.TYPE_BACK_FORWARD ,
+ "Should have navigation type TYPE_BACK_FORWARD .");
+ bc.postMessage("close");
+ } else {
+ ok(false, "Unexpected load");
+ }
+ }
+
+ function test() {
+ window.open("file_navigation_type.html", "", "noopener");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_nested_frames.html b/docshell/test/navigation/test_nested_frames.html
new file mode 100644
index 0000000000..c3b49e0e23
--- /dev/null
+++ b/docshell/test/navigation/test_nested_frames.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1090918</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1090918">Mozilla Bug 1090918</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_nested_frames.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close()
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_new_shentry_during_history_navigation.html b/docshell/test/navigation/test_new_shentry_during_history_navigation.html
new file mode 100644
index 0000000000..0c9adc5280
--- /dev/null
+++ b/docshell/test/navigation/test_new_shentry_during_history_navigation.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test adding new session history entries while navigating to another one</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ var win;
+
+ function waitForMessage(msg) {
+ return new Promise(
+ function(resolve) {
+ window.addEventListener("message",
+ function(event) {
+ is(event.data, msg, "Got the expected message " + msg);
+ resolve();
+ }, { once: true }
+ );
+ }
+ );
+ }
+
+ async function test() {
+
+ let loadPromise = waitForMessage("load");
+ win = window.open("file_new_shentry_during_history_navigation_1.html");
+ await loadPromise;
+
+ loadPromise = waitForMessage("load");
+ win.location.href = "file_new_shentry_during_history_navigation_2.html";
+ await loadPromise;
+
+ let beforeunloadPromise = waitForMessage("beforeunload");
+ win.history.back();
+ await beforeunloadPromise;
+ await waitForMessage("load");
+
+ win.history.back();
+ SimpleTest.requestFlakyTimeout("Test that history.back() does not work on the initial entry.");
+ setTimeout(function() {
+ win.onmessage = null;
+ win.close();
+ testBfcache();
+ }, 500);
+ window.onmessage = function(event) {
+ ok(false, "Should not get a message " + event.data);
+ }
+ }
+
+ async function testBfcache() {
+ let bc = new BroadcastChannel("new_shentry_during_history_navigation");
+ let pageshowCount = 0;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ info("pageshow: " + pageshowCount + ", page: " + event.data.page);
+ if (pageshowCount == 1) {
+ ok(!event.data.persisted, "The page should not be bfcached.");
+ bc.postMessage("loadnext");
+ } else if (pageshowCount == 2) {
+ ok(!event.data.persisted, "The page should not be bfcached.");
+ bc.postMessage("loadnext");
+ } else if (pageshowCount == 3) {
+ ok(!event.data.persisted, "The page should not be bfcached.");
+ bc.postMessage("back");
+ } else if (pageshowCount == 4) {
+ ok(event.data.persisted, "The page should be bfcached.");
+ bc.postMessage("forward");
+ } else if (pageshowCount == 5) {
+ ok(event.data.page.includes("v2"), "Should have gone forward.");
+ bc.postMessage("close");
+ SimpleTest.finish();
+ }
+ }
+ };
+ win = window.open("file_new_shentry_during_history_navigation_3.html", "", "noopener");
+
+ }
+
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_not-opener.html b/docshell/test/navigation/test_not-opener.html
new file mode 100644
index 0000000000..acdb9473e6
--- /dev/null
+++ b/docshell/test/navigation/test_not-opener.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+if (!navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+window.onload = async function() {
+ // navigateByLocation(window0); // Don't have a handle to the window.
+ navigateByOpen("window1");
+ navigateByForm("window2");
+ navigateByHyperlink("window3");
+
+ await waitForFinishedFrames(6);
+
+ is((await getFramesByName("window1")).length, 2, "Should not be able to navigate popup's popup by calling window.open.");
+ is((await getFramesByName("window2")).length, 2, "Should not be able to navigate popup's popup by submitting form.");
+ is((await getFramesByName("window3")).length, 2, "Should not be able to navigate popup's popup by targeted hyperlink.");
+
+ // opener0.close();
+ opener1.close();
+ opener2.close();
+ opener3.close();
+
+ info("here")
+ await cleanupWindows();
+ info("there")
+ SimpleTest.finish();
+};
+
+// opener0 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window0", "_blank", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+let opener1 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window1", "_blank", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+let opener2 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window2", "_blank", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+let opener3 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window3", "_blank", "width=10,height=10");
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_online_offline_bfcache.html b/docshell/test/navigation/test_online_offline_bfcache.html
new file mode 100644
index 0000000000..4ad90fd52e
--- /dev/null
+++ b/docshell/test/navigation/test_online_offline_bfcache.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Online/Offline with BFCache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+
+ /*
+ * The test is designed to work with and without bfcache.
+ * (1) First the test opens a window which then loads another page which
+ * goes back to the original page to detect if bfcache is enabled. If
+ * bfcache isn't enabled, close message is sent to the opened window and it
+ * closes itself and sends a message back and the test finishes.
+ * (2) The browser is set to offline mode. The opened page sends message
+ * that it has received offline event. This controller page then asks the
+ * page to go forward. The page which comes out from the bfcache gets
+ * offline event and sends message about that to this controller.
+ * (3) Browser is set to online mode. Similar cycle as with offline happens.
+ * (4) Controller page sends close message to the opened window and it
+ * closes itself and sends a message back and the test finishes.
+ */
+
+ function offlineOnline(online) {
+ function offlineFn() {
+ /* eslint-env mozilla/chrome-script */
+ Services.io.offline = true;
+ }
+ function onlineFn() {
+ /* eslint-env mozilla/chrome-script */
+ Services.io.offline = false;
+ }
+ SpecialPowers.loadChromeScript(online ? onlineFn : offlineFn);
+ }
+
+ var bc = new BroadcastChannel("online_offline_bfcache");
+ var pageshowCount = 0;
+ var offlineCount = 0;
+ var onlineCount = 0;
+
+ bc.onmessage = function(event) {
+ if (event.data.event == "pageshow") {
+ ++pageshowCount;
+ info("pageshow " + pageshowCount);
+ if (pageshowCount == 1) {
+ ok(!event.data.persisted);
+ bc.postMessage("nextpage");
+ } else if (pageshowCount == 2) {
+ ok(!event.data.persisted);
+ bc.postMessage("back");
+ } else if (pageshowCount == 3) {
+ if (!event.data.persisted) {
+ info("BFCache is not enabled, return early");
+ bc.postMessage("close");
+ } else {
+ offlineOnline(false);
+ }
+ }
+ } else if (event.data == "offline") {
+ ++offlineCount;
+ info("offline " + offlineCount);
+ if (offlineCount == 1) {
+ bc.postMessage("forward");
+ } else if (offlineCount == 2) {
+ offlineOnline(true);
+ } else {
+ ok(false, "unexpected offline event");
+ }
+ } else if (event.data == "online") {
+ ++onlineCount;
+ info("online " + onlineCount);
+ if (onlineCount == 1) {
+ bc.postMessage("back");
+ } else if (onlineCount == 2) {
+ bc.postMessage("close");
+ } else {
+ ok(false, "unexpected online event");
+ }
+ } else if ("closed") {
+ ok(true, "Did pass the test");
+ bc.close();
+ SimpleTest.finish();
+ }
+ };
+
+ function runTest() {
+ SpecialPowers.pushPrefEnv({"set": [["network.manage-offline-status", false]]}, function() {
+ window.open("file_online_offline_bfcache.html", "", "noopener");
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ </script>
+</head>
+<body onload="runTest()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_open_javascript_noopener.html b/docshell/test/navigation/test_open_javascript_noopener.html
new file mode 100644
index 0000000000..81a6b70d61
--- /dev/null
+++ b/docshell/test/navigation/test_open_javascript_noopener.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <script>
+
+add_task(async function test_open_javascript_noopener() {
+ const topic = "test-javascript-was-run";
+ function jsuri(version) {
+ return `javascript:SpecialPowers.notifyObservers(null, "${topic}", "${version}");window.close()`;
+ }
+
+ let seen = [];
+ function observer(_subject, _topic, data) {
+ info(`got notification ${data}`);
+ seen.push(data);
+ }
+ SpecialPowers.addObserver(observer, topic);
+
+ isDeeply(seen, [], "seen no test notifications");
+ window.open(jsuri("1"));
+
+ // Bounce off the parent process to make sure the JS will have run.
+ await SpecialPowers.spawnChrome([], () => {});
+
+ isDeeply(seen, ["1"], "seen the opener notification");
+
+ window.open(jsuri("2"), "", "noopener");
+
+ // Bounce off the parent process to make sure the JS will have run.
+ await SpecialPowers.spawnChrome([], () => {});
+
+ isDeeply(seen, ["1"], "didn't get a notification from the noopener popup");
+
+ SpecialPowers.removeObserver(observer, topic);
+});
+
+ </script>
+ </body>
+</html>
diff --git a/docshell/test/navigation/test_opener.html b/docshell/test/navigation/test_opener.html
new file mode 100644
index 0000000000..ce966b897d
--- /dev/null
+++ b/docshell/test/navigation/test_opener.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+if (navigator.platform.startsWith("Linux")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+window.onload = async function() {
+ navigateByLocation(window0);
+ navigateByOpen("window1");
+ navigateByForm("window2");
+ navigateByHyperlink("window3");
+
+ await waitForFinishedFrames(4);
+ await isNavigated(window0, "Should be able to navigate popup by setting location.");
+ await isNavigated(window1, "Should be able to navigate popup by calling window.open.");
+ await isNavigated(window2, "Should be able to navigate popup by submitting form.");
+ await isNavigated(window3, "Should be able to navigate popup by targeted hyperlink.");
+
+ window0.close();
+ window1.close();
+ window2.close();
+ window3.close();
+
+ await cleanupWindows();
+
+ SimpleTest.finish();
+};
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window0 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window0", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window1 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window1", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window2 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window2", "width=10,height=10");
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var window3 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window3", "width=10,height=10");
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_performance_navigation.html b/docshell/test/navigation/test_performance_navigation.html
new file mode 100644
index 0000000000..75abbdd767
--- /dev/null
+++ b/docshell/test/navigation/test_performance_navigation.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=145971
+-->
+<head>
+ <title>Test for Bug 145971</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=145971">Mozilla Bug 145971</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+var testWindow;
+var bc = new BroadcastChannel("bug145971");
+bc.onmessage = function(msgEvent) {
+ var result = msgEvent.data.result;
+ if (result == undefined) {
+ info("Got unexpected message from BroadcastChannel");
+ return;
+ }
+ ok(result, "Bug 145971: Navigation type does not equal 2 when restoring document from session history.");
+ SimpleTest.finish();
+};
+
+function runTest() {
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ window.open("test_bug145971.html", "", "width=360,height=480,noopener");
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_popup-navigates-children.html b/docshell/test/navigation/test_popup-navigates-children.html
new file mode 100644
index 0000000000..82d69e7982
--- /dev/null
+++ b/docshell/test/navigation/test_popup-navigates-children.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+
+let window0 = null;
+let window1 = null;
+let window2 = null;
+let window3 = null;
+
+function testChild0() {
+ if (!window.window0)
+ window0 = window.open("navigate.html#opener.frames[0],location", "window0", "width=10,height=10");
+}
+
+function testChild1() {
+ if (!window.window1)
+ window1 = window.open("navigate.html#child1,open", "window1", "width=10,height=10");
+}
+
+function testChild2() {
+ if (!window.window2)
+ window2 = window.open("navigate.html#child2,form", "window2", "width=10,height=10");
+}
+
+function testChild3() {
+ if (!window.window3)
+ window3 = window.open("navigate.html#child3,hyperlink", "window3", "width=10,height=10");
+}
+
+window.onload = async function() {
+ await waitForFinishedFrames(4);
+ await isNavigated(frames[0], "Should be able to navigate on-domain opener's children by setting location.");
+ await isNavigated(frames[1], "Should be able to navigate on-domain opener's children by calling window.open.");
+ await isNavigated(frames[2], "Should be able to navigate on-domain opener's children by submitting form.");
+ await isNavigated(frames[3], "Should be able to navigate on-domain opener's children by targeted hyperlink.");
+
+ window0.close();
+ window1.close();
+ window2.close();
+ window3.close();
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+
+</script>
+</head>
+<body>
+<div id="frames">
+<iframe onload="testChild0()" name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe onload="testChild1()" name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe onload="testChild2()" name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe onload="testChild3()" name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_rate_limit_location_change.html b/docshell/test/navigation/test_rate_limit_location_change.html
new file mode 100644
index 0000000000..b1b51b92dd
--- /dev/null
+++ b/docshell/test/navigation/test_rate_limit_location_change.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1314912
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1314912</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1314912 */
+
+ const RATE_LIMIT_COUNT = 90;
+ const RATE_LIMIT_TIME_SPAN = 3;
+
+ async function setup() {
+ await SpecialPowers.pushPrefEnv({set: [
+ ["dom.navigation.locationChangeRateLimit.count", RATE_LIMIT_COUNT],
+ ["dom.navigation.locationChangeRateLimit.timespan", RATE_LIMIT_TIME_SPAN]]});
+ }
+
+ let inc = 0;
+
+ const rateLimitedFunctions = (win) => ({
+ "history.replaceState": () => win.history.replaceState(null, "test", `${win.location.href}#${inc++}`),
+ "history.pushState": () => win.history.pushState(null, "test", `${win.location.href}#${inc++}`),
+ "history.back": () => win.history.back(),
+ "history.forward": () => win.history.forward(),
+ "history.go": () => win.history.go(-1),
+ "location.href": () => win.location.href = win.location.href + "",
+ "location.hash": () => win.location.hash = inc++,
+ "location.host": () => win.location.host = win.location.host + "",
+ "location.hostname": () => win.location.hostname = win.location.hostname + "",
+ "location.pathname": () => win.location.pathname = win.location.pathname + "",
+ "location.port": () => win.location.port = win.location.port + "",
+ "location.protocol": () => win.location.protocol = win.location.protocol + "",
+ "location.search": () => win.location.search = win.location.search + "",
+ "location.assign": () => win.location.assign(`${win.location.href}#${inc++}`),
+ "location.replace": () => win.location.replace(`${win.location.href}#${inc++}`),
+ "location.reload": () => win.location.reload(),
+ });
+
+ async function test() {
+ await setup();
+
+ // Open new window and wait for it to load
+ let win = window.open("blank.html");
+ await new Promise((resolve) => SimpleTest.waitForFocus(resolve, win))
+
+ // Execute the history and location functions
+ Object.entries(rateLimitedFunctions(win)).forEach(([name, fn]) => {
+ // Reset the rate limit for the next run.
+ info("Reset rate limit.");
+ SpecialPowers.wrap(win).browsingContext.resetLocationChangeRateLimit();
+
+ info(`Calling ${name} ${RATE_LIMIT_COUNT} times to reach the rate limit.`);
+ for(let i = 0; i< RATE_LIMIT_COUNT; i++) {
+ fn.call(this);
+ }
+ // Next calls should throw because we're above the rate limit
+ for(let i = 0; i < 5; i++) {
+ SimpleTest.doesThrow(() => fn.call(this), `Call #${RATE_LIMIT_COUNT + i + 1} to ${name} should throw.`);
+ }
+ })
+
+ // We didn't reset the rate limit after the last loop iteration above.
+ // Wait for the rate limit timer to expire.
+ SimpleTest.requestFlakyTimeout("Waiting to trigger rate limit reset.");
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+
+ // Calls should be allowed again.
+ Object.entries(rateLimitedFunctions(win)).forEach(([name, fn]) => {
+ let didThrow = false;
+ try {
+ fn.call(this);
+ } catch(error) {
+ didThrow = true;
+ }
+ is(didThrow, false, `Call to ${name} must not throw.`)
+ });
+
+ // Cleanup
+ win.close();
+ SpecialPowers.wrap(win).browsingContext.resetLocationChangeRateLimit();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body onload="setTimeout(test, 0);">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1314912">Mozilla Bug 1314912</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_recursive_frames.html b/docshell/test/navigation/test_recursive_frames.html
new file mode 100644
index 0000000000..3ccc09dd14
--- /dev/null
+++ b/docshell/test/navigation/test_recursive_frames.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Recursive Loads</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1597427">Mozilla Bug 1597427</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ const TEST_CASES = [
+ { // too many recursive iframes
+ frameId: "recursiveFrame",
+ expectedLocations: [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_recursive.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_recursive.html",
+ "about:blank",
+ ],
+ },
+ { // too many recursive iframes
+ frameId: "twoRecursiveIframes",
+ expectedLocations: [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/tests/docshell/test/navigation/frame_load_as_example_org.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/tests/docshell/test/navigation/frame_load_as_example_org.html",
+ "about:blank",
+ ],
+ },
+ { // too many recursive iframes
+ frameId: "threeRecursiveIframes",
+ expectedLocations: [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_load_as_host2.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host3.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_load_as_host2.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host3.html",
+ "about:blank",
+ ],
+ },
+ { // too many nested iframes
+ frameId: "sixRecursiveIframes",
+ expectedLocations: [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_2_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_3_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/frame_4_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test2.mochi.test:8888/tests/docshell/test/navigation/frame_5_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/tests/docshell/test/navigation/frame_6_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_2_out_of_6.html",
+ ],
+ },
+ { // too many recursive objects
+ frameId: "recursiveObject",
+ expectedLocations: [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html",
+ ],
+ },
+ { // 3 nested srcdocs, should show all of them
+ frameId: "nestedSrcdoc",
+ expectedLocations: [
+ "about:srcdoc",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/tests/docshell/test/navigation/file_nested_srcdoc.html",
+ "about:srcdoc",
+ "about:srcdoc",
+ ],
+ },
+ ];
+
+ async function checkRecursiveLoad(level) {
+ let el = content.document.getElementById("static");
+ let documentURI = await SpecialPowers.spawn(
+ el,
+ [],
+ () => this.content.document.documentURI
+ );
+ if (documentURI == "about:blank") {
+ // If we had too many recursive frames, the most inner iframe's uri will be about:blank
+ return [documentURI];
+ }
+ if (documentURI == "about:srcdoc" && level == 3) {
+ // Check that we have the correct most inner srcdoc iframe
+ let innerText = await SpecialPowers.spawn(
+ el,
+ [],
+ () => this.content.document.body.innerText
+ );
+ is(innerText, "Third nested srcdoc", "correct most inner srcdoc iframe");
+ }
+ let nestedIfrOrObjectURI = [];
+ try {
+ // Throws an error when we have too many nested frames/objects, because we
+ // claim to have no content window for the inner most frame/object.
+ nestedIfrOrObjectURI = await SpecialPowers.spawn(
+ el,
+ [level + 1],
+ checkRecursiveLoad
+ );
+ } catch (err) {
+ info(
+ `Tried to spawn another task in the iframe/object, but got err: ${err}, must have had too many nested iframes/objects\n`
+ );
+ }
+ return [documentURI, ...nestedIfrOrObjectURI];
+ }
+
+ add_task(async () => {
+ for (const testCase of TEST_CASES) {
+ let el = document.getElementById(testCase.frameId);
+ let loc = await SpecialPowers.spawn(
+ el,
+ [],
+ () => this.content.location.href
+ );
+ let locations = await SpecialPowers.spawn(el, [1], checkRecursiveLoad);
+ isDeeply(
+ [loc, ...locations],
+ testCase.expectedLocations,
+ "iframes/object loaded in correct order"
+ );
+ }
+ });
+
+ </script>
+</pre>
+<div>
+ <iframe style="height: 100vh; width:25%;" id="recursiveFrame" src="http://example.com/tests/docshell/test/navigation/frame_recursive.html"></iframe>
+ <iframe style="height: 100vh; width:25%;" id="twoRecursiveIframes" src="http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html"></iframe>
+ <iframe style="height: 100vh; width:25%;" id="threeRecursiveIframes" src="http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html"></iframe>
+ <iframe style="height: 100vh; width:25%;" id="sixRecursiveIframes" src="http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html"></iframe>
+ <object width="400" height="300" id="recursiveObject" data="http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html"></object>
+ <iframe id="nestedSrcdoc" srcdoc="Srcdoc that will embed an iframe &lt;iframe id=&quot;static&quot; src=&quot;http://example.com/tests/docshell/test/navigation/file_nested_srcdoc.html&quot;&gt;&lt;/iframe&gt;"></iframe>
+</div>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_reload.html b/docshell/test/navigation/test_reload.html
new file mode 100644
index 0000000000..7e75c7c035
--- /dev/null
+++ b/docshell/test/navigation/test_reload.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure a page which is otherwise bfcacheable doesn't crash on reload</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ let pageshowCount = 0;
+ let bc = new BroadcastChannel("test_reload");
+ bc.onmessage = function(event) {
+ info("MessageEvent: " + event.data);
+ if (event.data == "pageshow") {
+ ++pageshowCount;
+ info("pageshow: " + pageshowCount);
+ if (pageshowCount < 3) {
+ info("Sending reload");
+ bc.postMessage("reload");
+ } else {
+ info("Sending close");
+ bc.postMessage("close");
+ }
+ } else if (event.data == "closed") {
+ info("closed");
+ bc.close();
+ ok(true, "Passed");
+ SimpleTest.finish();
+ }
+ }
+
+ function test() {
+ window.open("file_reload.html", "", "noopener");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_reload_large_postdata.html b/docshell/test/navigation/test_reload_large_postdata.html
new file mode 100644
index 0000000000..15fae33ac3
--- /dev/null
+++ b/docshell/test/navigation/test_reload_large_postdata.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+
+<form id="form" action="file_reload_large_postdata.sjs" target="_blank" rel="opener" method="POST">
+ <input id="input" name="payload" type="hidden" value=""/>
+</form>
+
+<pre id="test">
+<script>
+// This is derived from `kTooLargeStream` in `IPCStreamUtils.cpp`.
+const kTooLargeStream = 1024 * 1024;
+
+function waitForPopup(expected) {
+ return new Promise(resolve => {
+ addEventListener("message", evt => {
+ info("got message!");
+ is(evt.source.opener, window, "the event source's opener should be this window");
+ is(evt.data, expected, "got the expected data from the popup");
+ resolve(evt.source);
+ }, { once: true });
+ });
+}
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({"set": [["dom.confirm_repost.testing.always_accept", true]]});
+ let form = document.getElementById("form");
+ let input = document.getElementById("input");
+
+ // Create a very large value to include in the post payload. This should
+ // ensure that the value isn't sent directly over IPC, and is instead sent as
+ // an async inputstream.
+ let payloadSize = kTooLargeStream;
+
+ let popupReady = waitForPopup(payloadSize);
+ input.value = "A".repeat(payloadSize);
+ form.submit();
+
+ let popup = await popupReady;
+ try {
+ let popupReady2 = waitForPopup(payloadSize);
+ info("reloading popup");
+ popup.location.reload();
+ let popup2 = await popupReady2;
+ is(popup, popup2);
+ } finally {
+ popup.close();
+ }
+});
+
+// The .sjs server can time out processing the 1mb payload in debug builds.
+SimpleTest.requestLongerTimeout(2);
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_reload_nonbfcached_srcdoc.html b/docshell/test/navigation/test_reload_nonbfcached_srcdoc.html
new file mode 100644
index 0000000000..2399a0ad7d
--- /dev/null
+++ b/docshell/test/navigation/test_reload_nonbfcached_srcdoc.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test srcdoc handling when reloading a page.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ // The old session history implementation asserts in
+ // https://searchfox.org/mozilla-central/rev/b822a27de3947d3f4898defac6164e52caf1451b/docshell/shistory/nsSHEntry.cpp#670-672
+ SimpleTest.expectAssertions(0, 1);
+ SimpleTest.waitForExplicitFinish();
+
+ var win;
+ function test() {
+ window.onmessage = function(event) {
+ if (event.data == "pageload:") {
+ // Trigger a similar reload as what the reload button does.
+ SpecialPowers.wrap(win)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .sessionHistory
+ .reload(0);
+ } else if (event.data == "pageload:second") {
+ ok(true, "srcdoc iframe was updated.");
+ win.close();
+ SimpleTest.finish();
+ }
+ }
+ win = window.open("file_reload_nonbfcached_srcdoc.sjs");
+ }
+
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_reserved.html b/docshell/test/navigation/test_reserved.html
new file mode 100644
index 0000000000..0242f3941b
--- /dev/null
+++ b/docshell/test/navigation/test_reserved.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 200px; }
+ </style>
+<script>
+if (navigator.platform.startsWith("Mac")) {
+ SimpleTest.expectAssertions(0, 2);
+}
+
+async function testTop() {
+ let window0 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#top,location", "_blank", "width=10,height=10");
+
+ await waitForFinishedFrames(1);
+ isInaccessible(window0, "Should be able to navigate off-domain top by setting location.");
+ window0.close();
+ await cleanupWindows();
+
+ let window1 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_top,open", "_blank", "width=10,height=10");
+
+ await waitForFinishedFrames(1);
+ isInaccessible(window1, "Should be able to navigate off-domain top by calling window.open.");
+ window1.close();
+ await cleanupWindows();
+
+ let window2 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_top,form", "_blank", "width=10,height=10");
+
+ await waitForFinishedFrames(1);
+ isInaccessible(window2, "Should be able to navigate off-domain top by submitting form.");
+ window2.close();
+ await cleanupWindows();
+
+ let window3 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_top,hyperlink", "_blank", "width=10,height=10");
+
+ await waitForFinishedFrames(1);
+ isInaccessible(window3, "Should be able to navigate off-domain top by targeted hyperlink.");
+ window3.close();
+ await cleanupWindows();
+
+ await testParent();
+}
+
+async function testParent() {
+ document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#parent,location"></iframe>';
+
+ await waitForFinishedFrames(1);
+ isAccessible(frames[0], "Should not be able to navigate off-domain parent by setting location.");
+ await cleanupWindows();
+
+ document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_parent,open"></iframe>';
+
+ await waitForFinishedFrames(1);
+ isAccessible(frames[0], "Should not be able to navigate off-domain parent by calling window.open.");
+ await cleanupWindows();
+
+ document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_parent,form"></iframe>';
+
+ await waitForFinishedFrames(1);
+ isAccessible(frames[0], "Should not be able to navigate off-domain parent by submitting form.");
+ await cleanupWindows();
+
+ document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_parent,hyperlink"></iframe>';
+
+ await waitForFinishedFrames(1);
+ isAccessible(frames[0], "Should not be able to navigate off-domain parent by targeted hyperlink.");
+ await cleanupWindows();
+
+ document.getElementById("frames").innerHTML = "";
+ SimpleTest.finish();
+}
+
+window.onload = async function() {
+ await testTop();
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<div id="frames">
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_same_url.html b/docshell/test/navigation/test_same_url.html
new file mode 100644
index 0000000000..820caa7005
--- /dev/null
+++ b/docshell/test/navigation/test_same_url.html
@@ -0,0 +1,56 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script>
+ <script type="application/javascript">
+ // Since BFCache in parent requires no opener, use BroadcastChannel
+ // to communicate with file_same_url.html.
+ let bc = new BroadcastChannel("test_same_url");
+ async function test() {
+ var promise;
+ let historyLength;
+
+ promise = waitForLoad();
+ window.open("file_same_url.html", "_blank", "noopener=yes");
+ historyLength = await promise;
+ is(historyLength, 1, "before same page navigation");
+
+ promise = waitForLoad();
+ bc.postMessage("linkClick");
+ historyLength = await promise;
+ is(historyLength, 1, "after same page navigation");
+ bc.postMessage("closeWin");
+
+ SimpleTest.finish();
+ }
+
+ async function waitForLoad() {
+ return new Promise(resolve => {
+ let listener = e => {
+ if (e.data.bodyOnLoad) {
+ bc.removeEventListener("message", listener);
+ setTimeout(() => resolve(e.data.bodyOnLoad), 0);
+ }
+ };
+ bc.addEventListener("message", listener);
+ });
+ }
+ </script>
+</head>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1745730">Bug 1745730</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<body onload="test()">
+</body>
+</html>
+
diff --git a/docshell/test/navigation/test_scrollRestoration.html b/docshell/test/navigation/test_scrollRestoration.html
new file mode 100644
index 0000000000..d31598f391
--- /dev/null
+++ b/docshell/test/navigation/test_scrollRestoration.html
@@ -0,0 +1,214 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1155730</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155730">Mozilla Bug 1155730</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("untriaged");
+
+ function assertCheck(data) {
+ if (data.assertIs) {
+ for (const args of data.assertIs) {
+ is(args[0], args[1], args[2]);
+ }
+ }
+ if (data.assertOk) {
+ for (const args of data.assertOk) {
+ ok(args[0], args[1]);
+ }
+ }
+ if (data.assertIsNot) {
+ for (const args of data.assertIsNot) {
+ isnot(args[0], args[1], args[2]);
+ }
+ }
+ }
+
+ var bc1, currentCase = 0;
+ function test1() {
+ bc1 = new BroadcastChannel("bug1155730_part1");
+ bc1.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "pageshow") {
+ currentCase++;
+ var persisted = msg.persisted;
+ is(persisted, false, "Shouldn't have persisted session history entry.");
+ bc1.postMessage({command: "test", currentCase});
+ } else if (command == "asserts") {
+ is(msg.currentCase, currentCase, "correct case");
+ info(`Checking asserts for case ${msg.currentCase}`);
+ assertCheck(msg);
+ if (currentCase == 3) {
+ // move on to the next test
+ bc1.close();
+ test2();
+ }
+ }
+ }
+ window.open("file_scrollRestoration_part1_nobfcache.html", "", "width=360,height=480,noopener");
+ }
+
+ var bc2, bc2navigate;
+ function test2() {
+ currentCase = 0;
+ bc2 = new BroadcastChannel("bug1155730_part2");
+ bc2.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "pageshow") {
+ currentCase++;
+ var persisted = msg.persisted;
+ switch (currentCase) {
+ case 1:
+ is(persisted, false, "Shouldn't have persisted session history entry.");
+ break;
+ case 2:
+ is(persisted, true, "Should have persisted session history entry.");
+ }
+ bc2.postMessage({command: "test", currentCase});
+ } else if (command == "asserts") {
+ is(msg.currentCase, currentCase, "correct case");
+ info(`Checking asserts for case ${msg.currentCase}`);
+ assertCheck(msg);
+ if (currentCase == 3) {
+ // move on to the next test
+ bc2.close();
+ test3();
+ }
+ } else if (command == "nextCase") {
+ currentCase++;
+ }
+ }
+
+ bc2navigate = new BroadcastChannel("navigate");
+ bc2navigate.onmessage = (event) => {
+ if (event.data.command == "loaded") {
+ bc2navigate.postMessage({command: "back"})
+ bc2navigate.close();
+ }
+ }
+ window.open("file_scrollRestoration_part2_bfcache.html", "", "width=360,height=480,noopener");
+ }
+
+ var bc3, bc3navigate;
+ function test3() {
+ currentCase = 0;
+ bc3 = new BroadcastChannel("bug1155730_part3");
+ bc3.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "pageshow") {
+ currentCase++;
+ if (currentCase == 3) {
+ var persisted = msg.persisted;
+ is(persisted, false, "Shouldn't have persisted session history entry.");
+ }
+
+ bc3.postMessage({command: "test", currentCase});
+ } else if (command == "asserts") {
+ is(msg.currentCase, currentCase, "correct case");
+ info(`Checking asserts for case ${msg.currentCase}`);
+ assertCheck(msg);
+ } else if (command == "nextCase") {
+ currentCase++;
+ } else if (command == "finishing") {
+ bc3.close();
+ test4();
+ }
+ }
+
+ bc3navigate = new BroadcastChannel("navigate");
+ bc3navigate.onmessage = (event) => {
+ if (event.data.command == "loaded") {
+ is(event.data.scrollRestoration, 'auto', "correct scroll restoration");
+ bc3navigate.postMessage({command: "back"})
+ bc3navigate.close();
+ }
+ }
+ window.open("file_scrollRestoration_part3_nobfcache.html", "", "width=360,height=480,noopener");
+ }
+
+ // test4 opens a new page which can enter bfcache. That page then loads
+ // another page which can't enter bfcache. That second page then scrolls
+ // down. History API is then used to navigate back and forward. When the
+ // second page loads again, it should scroll down automatically.
+ var bc4a, bc4b;
+ var scrollYCounter = 0;
+ function test4() {
+ currentCase = 0;
+ bc4a = new BroadcastChannel("bfcached");
+ bc4a.onmessage = (msgEvent) => {
+ var msg = msgEvent.data;
+ var command = msg.command;
+ if (command == "pageshow") {
+ ++currentCase;
+ if (currentCase == 1) {
+ ok(!msg.persisted, "The first page should not be persisted initially.");
+ bc4a.postMessage("loadNext");
+ } else if (currentCase == 3) {
+ ok(msg.persisted, "The first page should be persisted.");
+ bc4a.postMessage("forward");
+ bc4a.close();
+ }
+ }
+ }
+
+ bc4b = new BroadcastChannel("notbfcached");
+ bc4b.onmessage = (event) => {
+ var msg = event.data;
+ var command = msg.command;
+ if (command == "pageshow") {
+ ++currentCase;
+ if (currentCase == 2) {
+ ok(!msg.persisted, "The second page should not be persisted.");
+ bc4b.postMessage("getScrollY");
+ bc4b.postMessage("scroll");
+ bc4b.postMessage("getScrollY");
+ bc4b.postMessage("back");
+ } else if (currentCase == 4) {
+ ok(!msg.persisted, "The second page should not be persisted.");
+ bc4b.postMessage("getScrollY");
+ }
+ } else if (msg == "closed") {
+ bc4b.close();
+ SimpleTest.finish();
+ } else if ("scrollY" in msg) {
+ ++scrollYCounter;
+ if (scrollYCounter == 1) {
+ is(msg.scrollY, 0, "The page should be initially scrolled to top.");
+ } else if (scrollYCounter == 2) {
+ isnot(msg.scrollY, 0, "The page should be then scrolled down.");
+ } else if (scrollYCounter == 3) {
+ isnot(msg.scrollY, 0, "The page should be scrolled down after being restored from the session history.");
+ bc4b.postMessage("close");
+ }
+ }
+ }
+ window.open("file_scrollRestoration_bfcache_and_nobfcache.html", "", "width=360,height=480,noopener");
+ }
+
+ function runTest() {
+ // If Fission is disabled, the pref is no-op.
+ SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => {
+ test1();
+ });
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_session_history_entry_cleanup.html b/docshell/test/navigation/test_session_history_entry_cleanup.html
new file mode 100644
index 0000000000..a55de0d6c3
--- /dev/null
+++ b/docshell/test/navigation/test_session_history_entry_cleanup.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 534178</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534178">Mozilla Bug 534178</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_bug534178.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_session_history_on_redirect.html b/docshell/test/navigation/test_session_history_on_redirect.html
new file mode 100644
index 0000000000..a303f81536
--- /dev/null
+++ b/docshell/test/navigation/test_session_history_on_redirect.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Session history on redirect</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ /*
+ * The test opens a new window and loads a page there. Then another document
+ * is loaded to the window. The initial load of that second page doesn't do
+ * a redirect. Now another non-redirecting page is loaded. Then
+ * history.go(-2) and history.forward() are called. The second time
+ * the second page is loaded, it does a redirect. history.back() and
+ * history.forward() are called again. The page which did the redirect
+ * shouldn't be accessed, but the page which it redirected to.
+ * Finally history.forward() is called again and the third page should be
+ * loaded and history.length should have the same value as it had when the
+ * third page was loaded the first time.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+ var win;
+ var finalHistoryLength = 0;
+
+ function run() {
+ win = window.open("file_session_history_on_redirect.html");
+ }
+
+ var pageshowCounter = 0;
+ async function pageshow() {
+ // Need to trigger new loads asynchronously after page load, otherwise
+ // new loads are treated as replace loads.
+ await new Promise((r) => setTimeout(r));
+ ++pageshowCounter;
+ info("Page load: " + win.location.href);
+ switch (pageshowCounter) {
+ case 1:
+ ok(win.location.href.includes("file_session_history_on_redirect.html"));
+ win.location.href = "redirect_handlers.sjs";
+ break;
+ case 2:
+ ok(win.location.href.includes("redirect_handlers.sjs"));
+ // Put the initial page also as the last entry in the session history.
+ win.location.href = "file_session_history_on_redirect.html";
+ break;
+ case 3:
+ ok(win.location.href.includes("file_session_history_on_redirect.html"));
+ finalHistoryLength = win.history.length;
+ win.history.go(-2);
+ break;
+ case 4:
+ ok(win.location.href.includes("file_session_history_on_redirect.html"));
+ win.history.forward();
+ break;
+ case 5:
+ ok(win.location.href.includes("file_session_history_on_redirect_2.html"));
+ win.history.back();
+ break;
+ case 6:
+ ok(win.location.href.includes("file_session_history_on_redirect.html"));
+ win.history.forward();
+ break;
+ case 7:
+ ok(win.location.href.includes("file_session_history_on_redirect_2.html"));
+ is(win.history.length, finalHistoryLength, "Shouldn't have changed the history length.");
+ win.history.forward();
+ break;
+ case 8:
+ ok(win.location.href.includes("file_session_history_on_redirect.html"));
+ is(win.history.length, finalHistoryLength, "Shouldn't have changed the history length.");
+ win.onpagehide = null;
+ finishTest();
+ break;
+ default:
+ ok(false, "unexpected pageshow");
+ }
+ }
+
+ function finishTest() {
+ win.close()
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body onload="run()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_sessionhistory.html b/docshell/test/navigation/test_sessionhistory.html
new file mode 100644
index 0000000000..2254ec876b
--- /dev/null
+++ b/docshell/test/navigation/test_sessionhistory.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 462076</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="nextTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=462076">Mozilla Bug 462076</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ var testFiles =
+ [ "file_bug462076_1.html", // Dynamic frames before onload
+ "file_bug462076_2.html", // Dynamic frames when handling onload
+ "file_bug462076_3.html", // Dynamic frames after onload
+ ];
+ var testCount = 0; // Used by the test files.
+
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function nextTest_() {
+ if (testFiles.length) {
+ testCount = 0;
+ let nextFile = testFiles.shift();
+ info("Running " + nextFile);
+ testWindow = window.open(nextFile, "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ } else {
+ SimpleTest.finish();
+ }
+ }
+
+ function nextTest() {
+ setTimeout(nextTest_, 0);
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_sessionhistory_document_write.html b/docshell/test/navigation/test_sessionhistory_document_write.html
new file mode 100644
index 0000000000..2a48a8154e
--- /dev/null
+++ b/docshell/test/navigation/test_sessionhistory_document_write.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Session history + document.write</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_document_write_1.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_sessionhistory_iframe_removal.html b/docshell/test/navigation/test_sessionhistory_iframe_removal.html
new file mode 100644
index 0000000000..242e3baade
--- /dev/null
+++ b/docshell/test/navigation/test_sessionhistory_iframe_removal.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Session history + document.write</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_sessionhistory_iframe_removal.html", "", "width=360,height=480");
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_sessionstorage_across_coop.html b/docshell/test/navigation/test_sessionstorage_across_coop.html
new file mode 100644
index 0000000000..1195f8767c
--- /dev/null
+++ b/docshell/test/navigation/test_sessionstorage_across_coop.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1790666
+-->
+<head>
+ <title>Test for Bug 1790666</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790666">Mozilla Bug 1790666</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ async function singleTest(testValue, description) {
+ info(`Starting test: ${description}`);
+
+ let bc = new BroadcastChannel("testChannel");
+ let promise = new Promise(resolve => {
+ bc.addEventListener("message", event => {
+ info(`received message from testChannel: ${event.data}`);
+ resolve(event.data);
+ }, { once: true });
+ });
+
+ info("Opening pop-up");
+ let popup = window.open("", "_blank");
+ popup.sessionStorage.setItem("testItem", testValue);
+
+ info("Navigating pop-up to COOP page");
+ popup.location = new URL("file_sessionstorage_across_coop.html", window.location);
+
+ let newValue = await promise;
+ is(newValue, testValue, "Value matches expected value");
+ }
+
+ add_task(async function() {
+ // Cross-Origin-Opener-Policy is only supported in secure contexts, so
+ // make the test a secure context.
+ await SpecialPowers.pushPrefEnv({
+ "set": [["dom.securecontext.allowlist", "mochi.test"]],
+ });
+
+ await singleTest("short test value", "short test value");
+
+ let longValue = "A".repeat(SpecialPowers.getIntPref("browser.sessionstore.dom_storage_limit") * 2);
+ await singleTest(longValue, "long test value");
+ });
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_shiftReload_and_pushState.html b/docshell/test/navigation/test_shiftReload_and_pushState.html
new file mode 100644
index 0000000000..7525e2e21f
--- /dev/null
+++ b/docshell/test/navigation/test_shiftReload_and_pushState.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for Bug 1003100</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1003100">Mozilla Bug 1003100</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_shiftReload_and_pushState.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_ship_beforeunload_fired.html b/docshell/test/navigation/test_ship_beforeunload_fired.html
new file mode 100644
index 0000000000..e43711676b
--- /dev/null
+++ b/docshell/test/navigation/test_ship_beforeunload_fired.html
@@ -0,0 +1,63 @@
+<html>
+ <head>
+ <title>
+ Test that ensures beforeunload is fired when session-history-in-parent is enabled
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ /*
+ * This test ensures beforeunload is fired on the current page
+ * when it is entering BFCache and the next page is coming out
+ * from BFCache
+ *
+ * (1) The controller page opens a new window, and page A is loaded there.
+ * (2) Page A then navigates to page B, and a beforeunload event
+ * listener is registered on page B.
+ * (3) Page B then navigates back to page A, and the beforeunload handler
+ * should send a message to the controller page.
+ * (4) Page A then navigates back to page B to check if page B has
+ * been successfully added to BFCache.
+ */
+
+ var bc = new BroadcastChannel("ship_beforeunload");
+ var pageshowCount = 0;
+ var beforeUnloadFired = false;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ bc.postMessage({action: "navigate_to_page_b"});
+ } else if (pageshowCount == 2) {
+ ok(!event.data.persisted, "?pageb shouldn't in BFCache because it's the first navigation");
+ bc.postMessage({action: "register_beforeunload", loadNextPageFromSessionHistory: true});
+ } else if (pageshowCount == 3) {
+ ok(event.data.persisted, "navigated back to page A that was in BFCacache from page B");
+ ok(beforeUnloadFired, "beforeunload has fired on page B");
+ bc.postMessage({action: "back_to_page_b", forwardNavigateToPageB: true});
+ } else if (pageshowCount == 4) {
+ ok(event.data.persisted, "page B has beforeunload fired and also entered BFCache");
+ bc.postMessage({action: "close"});
+ SimpleTest.finish();
+ }
+ } else if (event.data == "beforeunload_fired") {
+ beforeUnloadFired = true;
+ }
+ }
+
+ function runTest() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["fission.bfcacheInParent", true],
+ ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", true]
+ ]},
+ function() {
+ window.open("file_ship_beforeunload_fired.html", "", "noopener");
+ }
+ );
+ }
+ </script>
+ <body onload="runTest()"></body>
+</html>
diff --git a/docshell/test/navigation/test_ship_beforeunload_fired_2.html b/docshell/test/navigation/test_ship_beforeunload_fired_2.html
new file mode 100644
index 0000000000..93669502a5
--- /dev/null
+++ b/docshell/test/navigation/test_ship_beforeunload_fired_2.html
@@ -0,0 +1,65 @@
+<html>
+ <head>
+ <title>
+ Test that ensures beforeunload is fired when session-history-in-parent is enabled
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ /*
+ * This test ensures beforeunload is fired on the current page
+ * when it is entering BFCache, and the next page is not coming from
+ * session history and also not coming out from BFCache.
+ *
+ * (1) The controller page opens a new window, and page A is loaded there.
+ * (2) Page A then navigates to page B, and a beforeunload event
+ * listener is registered on page B.
+ * (3) Page B then navigates to page C, and the beforeunload handler
+ * should send a message to the controller page.
+ * (4) Page C then navigates back to page B to check if page B has
+ * been successfully added to BFCache.
+ */
+
+ var bc = new BroadcastChannel("ship_beforeunload");
+ var pageshowCount = 0;
+
+ var beforeUnloadFired = false;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ bc.postMessage({action: "navigate_to_page_b"});
+ } else if (pageshowCount == 2) {
+ ok(!event.data.persisted, "?page B shouldn't in BFCache because it's the first navigation");
+ bc.postMessage({action: "register_beforeunload",
+ loadNextPageFromSessionHistory: false});
+ } else if (pageshowCount == 3) {
+ ok(!event.data.persisted, "navigated to page C that was a new page");
+ ok(beforeUnloadFired, "beforeUnload should be fired on page B");
+ bc.postMessage({action: "back_to_page_b", forwardNavigateToPageB: false});
+ } else if (pageshowCount == 4) {
+ ok(event.data.persisted, "page B has been successfully added to BFCache");
+ bc.postMessage({action: "close"});
+ SimpleTest.finish();
+ }
+ } else if (event.data == "beforeunload_fired") {
+ beforeUnloadFired = true;
+ }
+ }
+
+ function runTest() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["fission.bfcacheInParent", true],
+ ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", true]
+ ]},
+ function() {
+ window.open("file_ship_beforeunload_fired.html", "", "noopener");
+ }
+ );
+ }
+ </script>
+ <body onload="runTest()"></body>
+</html>
diff --git a/docshell/test/navigation/test_ship_beforeunload_fired_3.html b/docshell/test/navigation/test_ship_beforeunload_fired_3.html
new file mode 100644
index 0000000000..8951f269c5
--- /dev/null
+++ b/docshell/test/navigation/test_ship_beforeunload_fired_3.html
@@ -0,0 +1,65 @@
+<html>
+ <head>
+ <title>
+ Test that ensures beforeunload is fired when session-history-in-parent is enabled
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ /*
+ * This test ensures beforeunload is fired on the current page
+ * when it is entering BFCache, and the next page is not coming out
+ * from BFCache, but coming from session history.
+ *
+ * (1) The controller page opens a new window, and page A is loaded there.
+ * (2) Page A then navigates to page B, and a beforeunload event
+ * listener is registered on page B.
+ * (3) Page B then navigates back to page A, and the beforeunload handler
+ * should send a message to the controller page.
+ * (4) Page A then navigates back to page B to check if page B has
+ * been successfully added to BFCache.
+ */
+
+ var bc = new BroadcastChannel("ship_beforeunload");
+ var pageshowCount = 0;
+
+ var beforeUnloadFired = false;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ bc.postMessage({action: "navigate_to_page_b", blockBFCache: true});
+ } else if (pageshowCount == 2) {
+ ok(!event.data.persisted, "?page B shouldn't in BFCache because it's the first navigation");
+ bc.postMessage({action: "register_beforeunload",
+ loadNextPageFromSessionHistory: true});
+ } else if (pageshowCount == 3) {
+ ok(!event.data.persisted, "navigated back to page A that was session history but not in BFCache");
+ ok(beforeUnloadFired, "beforeUnload should be fired on page B");
+ bc.postMessage({action: "back_to_page_b", forwardNavigateToPageB: true});
+ } else if (pageshowCount == 4) {
+ ok(event.data.persisted, "page B has been successfully added to BFCache");
+ bc.postMessage({action: "close"});
+ SimpleTest.finish();
+ }
+ } else if (event.data == "beforeunload_fired") {
+ beforeUnloadFired = true;
+ }
+ }
+
+ function runTest() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["fission.bfcacheInParent", true],
+ ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", true]
+ ]},
+ function() {
+ window.open("file_ship_beforeunload_fired.html", "", "noopener");
+ }
+ );
+ }
+ </script>
+ <body onload="runTest()"></body>
+</html>
diff --git a/docshell/test/navigation/test_sibling-matching-parent.html b/docshell/test/navigation/test_sibling-matching-parent.html
new file mode 100644
index 0000000000..3c1bc768db
--- /dev/null
+++ b/docshell/test/navigation/test_sibling-matching-parent.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+window.onload = async function() {
+ document.getElementById("active").innerHTML =
+ '<iframe src="navigate.html#parent.frames[0],location"></iframe>' +
+ '<iframe src="navigate.html#child1,open"></iframe>' +
+ '<iframe src="navigate.html#child2,form"></iframe>' +
+ '<iframe src="navigate.html#child3,hyperlink"></iframe>';
+
+ await waitForFinishedFrames(4);
+
+ await isNavigated(frames[0], "Should be able to navigate sibling with on-domain parent by setting location.");
+ await isNavigated(frames[1], "Should be able to navigate sibling with on-domain parent by calling window.open.");
+ await isNavigated(frames[2], "Should be able to navigate sibling with on-domain parent by submitting form.");
+ await isNavigated(frames[3], "Should be able to navigate sibling with on-domain parent by targeted hyperlink.");
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<div id="frames">
+<iframe name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+<iframe name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe>
+</div>
+<div id="active"></div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_sibling-off-domain.html b/docshell/test/navigation/test_sibling-off-domain.html
new file mode 100644
index 0000000000..cd70d1ae91
--- /dev/null
+++ b/docshell/test/navigation/test_sibling-off-domain.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+ <style type="text/css">
+ iframe { width: 90%; height: 50px; }
+ </style>
+<script>
+window.onload = async function() {
+ document.getElementById("active").innerHTML =
+ '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#parent.frames[0],location"></iframe>' +
+ '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#child1,open"></iframe>' +
+ '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#child2,form"></iframe>' +
+ '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#child3,hyperlink"></iframe>';
+
+ await waitForFinishedFrames(4);
+
+ isBlank(frames[0], "Should not be able to navigate off-domain sibling by setting location.");
+ isBlank(frames[1], "Should not be able to navigate off-domain sibling by calling window.open.");
+ isBlank(frames[2], "Should not be able to navigate off-domain sibling by submitting form.");
+ isBlank(frames[3], "Should not be able to navigate off-domain sibling by targeted hyperlink.");
+
+ await cleanupWindows();
+ SimpleTest.finish();
+};
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a>
+<div id="frames">
+<iframe name="child0" src="blank.html"></iframe>
+<iframe name="child1" src="blank.html"></iframe>
+<iframe name="child2" src="blank.html"></iframe>
+<iframe name="child3" src="blank.html"></iframe>
+</div>
+<div id="active"></div>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_state_size.html b/docshell/test/navigation/test_state_size.html
new file mode 100644
index 0000000000..f089a460ec
--- /dev/null
+++ b/docshell/test/navigation/test_state_size.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test the max size of the data parameter of push/replaceState</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function test() {
+ let tooLarge = SpecialPowers.getIntPref("browser.history.maxStateObjectSize");
+ let allowed = Math.floor(tooLarge / 2);
+
+ history.pushState(new Array(allowed).join("a"), "");
+ ok(true, "Adding a state should succeed.");
+
+ try {
+ history.pushState(new Array(tooLarge).join("a"), "");
+ ok(false, "Adding a too large state object should fail.");
+ } catch(ex) {
+ ok(true, "Adding a too large state object should fail.");
+ }
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_static_and_dynamic.html b/docshell/test/navigation/test_static_and_dynamic.html
new file mode 100644
index 0000000000..ff72a8188c
--- /dev/null
+++ b/docshell/test/navigation/test_static_and_dynamic.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <title>Test for static and dynamic frames and forward-back </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTest()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ var testCount = 0; // Used by the test files.
+
+ SimpleTest.waitForExplicitFinish();
+
+ var testWindow;
+ function runTest() {
+ testWindow = window.open("file_static_and_dynamic_1.html", "", "width=360,height=480");
+ testWindow.onunload = function() { }; // to prevent bfcache
+ }
+
+ function finishTest() {
+ testWindow.close();
+ SimpleTest.finish();
+ }
+ </script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_triggeringprincipal_frame_nav.html b/docshell/test/navigation/test_triggeringprincipal_frame_nav.html
new file mode 100644
index 0000000000..d7046e9236
--- /dev/null
+++ b/docshell/test/navigation/test_triggeringprincipal_frame_nav.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1181370 - Test triggeringPrincipal for iframe navigations</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe1"></iframe>
+<iframe style="width:100%;" id="testframe2"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ *
+ * +------------------------------------+
+ * | +----------+ +--------------+ |
+ * | | Frame 1 | | Frame 2 | |
+ * | +----------+ | | |
+ * | | +----------+ | |
+ * | | | Subframe | | |
+ * | | +----------+ | |
+ * | +--------------+ |
+ * +------------------------------------+
+ *
+ * Frame1: test1.mochi.test
+ * Frame2: test2.mochi.test
+ * Subframe: test2.mochi.test
+ *
+ * (*) Frame1 and Subframe set their document.domain to mochi.test
+ * (*) Frame1 navigates the Subframe
+ * (*) TriggeringPrincipal for the Subframe navigation should be
+ * ==> test1.mochi.test
+ * (*) LoadingPrincipal for the Subframe navigation should be
+ * ==> test2.mochi.test
+ */
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const BASEDOMAIN1 = "http://test1.mochi.test:8888/";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const BASEDOMAIN2 = "http://test2.mochi.test:8888/";
+const PATH = "tests/docshell/test/navigation/";
+const BASEURL1 = BASEDOMAIN1 + PATH;
+const BASEURL2 = BASEDOMAIN2 + PATH;
+const TRIGGERINGPRINCIPALURI = BASEURL1 + "file_triggeringprincipal_frame_1.html";
+const LOADINGPRINCIPALURI = BASEURL2 + "file_triggeringprincipal_frame_2.html";
+
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ is(event.data.triggeringPrincipalURI, TRIGGERINGPRINCIPALURI,
+ "TriggeringPrincipal should be the navigating iframe (Frame 1)");
+ is(event.data.loadingPrincipalURI, LOADINGPRINCIPALURI,
+ "LoadingPrincipal should be the enclosing iframe (Frame 2)");
+ is(event.data.referrerURI, BASEDOMAIN1,
+ "The path of Referrer should be trimmed (Frame 1)");
+
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+var frame1 = document.getElementById("testframe1");
+frame1.src = BASEURL1 + "file_triggeringprincipal_frame_1.html";
+
+var frame2 = document.getElementById("testframe2");
+frame2.src = BASEURL2 + "file_triggeringprincipal_frame_2.html";
+
+</script>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html b/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html
new file mode 100644
index 0000000000..4483efb13e
--- /dev/null
+++ b/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1639195 - Test triggeringPrincipal for iframe same-origin navigations</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe" src="http://example.com/"></iframe>
+
+<script type="text/javascript">
+/* We load an third-party iframe which then gets navigated by the iframe's
+ * parent by calling iframe.setAttribute("src", same-origin url) later in the
+ * test. We then verify the TriggeringPrincipal and LoadingPrincipal of the
+ * navigated iframe.
+ *
+ * +------------------------------------------+
+ * | |
+ * | +------------------+ |
+ * | | testframe | |
+ * | +------------------+ |
+ * | |
+ * | iframe.setAttribute("src", |
+ * | same-origin url) |
+ * | |
+ * +------------------------------------------+
+ */
+
+var testframe = document.getElementById("testframe");
+
+window.addEventListener("message", receiveMessage);
+
+const TRIGGERING_PRINCIPAL_URI =
+ "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html";
+
+const LOADING_PRINCIPAL_URI = TRIGGERING_PRINCIPAL_URI;
+
+function receiveMessage(event) {
+ is(event.data.triggeringPrincipalURI.split("?")[0], TRIGGERING_PRINCIPAL_URI,
+ "TriggeringPrincipal should be the parent iframe");
+ is(event.data.loadingPrincipalURI.split("?")[0], TRIGGERING_PRINCIPAL_URI,
+ "LoadingPrincipal should be the parent iframe");
+
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function performNavigation() {
+ testframe.removeEventListener("load", performNavigation);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ testframe.setAttribute("src", "http://example.com/tests/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html");
+}
+
+// start the test
+SimpleTest.waitForExplicitFinish();
+
+testframe.addEventListener("load", performNavigation);
+</script>
+
+</body>
+</html>
diff --git a/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html b/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html
new file mode 100644
index 0000000000..115c5f4462
--- /dev/null
+++ b/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+</head>
+<body>
+
+<iframe name="framea" id="framea" src="file_triggeringprincipal_iframe_iframe_window_open_frame_a.html"></iframe>
+<iframe name="frameb" id="frameb"></iframe>
+
+<script type="text/javascript">
+
+/* We load an iframe (Frame A) which then gets navigated by another iframe (Frame B)
+ * by calling window.open("http://", "Frame A") later in the test. We then verify the
+ * TriggeringPrincipal and LoadingPrincipal of the navigated iframe (Frame A).
+ *
+ * +---------------------------------------+
+ * | Parent |
+ * | |
+ * | +----------------------------+ |
+ * | | Frame A | |
+ * | | | |
+ * | | | |
+ * | +----------------------------+ |
+ * | |
+ * | +----------------------------+ |
+ * | | Frame B | |
+ * | | | |
+ * | | win.open("http://", "A") | |
+ * | +----------------------------+ |
+ * | |
+ * +---------------------------------------+
+ *
+ * Sequence of the test:
+ * [1] load Frame A
+ * [2] load Frame B which navigates A
+ * [3] load navigated Frame A and check triggeringPrincipal and loadingPrincipal
+ */
+
+const TRIGGERING_PRINCIPAL_URI =
+ "http://mochi.test:8888/tests/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html";
+
+const LOADING_PRINCIPAL_URI =
+ "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html";
+
+var frameA = document.getElementById("framea");
+
+function checkResults() {
+ frameA.removeEventListener("load", checkResults);
+
+ var channel = SpecialPowers.wrap(frameA.contentWindow).docShell.currentDocumentChannel;
+ var triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec;
+ var loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec;
+
+ is(triggeringPrincipal, TRIGGERING_PRINCIPAL_URI,
+ "TriggeringPrincipal for targeted window.open() should be the iframe triggering the load");
+
+ is(frameA.contentDocument.referrer, TRIGGERING_PRINCIPAL_URI,
+ "Referrer for targeted window.open() should be the principal of the iframe triggering the load");
+
+ is(loadingPrincipal.split("?")[0], LOADING_PRINCIPAL_URI,
+ "LoadingPrincipal for targeted window.open() should be the containing document");
+
+ SimpleTest.finish();
+}
+
+function performNavigation() {
+ frameA.removeEventListener("load", performNavigation);
+ frameA.addEventListener("load", checkResults);
+
+ // load Frame B which then navigates Frame A
+ var frameB = document.getElementById("frameb");
+ frameB.src = "file_triggeringprincipal_iframe_iframe_window_open_frame_b.html";
+}
+
+// start the test
+SimpleTest.waitForExplicitFinish();
+
+frameA.addEventListener("load", performNavigation);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html b/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html
new file mode 100644
index 0000000000..1611ebf479
--- /dev/null
+++ b/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+</head>
+<body>
+
+<iframe name="testframe" id="testframe" src="file_triggeringprincipal_iframe_iframe_window_open_base.html"></iframe>
+
+<script type="text/javascript">
+
+/* We load an iframe which then gets navigated by the iframe's parent by calling
+ * window.open("http://", iframe) later in the test. We then verify the
+ * TriggeringPrincipal and LoadingPrincipal of the navigated iframe.
+ *
+ * +------------------------------------------+
+ * | |
+ * | +------------------+ |
+ * | | testframe | |
+ * | +------------------+ |
+ * | |
+ * | window.open("http://", "testframe"); |
+ * | |
+ * +------------------------------------------+
+ */
+
+const TRIGGERING_PRINCIPAL_URI =
+ "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html";
+
+const LOADING_PRINCIPAL_URI = TRIGGERING_PRINCIPAL_URI;
+
+var testframe = document.getElementById("testframe");
+
+function checkResults() {
+ testframe.removeEventListener("load", checkResults);
+
+ var channel = SpecialPowers.wrap(testframe.contentWindow).docShell.currentDocumentChannel;
+ var triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec.split("?")[0];
+ var loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec.split("?")[0];
+
+ is(triggeringPrincipal, TRIGGERING_PRINCIPAL_URI,
+ "TriggeringPrincipal for targeted window.open() should be the principal of the document");
+
+ is(testframe.contentDocument.referrer.split("?")[0], TRIGGERING_PRINCIPAL_URI,
+ "Referrer for targeted window.open() should be the principal of the document");
+
+ is(loadingPrincipal, LOADING_PRINCIPAL_URI,
+ "LoadingPrincipal for targeted window.open() should be the <iframe>.ownerDocument");
+
+ SimpleTest.finish();
+}
+
+function performNavigation() {
+ testframe.removeEventListener("load", performNavigation);
+ testframe.addEventListener("load", checkResults);
+ window.open("file_triggeringprincipal_parent_iframe_window_open_nav.html", "testframe");
+}
+
+// start the test
+SimpleTest.waitForExplicitFinish();
+
+testframe.addEventListener("load", performNavigation);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/navigation/test_triggeringprincipal_window_open.html b/docshell/test/navigation/test_triggeringprincipal_window_open.html
new file mode 100644
index 0000000000..439a125f97
--- /dev/null
+++ b/docshell/test/navigation/test_triggeringprincipal_window_open.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="NavigationUtils.js"></script>
+</head>
+<body>
+
+<script type="text/javascript">
+
+/* We call window.open() using different URIs and make sure the triggeringPrincipal
+ * loadingPrincipal are correct.
+ * Test1: window.open(http:)
+ * Test2: window.open(javascript:)
+ */
+
+const TRIGGERING_PRINCIPAL_URI =
+ "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_window_open.html";
+
+SimpleTest.waitForExplicitFinish();
+
+const NUM_TESTS = 2;
+var test_counter = 0;
+
+function checkFinish() {
+ test_counter++;
+ if (test_counter === NUM_TESTS) {
+ SimpleTest.finish();
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Test 1: window.open(http:)
+var httpWin = window.open("file_triggeringprincipal_window_open.html", "_blank", "width=10,height=10");
+httpWin.onload = function() {
+ var httpChannel = SpecialPowers.wrap(httpWin).docShell.currentDocumentChannel;
+ var httpTriggeringPrincipal = httpChannel.loadInfo.triggeringPrincipal.asciiSpec;
+ var httpLoadingPrincipal = httpChannel.loadInfo.loadingPrincipal;
+
+ is(httpTriggeringPrincipal.split("?")[0], TRIGGERING_PRINCIPAL_URI,
+ "TriggeringPrincipal for window.open(http:) should be the principal of the document");
+
+ is(httpWin.document.referrer.split("?")[0], TRIGGERING_PRINCIPAL_URI,
+ "Referrer for window.open(http:) should be the principal of the document");
+
+ is(httpLoadingPrincipal, null,
+ "LoadingPrincipal for window.open(http:) should be null");
+
+ httpWin.close();
+ checkFinish();
+};
+
+// ----------------------------------------------------------------------------
+// Test 2: window.open(javascript:)
+var jsWin = window.open("javascript:'<html><body>js</body></html>';", "_blank", "width=10,height=10");
+jsWin.onload = function() {
+ var jsChannel = SpecialPowers.wrap(jsWin).docShell.currentDocumentChannel;
+ var jsTriggeringPrincipal = jsChannel.loadInfo.triggeringPrincipal.asciiSpec;
+ var jsLoadingPrincipal = jsChannel.loadInfo.loadingPrincipal;
+
+ is(jsTriggeringPrincipal.split("?")[0], TRIGGERING_PRINCIPAL_URI,
+ "TriggeringPrincipal for window.open(javascript:) should be the principal of the document");
+
+ is(jsWin.document.referrer, "",
+ "Referrer for window.open(javascript:) should be empty");
+
+ is(jsLoadingPrincipal, null,
+ "LoadingPrincipal for window.open(javascript:) should be null");
+
+ jsWin.close();
+ checkFinish();
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/docshell/test/unit/AllowJavascriptChild.sys.mjs b/docshell/test/unit/AllowJavascriptChild.sys.mjs
new file mode 100644
index 0000000000..a7c3fa6172
--- /dev/null
+++ b/docshell/test/unit/AllowJavascriptChild.sys.mjs
@@ -0,0 +1,41 @@
+export class AllowJavascriptChild extends JSWindowActorChild {
+ async receiveMessage(msg) {
+ switch (msg.name) {
+ case "CheckScriptsAllowed":
+ return this.checkScriptsAllowed();
+ case "CheckFiredLoadEvent":
+ return this.contentWindow.wrappedJSObject.gFiredOnload;
+ case "CreateIframe":
+ return this.createIframe(msg.data.url);
+ }
+ return null;
+ }
+
+ handleEvent(event) {
+ if (event.type === "load") {
+ this.sendAsyncMessage("LoadFired");
+ }
+ }
+
+ checkScriptsAllowed() {
+ let win = this.contentWindow;
+
+ win.wrappedJSObject.gFiredOnclick = false;
+ win.document.body.click();
+ return win.wrappedJSObject.gFiredOnclick;
+ }
+
+ async createIframe(url) {
+ let doc = this.contentWindow.document;
+
+ let iframe = doc.createElement("iframe");
+ iframe.src = url;
+ doc.body.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+
+ return iframe.browsingContext;
+ }
+}
diff --git a/docshell/test/unit/AllowJavascriptParent.sys.mjs b/docshell/test/unit/AllowJavascriptParent.sys.mjs
new file mode 100644
index 0000000000..5631fcdb09
--- /dev/null
+++ b/docshell/test/unit/AllowJavascriptParent.sys.mjs
@@ -0,0 +1,28 @@
+let loadPromises = new WeakMap();
+
+export class AllowJavascriptParent extends JSWindowActorParent {
+ async receiveMessage(msg) {
+ switch (msg.name) {
+ case "LoadFired":
+ let bc = this.browsingContext;
+ let deferred = loadPromises.get(bc);
+ if (deferred) {
+ loadPromises.delete(bc);
+ deferred.resolve(this);
+ }
+ break;
+ }
+ }
+
+ static promiseLoad(bc) {
+ let deferred = loadPromises.get(bc);
+ if (!deferred) {
+ deferred = {};
+ deferred.promise = new Promise(resolve => {
+ deferred.resolve = resolve;
+ });
+ loadPromises.set(bc, deferred);
+ }
+ return deferred.promise;
+ }
+}
diff --git a/docshell/test/unit/data/engine.xml b/docshell/test/unit/data/engine.xml
new file mode 100644
index 0000000000..3a2bd85c1b
--- /dev/null
+++ b/docshell/test/unit/data/engine.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>test_urifixup_search_engine</ShortName>
+<Description>test_urifixup_search_engine</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="https://www.example.org/">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.example.org/</SearchForm>
+</SearchPlugin>
diff --git a/docshell/test/unit/data/enginePost.xml b/docshell/test/unit/data/enginePost.xml
new file mode 100644
index 0000000000..14775b6f0a
--- /dev/null
+++ b/docshell/test/unit/data/enginePost.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>test_urifixup_search_engine_post</ShortName>
+<Description>test_urifixup_search_engine_post</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="POST" template="https://www.example.org/">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.example.org/</SearchForm>
+</SearchPlugin>
diff --git a/docshell/test/unit/data/enginePrivate.xml b/docshell/test/unit/data/enginePrivate.xml
new file mode 100644
index 0000000000..7d87de98fa
--- /dev/null
+++ b/docshell/test/unit/data/enginePrivate.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>test_urifixup_search_engine_private</ShortName>
+<Description>test_urifixup_search_engine_private</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="https://www.example.org/">
+ <Param name="private" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.example.org/</SearchForm>
+</SearchPlugin>
diff --git a/docshell/test/unit/head_docshell.js b/docshell/test/unit/head_docshell.js
new file mode 100644
index 0000000000..1d74bd61c7
--- /dev/null
+++ b/docshell/test/unit/head_docshell.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ HttpServer: "resource://testing-common/httpd.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+var profileDir = do_get_profile();
+
+const kSearchEngineID = "test_urifixup_search_engine";
+const kSearchEngineURL = "https://www.example.org/?search={searchTerms}";
+const kPrivateSearchEngineID = "test_urifixup_search_engine_private";
+const kPrivateSearchEngineURL =
+ "https://www.example.org/?private={searchTerms}";
+const kPostSearchEngineID = "test_urifixup_search_engine_post";
+const kPostSearchEngineURL = "https://www.example.org/";
+const kPostSearchEngineData = "q={searchTerms}";
+
+const SEARCH_CONFIG = [
+ {
+ appliesTo: [
+ {
+ included: {
+ everywhere: true,
+ },
+ },
+ ],
+ default: "yes",
+ webExtension: {
+ id: "fixup_search@search.mozilla.org",
+ },
+ },
+];
+
+async function setupSearchService() {
+ SearchTestUtils.init(this);
+
+ AddonTestUtils.init(this);
+ AddonTestUtils.overrideCertDB();
+ AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+ );
+
+ await SearchTestUtils.useTestEngines(".", null, SEARCH_CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+}
+
+/**
+ * After useHttpServer() is called, this string contains the URL of the "data"
+ * directory, including the final slash.
+ */
+var gDataUrl;
+
+/**
+ * Initializes the HTTP server and ensures that it is terminated when tests end.
+ *
+ * @param {string} dir
+ * The test sub-directory to use for the engines.
+ * @returns {HttpServer}
+ * The HttpServer object in case further customization is needed.
+ */
+function useHttpServer(dir = "data") {
+ let httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerDirectory("/", do_get_cwd());
+ gDataUrl = `http://localhost:${httpServer.identity.primaryPort}/${dir}/`;
+ registerCleanupFunction(async function cleanup_httpServer() {
+ await new Promise(resolve => {
+ httpServer.stop(resolve);
+ });
+ });
+ return httpServer;
+}
+
+async function addTestEngines() {
+ useHttpServer();
+ // This is a hack, ideally we should be setting up a configuration with
+ // built-in engines, but the `chrome_settings_overrides` section that
+ // WebExtensions need is only defined for browser/
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}/engine.xml`,
+ });
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}/enginePrivate.xml`,
+ });
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}/enginePost.xml`,
+ });
+}
diff --git a/docshell/test/unit/test_URIFixup.js b/docshell/test/unit/test_URIFixup.js
new file mode 100644
index 0000000000..794ee8cfa2
--- /dev/null
+++ b/docshell/test/unit/test_URIFixup.js
@@ -0,0 +1,169 @@
+var pref = "browser.fixup.typo.scheme";
+
+var data = [
+ {
+ // ttp -> http.
+ wrong: "ttp://www.example.com/",
+ fixed: "http://www.example.com/",
+ },
+ {
+ // htp -> http.
+ wrong: "htp://www.example.com/",
+ fixed: "http://www.example.com/",
+ },
+ {
+ // ttps -> https.
+ wrong: "ttps://www.example.com/",
+ fixed: "https://www.example.com/",
+ },
+ {
+ // tps -> https.
+ wrong: "tps://www.example.com/",
+ fixed: "https://www.example.com/",
+ },
+ {
+ // ps -> https.
+ wrong: "ps://www.example.com/",
+ fixed: "https://www.example.com/",
+ },
+ {
+ // htps -> https.
+ wrong: "htps://www.example.com/",
+ fixed: "https://www.example.com/",
+ },
+ {
+ // ile -> file.
+ wrong: "ile:///this/is/a/test.html",
+ fixed: "file:///this/is/a/test.html",
+ },
+ {
+ // le -> file.
+ wrong: "le:///this/is/a/test.html",
+ fixed: "file:///this/is/a/test.html",
+ },
+ {
+ // Replace ';' with ':'.
+ wrong: "http;//www.example.com/",
+ fixed: "http://www.example.com/",
+ noPrefValue: "http://http;//www.example.com/",
+ },
+ {
+ // Missing ':'.
+ wrong: "https//www.example.com/",
+ fixed: "https://www.example.com/",
+ noPrefValue: "http://https//www.example.com/",
+ },
+ {
+ // Missing ':' for file scheme.
+ wrong: "file///this/is/a/test.html",
+ fixed: "file:///this/is/a/test.html",
+ noPrefValue: "http://file///this/is/a/test.html",
+ },
+ {
+ // Valid should not be changed.
+ wrong: "https://example.com/this/is/a/test.html",
+ fixed: "https://example.com/this/is/a/test.html",
+ },
+ {
+ // Unmatched should not be changed.
+ wrong: "whatever://this/is/a/test.html",
+ fixed: "whatever://this/is/a/test.html",
+ },
+ {
+ // Spaces before @ are valid if it appears after the domain.
+ wrong: "example.com/ @test.com",
+ fixed: "http://example.com/%20@test.com",
+ noPrefValue: "http://example.com/%20@test.com",
+ },
+];
+
+var dontFixURIs = [
+ {
+ input: " leadingSpaceUsername@example.com/",
+ testInfo: "dont fix usernames with leading space",
+ },
+ {
+ input: "trailingSpacerUsername @example.com/",
+ testInfo: "dont fix usernames with trailing space",
+ },
+ {
+ input: "multiple words username@example.com/",
+ testInfo: "dont fix usernames with multiple spaces",
+ },
+ {
+ input: "one spaceTwo SpacesThree Spaces@example.com/",
+ testInfo: "dont match multiple consecutive spaces",
+ },
+ {
+ input: " dontMatchCredentialsWithSpaces: secret password @example.com/",
+ testInfo: "dont fix credentials with spaces",
+ },
+];
+
+var len = data.length;
+
+add_task(async function setup() {
+ await setupSearchService();
+ // Now we've initialised the search service, we force remove the engines
+ // it has, so they don't interfere with this test.
+ // Search engine integration is tested in test_URIFixup_search.js.
+ Services.search.wrappedJSObject._engines.clear();
+});
+
+// Make sure we fix what needs fixing when there is no pref set.
+add_task(function test_unset_pref_fixes_typos() {
+ Services.prefs.clearUserPref(pref);
+ for (let i = 0; i < len; ++i) {
+ let item = data[i];
+ let { preferredURI } = Services.uriFixup.getFixupURIInfo(
+ item.wrong,
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
+ );
+ Assert.equal(preferredURI.spec, item.fixed);
+ }
+});
+
+// Make sure we don't do anything when the pref is explicitly
+// set to false.
+add_task(function test_false_pref_keeps_typos() {
+ Services.prefs.setBoolPref(pref, false);
+ for (let i = 0; i < len; ++i) {
+ let item = data[i];
+ let { preferredURI } = Services.uriFixup.getFixupURIInfo(
+ item.wrong,
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
+ );
+ Assert.equal(preferredURI.spec, item.noPrefValue || item.wrong);
+ }
+});
+
+// Finally, make sure we still fix what needs fixing if the pref is
+// explicitly set to true.
+add_task(function test_true_pref_fixes_typos() {
+ Services.prefs.setBoolPref(pref, true);
+ for (let i = 0; i < len; ++i) {
+ let item = data[i];
+ let { preferredURI } = Services.uriFixup.getFixupURIInfo(
+ item.wrong,
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
+ );
+ Assert.equal(preferredURI.spec, item.fixed);
+ }
+});
+
+add_task(function test_dont_fix_uris() {
+ let dontFixLength = dontFixURIs.length;
+ for (let i = 0; i < dontFixLength; i++) {
+ let testCase = dontFixURIs[i];
+ Assert.throws(
+ () => {
+ Services.uriFixup.getFixupURIInfo(
+ testCase.input,
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
+ );
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ testCase.testInfo
+ );
+ }
+});
diff --git a/docshell/test/unit/test_URIFixup_check_host.js b/docshell/test/unit/test_URIFixup_check_host.js
new file mode 100644
index 0000000000..132d74ca9b
--- /dev/null
+++ b/docshell/test/unit/test_URIFixup_check_host.js
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const lazy = {};
+
+// Ensure DNS lookups don't hit the server
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gDNSOverride",
+ "@mozilla.org/network/native-dns-override;1",
+ "nsINativeDNSResolverOverride"
+);
+
+add_task(async function setup() {
+ Services.prefs.setStringPref("browser.fixup.alternate.prefix", "www.");
+ Services.prefs.setStringPref("browser.fixup.alternate.suffix", ".com");
+ Services.prefs.setStringPref("browser.fixup.alternate.protocol", "https");
+ Services.prefs.setBoolPref(
+ "browser.urlbar.dnsResolveFullyQualifiedNames",
+ true
+ );
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.fixup.alternate.prefix");
+ Services.prefs.clearUserPref("browser.fixup.alternate.suffix");
+ Services.prefs.clearUserPref("browser.fixup.alternate.protocol");
+ Services.prefs.clearUserPref(
+ "browser.urlbar.dnsResolveFullyQualifiedNames"
+ );
+ });
+});
+
+// TODO: Export Listener from test_dns_override instead of duping
+class Listener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+
+ async firstAddress() {
+ let all = await this.addresses();
+ if (all.length) {
+ return all[0];
+ }
+ return undefined;
+ }
+
+ async addresses() {
+ let [, inRecord] = await this.promise;
+ let addresses = [];
+ if (!inRecord) {
+ return addresses; // returns []
+ }
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ while (inRecord.hasMore()) {
+ addresses.push(inRecord.getNextAddrAsString());
+ }
+ return addresses;
+ }
+
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+const FAKE_IP = "::1";
+const FAKE_INTRANET_IP = "::2";
+const ORIGIN_ATTRIBUTE = {};
+
+add_task(async function test_uri_with_force_fixup() {
+ let listener = new Listener();
+ let { fixedURI } = Services.uriFixup.forceHttpFixup("http://www.example.com");
+
+ lazy.gDNSOverride.addIPOverride(fixedURI.displayHost, FAKE_IP);
+
+ Services.uriFixup.checkHost(fixedURI, listener, ORIGIN_ATTRIBUTE);
+ Assert.equal(
+ await listener.firstAddress(),
+ FAKE_IP,
+ "Should've received fake IP"
+ );
+
+ lazy.gDNSOverride.clearHostOverride(fixedURI.displayHost);
+ Services.dns.clearCache(false);
+});
+
+add_task(async function test_uri_with_get_fixup() {
+ let listener = new Listener();
+ let uri = Services.io.newURI("http://www.example.com");
+
+ lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP);
+
+ Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE);
+ Assert.equal(
+ await listener.firstAddress(),
+ FAKE_IP,
+ "Should've received fake IP"
+ );
+
+ lazy.gDNSOverride.clearHostOverride(uri.displayHost);
+ Services.dns.clearCache(false);
+});
+
+add_task(async function test_intranet_like_uri() {
+ let listener = new Listener();
+ let uri = Services.io.newURI("http://someintranet");
+
+ lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP);
+ // Hosts without periods should end with a period to make them FQN
+ lazy.gDNSOverride.addIPOverride(uri.displayHost + ".", FAKE_INTRANET_IP);
+
+ Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE);
+ Assert.deepEqual(
+ await listener.addresses(),
+ FAKE_INTRANET_IP,
+ "Should've received fake intranet IP"
+ );
+
+ lazy.gDNSOverride.clearHostOverride(uri.displayHost);
+ lazy.gDNSOverride.clearHostOverride(uri.displayHost + ".");
+ Services.dns.clearCache(false);
+});
+
+add_task(async function test_intranet_like_uri_without_fixup() {
+ let listener = new Listener();
+ let uri = Services.io.newURI("http://someintranet");
+ Services.prefs.setBoolPref(
+ "browser.urlbar.dnsResolveFullyQualifiedNames",
+ false
+ );
+
+ lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP);
+ // Hosts without periods should end with a period to make them FQN
+ lazy.gDNSOverride.addIPOverride(uri.displayHost + ".", FAKE_INTRANET_IP);
+
+ Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE);
+ Assert.deepEqual(
+ await listener.addresses(),
+ FAKE_IP,
+ "Should've received non-fixed up IP"
+ );
+
+ lazy.gDNSOverride.clearHostOverride(uri.displayHost);
+ lazy.gDNSOverride.clearHostOverride(uri.displayHost + ".");
+ Services.dns.clearCache(false);
+});
+
+add_task(async function test_ip_address() {
+ let listener = new Listener();
+ let uri = Services.io.newURI("http://192.168.0.1");
+
+ // Testing IP address being passed into the method
+ // requires observing if the asyncResolve method gets called
+ let didResolve = false;
+ let topic = "uri-fixup-check-dns";
+ let observer = {
+ observe(aSubject, aTopicInner, aData) {
+ if (aTopicInner == topic) {
+ didResolve = true;
+ }
+ },
+ };
+ Services.obs.addObserver(observer, topic);
+ lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP);
+
+ Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE);
+ Assert.equal(
+ didResolve,
+ false,
+ "Passing an IP address should not conduct a host lookup"
+ );
+
+ lazy.gDNSOverride.clearHostOverride(uri.displayHost);
+ Services.dns.clearCache(false);
+ Services.obs.removeObserver(observer, topic);
+});
diff --git a/docshell/test/unit/test_URIFixup_external_protocol_fallback.js b/docshell/test/unit/test_URIFixup_external_protocol_fallback.js
new file mode 100644
index 0000000000..0846070b7a
--- /dev/null
+++ b/docshell/test/unit/test_URIFixup_external_protocol_fallback.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test whether fallback mechanism is working if don't trust nsIExternalProtocolService.
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+add_task(async function setup() {
+ info(
+ "Prepare mock nsIExternalProtocolService whose externalProtocolHandlerExists returns always true"
+ );
+ const externalProtocolService = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ const mockId = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ {
+ getProtocolHandlerInfo: scheme =>
+ externalProtocolService.getProtocolHandlerInfo(scheme),
+ externalProtocolHandlerExists: () => true,
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+ }
+ );
+ const mockExternalProtocolService = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ Assert.ok(
+ mockExternalProtocolService.externalProtocolHandlerExists("__invalid__"),
+ "Mock service is working"
+ );
+
+ info("Register new dummy protocol");
+ const dummyProtocolHandlerInfo =
+ externalProtocolService.getProtocolHandlerInfo("dummy");
+ const handlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+ handlerService.store(dummyProtocolHandlerInfo);
+
+ info("Prepare test search engine");
+ await setupSearchService();
+ await addTestEngines();
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kSearchEngineID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ registerCleanupFunction(() => {
+ handlerService.remove(dummyProtocolHandlerInfo);
+ MockRegistrar.unregister(mockId);
+ });
+});
+
+add_task(function basic() {
+ const testData = [
+ {
+ input: "mailto:test@example.com",
+ expected:
+ isSupportedInHandlerService("mailto") ||
+ // Thunderbird IS a mailto handler, it doesn't have handlers.
+ AppConstants.MOZ_APP_NAME == "thunderbird"
+ ? "mailto:test@example.com"
+ : "http://mailto:test@example.com/",
+ },
+ {
+ input: "keyword:search",
+ expected: "https://www.example.org/?search=keyword%3Asearch",
+ },
+ {
+ input: "dummy:protocol",
+ expected: "dummy:protocol",
+ },
+ ];
+
+ for (const { input, expected } of testData) {
+ assertFixup(input, expected);
+ }
+});
+
+function assertFixup(input, expected) {
+ const { preferredURI } = Services.uriFixup.getFixupURIInfo(
+ input,
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
+ );
+ Assert.equal(preferredURI.spec, expected);
+}
+
+function isSupportedInHandlerService(scheme) {
+ const externalProtocolService = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ const handlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+ return handlerService.exists(
+ externalProtocolService.getProtocolHandlerInfo(scheme)
+ );
+}
diff --git a/docshell/test/unit/test_URIFixup_forced.js b/docshell/test/unit/test_URIFixup_forced.js
new file mode 100644
index 0000000000..1d4dfd94a7
--- /dev/null
+++ b/docshell/test/unit/test_URIFixup_forced.js
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var data = [
+ {
+ // singleword.
+ wrong: "http://example/",
+ fixed: "https://www.example.com/",
+ },
+ {
+ // upgrade protocol.
+ wrong: "http://www.example.com/",
+ fixed: "https://www.example.com/",
+ noAlternateURI: true,
+ },
+ {
+ // no difference.
+ wrong: "https://www.example.com/",
+ fixed: "https://www.example.com/",
+ noProtocolFixup: true,
+ noAlternateURI: true,
+ },
+ {
+ // don't add prefix when there's more than one dot.
+ wrong: "http://www.example.abc.def/",
+ fixed: "https://www.example.abc.def/",
+ noAlternateURI: true,
+ },
+ {
+ // http -> https.
+ wrong: "http://www.example/",
+ fixed: "https://www.example/",
+ noAlternateURI: true,
+ },
+ {
+ // domain.com -> https://www.domain.com.
+ wrong: "http://example.com/",
+ fixed: "https://www.example.com/",
+ },
+ {
+ // example/example... -> https://www.example.com/example/
+ wrong: "http://example/example/",
+ fixed: "https://www.example.com/example/",
+ },
+ {
+ // example/example/s#q -> www.example.com/example/s#q.
+ wrong: "http://example/example/s#q",
+ fixed: "https://www.example.com/example/s#q",
+ },
+ {
+ wrong: "http://モジラ.org",
+ fixed: "https://www.xn--yck6dwa.org/",
+ },
+ {
+ wrong: "http://モジラ",
+ fixed: "https://www.xn--yck6dwa.com/",
+ },
+ {
+ wrong: "http://xn--yck6dwa",
+ fixed: "https://www.xn--yck6dwa.com/",
+ },
+ {
+ wrong: "https://モジラ.org",
+ fixed: "https://www.xn--yck6dwa.org/",
+ noProtocolFixup: true,
+ },
+ {
+ wrong: "https://モジラ",
+ fixed: "https://www.xn--yck6dwa.com/",
+ noProtocolFixup: true,
+ },
+ {
+ wrong: "https://xn--yck6dwa",
+ fixed: "https://www.xn--yck6dwa.com/",
+ noProtocolFixup: true,
+ },
+ {
+ // Host is https -> fixup typos of protocol
+ wrong: "htp://https://mozilla.org",
+ fixed: "http://https//mozilla.org",
+ noAlternateURI: true,
+ },
+ {
+ // Host is http -> fixup typos of protocol
+ wrong: "ttp://http://mozilla.org",
+ fixed: "http://http//mozilla.org",
+ noAlternateURI: true,
+ },
+ {
+ // Host is localhost -> fixup typos of protocol
+ wrong: "htps://localhost://mozilla.org",
+ fixed: "https://localhost//mozilla.org",
+ noAlternateURI: true,
+ },
+ {
+ // view-source is not http/https -> error
+ wrong: "view-source:http://example/example/example/example",
+ reject: true,
+ comment: "Scheme should be either http or https",
+ },
+ {
+ // file is not http/https -> error
+ wrong: "file://http://example/example/example/example",
+ reject: true,
+ comment: "Scheme should be either http or https",
+ },
+ {
+ // Protocol is missing -> error
+ wrong: "example.com",
+ reject: true,
+ comment: "Scheme should be either http or https",
+ },
+ {
+ // Empty input -> error
+ wrong: "",
+ reject: true,
+ comment: "Should pass a non-null uri",
+ },
+];
+
+add_task(async function setup() {
+ Services.prefs.setStringPref("browser.fixup.alternate.prefix", "www.");
+ Services.prefs.setStringPref("browser.fixup.alternate.suffix", ".com");
+ Services.prefs.setStringPref("browser.fixup.alternate.protocol", "https");
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.fixup.alternate.prefix");
+ Services.prefs.clearUserPref("browser.fixup.alternate.suffix");
+ Services.prefs.clearUserPref("browser.fixup.alternate.protocol");
+ });
+});
+
+add_task(function test_default_https_pref() {
+ for (let item of data) {
+ if (item.reject) {
+ Assert.throws(
+ () => Services.uriFixup.forceHttpFixup(item.wrong),
+ /NS_ERROR_FAILURE/,
+ item.comment
+ );
+ } else {
+ let { fixupChangedProtocol, fixupCreatedAlternateURI, fixedURI } =
+ Services.uriFixup.forceHttpFixup(item.wrong);
+ Assert.equal(fixedURI.spec, item.fixed, "Specs should be the same");
+ Assert.equal(
+ fixupChangedProtocol,
+ !item.noProtocolFixup,
+ `fixupChangedProtocol should be ${!item.noAlternateURI}`
+ );
+ Assert.equal(
+ fixupCreatedAlternateURI,
+ !item.noAlternateURI,
+ `fixupCreatedAlternateURI should be ${!item.limitedFixup}`
+ );
+ }
+ }
+});
diff --git a/docshell/test/unit/test_URIFixup_info.js b/docshell/test/unit/test_URIFixup_info.js
new file mode 100644
index 0000000000..ec56af1c9b
--- /dev/null
+++ b/docshell/test/unit/test_URIFixup_info.js
@@ -0,0 +1,1079 @@
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const kForceDNSLookup = "browser.fixup.dns_first_for_single_words";
+
+// TODO(bug 1522134), this test should also use
+// combinations of the following flags.
+var flagInputs = [
+ Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP,
+ Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP |
+ Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT,
+ Services.uriFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI,
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS,
+ // This should not really generate a search, but it does, see Bug 1588118.
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT,
+];
+
+/*
+ The following properties are supported for these test cases:
+ {
+ input: "", // Input string, required
+ fixedURI: "", // Expected fixedURI
+ alternateURI: "", // Expected alternateURI
+ keywordLookup: false, // Whether a keyword lookup is expected
+ protocolChange: false, // Whether a protocol change is expected
+ inWhitelist: false, // Whether the input host is in the whitelist
+ affectedByDNSForSingleWordHosts: false, // Whether the input host could be a host, but is normally assumed to be a keyword query
+ }
+*/
+var testcases = [
+ {
+ input: "about:home",
+ fixedURI: "about:home",
+ },
+ {
+ input: "http://www.mozilla.org",
+ fixedURI: "http://www.mozilla.org/",
+ },
+ {
+ input: "http://127.0.0.1/",
+ fixedURI: "http://127.0.0.1/",
+ },
+ {
+ input: "file:///foo/bar",
+ fixedURI: "file:///foo/bar",
+ },
+ {
+ input: "://www.mozilla.org",
+ fixedURI: "http://www.mozilla.org/",
+ protocolChange: true,
+ },
+ {
+ input: "www.mozilla.org",
+ fixedURI: "http://www.mozilla.org/",
+ protocolChange: true,
+ },
+ {
+ input: "http://mozilla/",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ },
+ {
+ input: "http://test./",
+ fixedURI: "http://test./",
+ alternateURI: "https://www.test./",
+ },
+ {
+ input: "127.0.0.1",
+ fixedURI: "http://127.0.0.1/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3.4/",
+ fixedURI: "http://1.2.3.4/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3.4/foo",
+ fixedURI: "http://1.2.3.4/foo",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3.4:8000",
+ fixedURI: "http://1.2.3.4:8000/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3.4:8000/",
+ fixedURI: "http://1.2.3.4:8000/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3.4:8000/foo",
+ fixedURI: "http://1.2.3.4:8000/foo",
+ protocolChange: true,
+ },
+ {
+ input: "192.168.10.110",
+ fixedURI: "http://192.168.10.110/",
+ protocolChange: true,
+ },
+ {
+ input: "192.168.10.110/123",
+ fixedURI: "http://192.168.10.110/123",
+ protocolChange: true,
+ },
+ {
+ input: "192.168.10.110/123foo",
+ fixedURI: "http://192.168.10.110/123foo",
+ protocolChange: true,
+ },
+ {
+ input: "192.168.10.110:1234/123",
+ fixedURI: "http://192.168.10.110:1234/123",
+ protocolChange: true,
+ },
+ {
+ input: "192.168.10.110:1234/123foo",
+ fixedURI: "http://192.168.10.110:1234/123foo",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3",
+ fixedURI: "http://1.2.0.3/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3/",
+ fixedURI: "http://1.2.0.3/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3/foo",
+ fixedURI: "http://1.2.0.3/foo",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3/123",
+ fixedURI: "http://1.2.0.3/123",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3:8000",
+ fixedURI: "http://1.2.0.3:8000/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3:8000/",
+ fixedURI: "http://1.2.0.3:8000/",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3:8000/foo",
+ fixedURI: "http://1.2.0.3:8000/foo",
+ protocolChange: true,
+ },
+ {
+ input: "1.2.3:8000/123",
+ fixedURI: "http://1.2.0.3:8000/123",
+ protocolChange: true,
+ },
+ {
+ input: "http://1.2.3",
+ fixedURI: "http://1.2.0.3/",
+ },
+ {
+ input: "http://1.2.3/",
+ fixedURI: "http://1.2.0.3/",
+ },
+ {
+ input: "http://1.2.3/foo",
+ fixedURI: "http://1.2.0.3/foo",
+ },
+ {
+ input: "[::1]",
+ fixedURI: "http://[::1]/",
+ protocolChange: true,
+ },
+ {
+ input: "[::1]/",
+ fixedURI: "http://[::1]/",
+ protocolChange: true,
+ },
+ {
+ input: "[::1]:8000",
+ fixedURI: "http://[::1]:8000/",
+ protocolChange: true,
+ },
+ {
+ input: "[::1]:8000/",
+ fixedURI: "http://[::1]:8000/",
+ protocolChange: true,
+ },
+ {
+ input: "[[::1]]/",
+ keywordLookup: true,
+ },
+ {
+ input: "[fe80:cd00:0:cde:1257:0:211e:729c]",
+ fixedURI: "http://[fe80:cd00:0:cde:1257:0:211e:729c]/",
+ protocolChange: true,
+ },
+ {
+ input: "[64:ff9b::8.8.8.8]",
+ fixedURI: "http://[64:ff9b::808:808]/",
+ protocolChange: true,
+ },
+ {
+ input: "[64:ff9b::8.8.8.8]/~moz",
+ fixedURI: "http://[64:ff9b::808:808]/~moz",
+ protocolChange: true,
+ },
+ {
+ input: "[::1][::1]",
+ keywordLookup: true,
+ },
+ {
+ input: "[::1][100",
+ keywordLookup: true,
+ },
+ {
+ input: "[::1]]",
+ keywordLookup: true,
+ },
+ {
+ input: "1234",
+ fixedURI: "http://0.0.4.210/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "whitelisted/foo.txt",
+ fixedURI: "http://whitelisted/foo.txt",
+ alternateURI: "https://www.whitelisted.com/foo.txt",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "test.",
+ fixedURI: "http://test./",
+ alternateURI: "https://www.test./",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: ".test",
+ fixedURI: "http://.test/",
+ alternateURI: "https://www.test/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mozilla is amazing",
+ keywordLookup: true,
+ },
+ {
+ input: "search ?mozilla",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla .com",
+ keywordLookup: true,
+ },
+ {
+ input: "what if firefox?",
+ keywordLookup: true,
+ },
+ {
+ input: "london's map",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla ",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: " mozilla ",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mozilla \\",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla \\ foo.txt",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla \\\r foo.txt",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla\n",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mozilla \r\n",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "moz\r\nfirefox\nos\r",
+ fixedURI: "http://mozfirefoxos/",
+ alternateURI: "https://www.mozfirefoxos.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "moz\r\n firefox\n",
+ keywordLookup: true,
+ },
+ {
+ input: "",
+ keywordLookup: true,
+ },
+ {
+ input: "[]",
+ keywordLookup: true,
+ },
+ {
+ input: "http://whitelisted/",
+ fixedURI: "http://whitelisted/",
+ alternateURI: "https://www.whitelisted.com/",
+ inWhitelist: true,
+ },
+ {
+ input: "whitelisted",
+ fixedURI: "http://whitelisted/",
+ alternateURI: "https://www.whitelisted.com/",
+ protocolChange: true,
+ inWhitelist: true,
+ },
+ {
+ input: "whitelisted.",
+ fixedURI: "http://whitelisted./",
+ alternateURI: "https://www.whitelisted./",
+ protocolChange: true,
+ inWhitelist: true,
+ },
+ {
+ input: "mochi.test",
+ fixedURI: "http://mochi.test/",
+ alternateURI: "https://www.mochi.test/",
+ protocolChange: true,
+ inWhitelist: true,
+ },
+ // local.domain is a whitelisted suffix...
+ {
+ input: "some.local.domain",
+ fixedURI: "http://some.local.domain/",
+ protocolChange: true,
+ inWhitelist: true,
+ },
+ // ...but .domain is not.
+ {
+ input: "some.domain",
+ fixedURI: "http://some.domain/",
+ alternateURI: "https://www.some.domain/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "café.com",
+ fixedURI: "http://xn--caf-dma.com/",
+ alternateURI: "https://www.xn--caf-dma.com/",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla.nonexistent",
+ fixedURI: "http://mozilla.nonexistent/",
+ alternateURI: "https://www.mozilla.nonexistent/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mochi.ocm",
+ fixedURI: "http://mochi.com/",
+ alternateURI: "https://www.mochi.com/",
+ protocolChange: true,
+ },
+ {
+ input: "47.6182,-122.830",
+ keywordLookup: true,
+ },
+ {
+ input: "-47.6182,-23.51",
+ keywordLookup: true,
+ },
+ {
+ input: "-22.14,23.51-",
+ fixedURI: "http://-22.14,23.51-/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "32.7",
+ fixedURI: "http://32.0.0.7/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "5+2",
+ fixedURI: "http://5+2/",
+ alternateURI: "https://www.5+2.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "5/2",
+ fixedURI: "http://0.0.0.5/2",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "moz ?.::%27",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla.com/?q=search",
+ fixedURI: "http://mozilla.com/?q=search",
+ alternateURI: "https://www.mozilla.com/?q=search",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla.com?q=search",
+ fixedURI: "http://mozilla.com/?q=search",
+ alternateURI: "https://www.mozilla.com/?q=search",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla.com ?q=search",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla.com.?q=search",
+ fixedURI: "http://mozilla.com./?q=search",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla.com'?q=search",
+ fixedURI: "http://mozilla.com/?q=search",
+ alternateURI: "https://www.mozilla.com/?q=search",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla.com':search",
+ keywordLookup: true,
+ },
+ {
+ input: "[mozilla]",
+ keywordLookup: true,
+ },
+ {
+ input: "':?",
+ fixedURI: "http://'/?",
+ alternateURI: "https://www.'.com/?",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "whitelisted?.com",
+ fixedURI: "http://whitelisted/?.com",
+ alternateURI: "https://www.whitelisted.com/?.com",
+ protocolChange: true,
+ },
+ {
+ input: "?'.com",
+ keywordLookup: true,
+ },
+ {
+ input: ".com",
+ keywordLookup: true,
+ affectedByDNSForSingleWordHosts: true,
+ fixedURI: "http://.com/",
+ alternateURI: "https://www.com/",
+ protocolChange: true,
+ },
+ {
+ input: "' ?.com",
+ keywordLookup: true,
+ },
+ {
+ input: "?mozilla",
+ keywordLookup: true,
+ },
+ {
+ input: "??mozilla",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla/",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ protocolChange: true,
+ keywordLookup: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mozilla5/2",
+ fixedURI: "http://mozilla5/2",
+ alternateURI: "https://www.mozilla5.com/2",
+ protocolChange: true,
+ keywordLookup: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mozilla/foo",
+ fixedURI: "http://mozilla/foo",
+ alternateURI: "https://www.mozilla.com/foo",
+ protocolChange: true,
+ keywordLookup: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "mozilla\\",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "localhost",
+ fixedURI: "http://localhost/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "http",
+ fixedURI: "http://http/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "https",
+ fixedURI: "http://https/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "localhost:8080",
+ fixedURI: "http://localhost:8080/",
+ protocolChange: true,
+ },
+ {
+ input: "plonk:8080",
+ fixedURI: "http://plonk:8080/",
+ protocolChange: true,
+ },
+ {
+ input: "plonk:8080?test",
+ fixedURI: "http://plonk:8080/?test",
+ protocolChange: true,
+ },
+ {
+ input: "plonk:8080#test",
+ fixedURI: "http://plonk:8080/#test",
+ protocolChange: true,
+ },
+ {
+ input: "plonk/ #",
+ fixedURI: "http://plonk/%20#",
+ alternateURI: "https://www.plonk.com/%20#",
+ protocolChange: true,
+ keywordLookup: false,
+ },
+ {
+ input: "blah.com.",
+ fixedURI: "http://blah.com./",
+ protocolChange: true,
+ },
+ {
+ input:
+ "\u10E0\u10D4\u10D2\u10D8\u10E1\u10E2\u10E0\u10D0\u10EA\u10D8\u10D0.\u10D2\u10D4",
+ fixedURI: "http://xn--lodaehvb5cdik4g.xn--node/",
+ alternateURI: "https://www.xn--lodaehvb5cdik4g.xn--node/",
+ protocolChange: true,
+ },
+ {
+ input: " \t mozilla.org/\t \t ",
+ fixedURI: "http://mozilla.org/",
+ alternateURI: "https://www.mozilla.org/",
+ protocolChange: true,
+ },
+ {
+ input: " moz\ti\tlla.org ",
+ keywordLookup: true,
+ },
+ {
+ input: "mozilla/",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla/ test /",
+ fixedURI: "http://mozilla/%20test%20/",
+ alternateURI: "https://www.mozilla.com/%20test%20/",
+ protocolChange: true,
+ },
+ {
+ input: "mozilla /test/",
+ keywordLookup: true,
+ },
+ {
+ input: "pserver:8080",
+ fixedURI: "http://pserver:8080/",
+ protocolChange: true,
+ },
+ {
+ input: "http;mozilla",
+ fixedURI: "http://http;mozilla/",
+ alternateURI: "https://www.http;mozilla.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ },
+ {
+ input: "http//mozilla.org",
+ fixedURI: "http://mozilla.org/",
+ shouldRunTest: flags =>
+ flags & Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS,
+ },
+ {
+ input: "http//mozilla.org",
+ fixedURI: "http://http//mozilla.org",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ shouldRunTest: flags =>
+ !(flags & Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS),
+ },
+ {
+ input: "www.mozilla",
+ fixedURI: "http://www.mozilla/",
+ protocolChange: true,
+ },
+ {
+ input: "https://sub.www..mozilla...org/",
+ fixedURI: "https://sub.www.mozilla.org/",
+ },
+ {
+ input: "sub.www..mozilla...org/",
+ fixedURI: "http://sub.www.mozilla.org/",
+ protocolChange: true,
+ },
+ {
+ input: "sub.www..mozilla...org",
+ fixedURI: "http://sub.www.mozilla.org/",
+ protocolChange: true,
+ },
+ {
+ input: "www...mozilla",
+ fixedURI: "http://www.mozilla/",
+ keywordLookup: true,
+ protocolChange: true,
+ shouldRunTest: flags =>
+ !gSingleWordDNSLookup &&
+ flags & Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP,
+ },
+ {
+ input: "www...mozilla",
+ fixedURI: "http://www.mozilla/",
+ protocolChange: true,
+ shouldRunTest: flags =>
+ gSingleWordDNSLookup ||
+ !(flags & Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP),
+ },
+ {
+ input: "mozilla...org",
+ fixedURI: "http://mozilla.org/",
+ protocolChange: true,
+ },
+ {
+ input: "モジラ...org",
+ fixedURI: "http://xn--yck6dwa.org/",
+ protocolChange: true,
+ },
+ {
+ input: "user@localhost",
+ fixedURI: "http://user@localhost/",
+ protocolChange: true,
+ shouldRunTest: () => gSingleWordDNSLookup,
+ },
+ {
+ input: "user@localhost",
+ fixedURI: "http://user@localhost/",
+ keywordLookup: true,
+ protocolChange: true,
+ shouldRunTest: () => !gSingleWordDNSLookup,
+ },
+ {
+ input: "user@192.168.0.1",
+ fixedURI: "http://user@192.168.0.1/",
+ protocolChange: true,
+ },
+ {
+ input: "user@dummy-host",
+ fixedURI: "http://user@dummy-host/",
+ protocolChange: true,
+ shouldRunTest: () => gSingleWordDNSLookup,
+ },
+ {
+ input: "user@dummy-host",
+ fixedURI: "http://user@dummy-host/",
+ keywordLookup: true,
+ protocolChange: true,
+ shouldRunTest: () => !gSingleWordDNSLookup,
+ },
+ {
+ input: "user:pass@dummy-host",
+ fixedURI: "http://user:pass@dummy-host/",
+ protocolChange: true,
+ },
+ {
+ input: ":pass@dummy-host",
+ fixedURI: "http://:pass@dummy-host/",
+ protocolChange: true,
+ },
+ {
+ input: "user@dummy-host/path",
+ fixedURI: "http://user@dummy-host/path",
+ protocolChange: true,
+ shouldRunTest: () => gSingleWordDNSLookup,
+ },
+ {
+ input: "jar:file:///omni.ja!/",
+ fixedURI: "jar:file:///omni.ja!/",
+ },
+];
+
+if (AppConstants.platform == "win") {
+ testcases.push({
+ input: "C:\\some\\file.txt",
+ fixedURI: "file:///C:/some/file.txt",
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "//mozilla",
+ fixedURI: "http://mozilla/",
+ alternateURI: "https://www.mozilla.com/",
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "/a",
+ fixedURI: "http://a/",
+ alternateURI: "https://www.a.com/",
+ keywordLookup: true,
+ protocolChange: true,
+ affectedByDNSForSingleWordHosts: true,
+ });
+} else {
+ const homeDir = Services.dirsvc.get("Home", Ci.nsIFile).path;
+ const homeBase = AppConstants.platform == "macosx" ? "/Users" : "/home";
+
+ testcases.push({
+ input: "~",
+ fixedURI: `file://${homeDir}`,
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "~/foo",
+ fixedURI: `file://${homeDir}/foo`,
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "~foo",
+ fixedURI: `file://${homeBase}/foo`,
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "~foo/bar",
+ fixedURI: `file://${homeBase}/foo/bar`,
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "/some/file.txt",
+ fixedURI: "file:///some/file.txt",
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "//mozilla",
+ fixedURI: "file:////mozilla",
+ protocolChange: true,
+ });
+ testcases.push({
+ input: "/a",
+ fixedURI: "file:///a",
+ protocolChange: true,
+ });
+}
+
+function sanitize(input) {
+ return input.replace(/\r|\n/g, "").trim();
+}
+
+add_task(async function setup() {
+ var prefList = [
+ "browser.fixup.typo.scheme",
+ "keyword.enabled",
+ "browser.fixup.domainwhitelist.whitelisted",
+ "browser.fixup.domainsuffixwhitelist.test",
+ "browser.fixup.domainsuffixwhitelist.local.domain",
+ "browser.search.separatePrivateDefault",
+ "browser.search.separatePrivateDefault.ui.enabled",
+ ];
+ for (let pref of prefList) {
+ Services.prefs.setBoolPref(pref, true);
+ }
+
+ await setupSearchService();
+ await addTestEngines();
+
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kSearchEngineID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ Services.search.getEngineByName(kPrivateSearchEngineID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
+
+var gSingleWordDNSLookup = false;
+add_task(async function run_test() {
+ // Only keywordlookup things should be affected by requiring a DNS lookup for single-word hosts:
+ info(
+ "Check only keyword lookup testcases should be affected by requiring DNS for single hosts"
+ );
+ let affectedTests = testcases.filter(
+ t => !t.keywordLookup && t.affectedByDNSForSingleWordHosts
+ );
+ if (affectedTests.length) {
+ for (let testcase of affectedTests) {
+ info("Affected: " + testcase.input);
+ }
+ }
+ Assert.equal(affectedTests.length, 0);
+ await do_single_test_run();
+ gSingleWordDNSLookup = true;
+ await do_single_test_run();
+ gSingleWordDNSLookup = false;
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kPostSearchEngineID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await do_single_test_run();
+});
+
+async function do_single_test_run() {
+ Services.prefs.setBoolPref(kForceDNSLookup, gSingleWordDNSLookup);
+ let relevantTests = gSingleWordDNSLookup
+ ? testcases.filter(t => t.keywordLookup)
+ : testcases;
+
+ let engine = await Services.search.getDefault();
+ let engineUrl =
+ engine.name == kPostSearchEngineID
+ ? kPostSearchEngineURL
+ : kSearchEngineURL;
+ let privateEngine = await Services.search.getDefaultPrivate();
+ let privateEngineUrl = kPrivateSearchEngineURL;
+
+ for (let {
+ input: testInput,
+ fixedURI: expectedFixedURI,
+ alternateURI: alternativeURI,
+ keywordLookup: expectKeywordLookup,
+ protocolChange: expectProtocolChange,
+ inWhitelist: inWhitelist,
+ affectedByDNSForSingleWordHosts: affectedByDNSForSingleWordHosts,
+ shouldRunTest,
+ } of relevantTests) {
+ // Explicitly force these into a boolean
+ expectKeywordLookup = !!expectKeywordLookup;
+ expectProtocolChange = !!expectProtocolChange;
+ inWhitelist = !!inWhitelist;
+ affectedByDNSForSingleWordHosts = !!affectedByDNSForSingleWordHosts;
+
+ expectKeywordLookup =
+ expectKeywordLookup &&
+ (!affectedByDNSForSingleWordHosts || !gSingleWordDNSLookup);
+
+ for (let flags of flagInputs) {
+ info(
+ 'Checking "' +
+ testInput +
+ '" with flags ' +
+ flags +
+ " (DNS lookup for single words: " +
+ (gSingleWordDNSLookup ? "yes" : "no") +
+ ")"
+ );
+
+ if (shouldRunTest && !shouldRunTest(flags)) {
+ continue;
+ }
+
+ let URIInfo;
+ try {
+ URIInfo = Services.uriFixup.getFixupURIInfo(testInput, flags);
+ } catch (ex) {
+ // Both APIs should return an error in the same cases.
+ info("Caught exception: " + ex);
+ Assert.equal(expectedFixedURI, null);
+ continue;
+ }
+
+ // Check the fixedURI:
+ let makeAlternativeURI =
+ flags & Services.uriFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI;
+
+ if (makeAlternativeURI && alternativeURI != null) {
+ Assert.equal(
+ URIInfo.fixedURI.spec,
+ alternativeURI,
+ "should have gotten alternate URI"
+ );
+ } else {
+ Assert.equal(
+ URIInfo.fixedURI && URIInfo.fixedURI.spec,
+ expectedFixedURI,
+ "should get correct fixed URI"
+ );
+ }
+
+ // Check booleans on input:
+ let couldDoKeywordLookup =
+ flags & Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ Assert.equal(
+ !!URIInfo.keywordProviderName,
+ couldDoKeywordLookup && expectKeywordLookup,
+ "keyword lookup as expected"
+ );
+
+ let expectProtocolChangeAfterAlternate = false;
+ // If alternativeURI was created, the protocol of the URI
+ // might have been changed to browser.fixup.alternate.protocol
+ // If the protocol is not the same as what was in expectedFixedURI,
+ // the protocol must've changed in the fixup process.
+ if (
+ makeAlternativeURI &&
+ alternativeURI != null &&
+ !expectedFixedURI.startsWith(URIInfo.fixedURI.scheme)
+ ) {
+ expectProtocolChangeAfterAlternate = true;
+ }
+
+ Assert.equal(
+ URIInfo.fixupChangedProtocol,
+ expectProtocolChange || expectProtocolChangeAfterAlternate,
+ "protocol change as expected"
+ );
+ Assert.equal(
+ URIInfo.fixupCreatedAlternateURI,
+ makeAlternativeURI && alternativeURI != null,
+ "alternative URI as expected"
+ );
+
+ // Check the preferred URI
+ if (couldDoKeywordLookup) {
+ if (expectKeywordLookup) {
+ if (!inWhitelist) {
+ let urlparamInput = encodeURIComponent(sanitize(testInput)).replace(
+ /%20/g,
+ "+"
+ );
+ // If the input starts with `?`, then URIInfo.preferredURI.spec will omit it
+ // In order to test this behaviour, remove `?` only if it is the first character
+ if (urlparamInput.startsWith("%3F")) {
+ urlparamInput = urlparamInput.replace("%3F", "");
+ }
+ let isPrivate =
+ flags & Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ let searchEngineUrl = isPrivate ? privateEngineUrl : engineUrl;
+ let searchURL = searchEngineUrl.replace(
+ "{searchTerms}",
+ urlparamInput
+ );
+ let spec = URIInfo.preferredURI.spec.replace(/%27/g, "'");
+ Assert.equal(spec, searchURL, "should get correct search URI");
+ let providerName = isPrivate ? privateEngine.name : engine.name;
+ Assert.equal(
+ URIInfo.keywordProviderName,
+ providerName,
+ "should get correct provider name"
+ );
+ // Also check keywordToURI() uses the right engine.
+ let kwInfo = Services.uriFixup.keywordToURI(
+ urlparamInput,
+ isPrivate
+ );
+ Assert.equal(kwInfo.providerName, URIInfo.providerName);
+ if (providerName == kPostSearchEngineID) {
+ Assert.ok(kwInfo.postData);
+ let submission = engine.getSubmission(urlparamInput);
+ let enginePostData = NetUtil.readInputStreamToString(
+ submission.postData,
+ submission.postData.available()
+ );
+ let postData = NetUtil.readInputStreamToString(
+ kwInfo.postData,
+ kwInfo.postData.available()
+ );
+ Assert.equal(postData, enginePostData);
+ }
+ } else {
+ Assert.equal(
+ URIInfo.preferredURI,
+ null,
+ "not expecting a preferred URI"
+ );
+ }
+ } else {
+ Assert.equal(
+ URIInfo.preferredURI.spec,
+ URIInfo.fixedURI.spec,
+ "fixed URI should match"
+ );
+ }
+ } else {
+ // In these cases, we should never be doing a keyword lookup and
+ // the fixed URI should be preferred:
+ let prefURI = URIInfo.preferredURI && URIInfo.preferredURI.spec;
+ let fixedURI = URIInfo.fixedURI && URIInfo.fixedURI.spec;
+ Assert.equal(prefURI, fixedURI, "fixed URI should be same as expected");
+ }
+ Assert.equal(
+ sanitize(testInput),
+ URIInfo.originalInput,
+ "should mirror original input"
+ );
+ }
+ }
+}
diff --git a/docshell/test/unit/test_URIFixup_search.js b/docshell/test/unit/test_URIFixup_search.js
new file mode 100644
index 0000000000..fa1c5b38ba
--- /dev/null
+++ b/docshell/test/unit/test_URIFixup_search.js
@@ -0,0 +1,143 @@
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var isWin = AppConstants.platform == "win";
+
+var data = [
+ {
+ // Valid should not be changed.
+ wrong: "https://example.com/this/is/a/test.html",
+ fixed: "https://example.com/this/is/a/test.html",
+ },
+ {
+ // Unrecognized protocols should be changed.
+ wrong: "whatever://this/is/a/test.html",
+ fixed: kSearchEngineURL.replace(
+ "{searchTerms}",
+ encodeURIComponent("whatever://this/is/a/test.html")
+ ),
+ },
+
+ {
+ // Unrecognized protocols should be changed.
+ wrong: "whatever://this/is/a/test.html",
+ fixed: kPrivateSearchEngineURL.replace(
+ "{searchTerms}",
+ encodeURIComponent("whatever://this/is/a/test.html")
+ ),
+ inPrivateBrowsing: true,
+ },
+
+ // The following tests check that when a user:password is present in the URL
+ // `user:` isn't treated as an unknown protocol thus leaking the user and
+ // password to the search engine.
+ {
+ wrong: "user:pass@example.com/this/is/a/test.html",
+ fixed: "http://user:pass@example.com/this/is/a/test.html",
+ },
+ {
+ wrong: "user@example.com:8080/this/is/a/test.html",
+ fixed: "http://user@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "https:pass@example.com/this/is/a/test.html",
+ fixed: "https://pass@example.com/this/is/a/test.html",
+ },
+ {
+ wrong: "user:pass@example.com:8080/this/is/a/test.html",
+ fixed: "http://user:pass@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "http:user:pass@example.com:8080/this/is/a/test.html",
+ fixed: "http://user:pass@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "ttp:user:pass@example.com:8080/this/is/a/test.html",
+ fixed: "http://user:pass@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "nonsense:user:pass@example.com:8080/this/is/a/test.html",
+ fixed: "http://nonsense:user%3Apass@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "user:@example.com:8080/this/is/a/test.html",
+ fixed: "http://user@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "//user:pass@example.com:8080/this/is/a/test.html",
+ fixed:
+ (isWin ? "http:" : "file://") +
+ "//user:pass@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "://user:pass@example.com:8080/this/is/a/test.html",
+ fixed: "http://user:pass@example.com:8080/this/is/a/test.html",
+ },
+ {
+ wrong: "localhost:8080/?param=1",
+ fixed: "http://localhost:8080/?param=1",
+ },
+ {
+ wrong: "localhost:8080?param=1",
+ fixed: "http://localhost:8080/?param=1",
+ },
+ {
+ wrong: "localhost:8080#somewhere",
+ fixed: "http://localhost:8080/#somewhere",
+ },
+ {
+ wrong: "whatever://this/is/a@b/test.html",
+ fixed: kSearchEngineURL.replace(
+ "{searchTerms}",
+ encodeURIComponent("whatever://this/is/a@b/test.html")
+ ),
+ },
+];
+
+var extProtocolSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+].getService(Ci.nsIExternalProtocolService);
+
+if (extProtocolSvc && extProtocolSvc.externalProtocolHandlerExists("mailto")) {
+ data.push({
+ wrong: "mailto:foo@bar.com",
+ fixed: "mailto:foo@bar.com",
+ });
+}
+
+var len = data.length;
+
+add_task(async function setup() {
+ await setupSearchService();
+ await addTestEngines();
+
+ Services.prefs.setBoolPref("keyword.enabled", true);
+ Services.prefs.setBoolPref("browser.search.separatePrivateDefault", true);
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kSearchEngineID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ Services.search.getEngineByName(kPrivateSearchEngineID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
+
+// Make sure we fix what needs fixing
+add_task(function test_fix_unknown_schemes() {
+ for (let i = 0; i < len; ++i) {
+ let item = data[i];
+ let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ if (item.inPrivateBrowsing) {
+ flags |= Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+ let { preferredURI } = Services.uriFixup.getFixupURIInfo(item.wrong, flags);
+ Assert.equal(preferredURI.spec, item.fixed);
+ }
+});
diff --git a/docshell/test/unit/test_allowJavascript.js b/docshell/test/unit/test_allowJavascript.js
new file mode 100644
index 0000000000..d1341c7bba
--- /dev/null
+++ b/docshell/test/unit/test_allowJavascript.js
@@ -0,0 +1,291 @@
+"use strict";
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+const ACTOR = "AllowJavascript";
+
+const HTML = String.raw`<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <script type="application/javascript">
+ "use strict";
+ var gFiredOnload = false;
+ var gFiredOnclick = false;
+ </script>
+</head>
+<body onload="gFiredOnload = true;" onclick="gFiredOnclick = true;">
+</body>
+</html>`;
+
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["example.com", "example.org"],
+});
+
+server.registerPathHandler("/", (request, response) => {
+ response.setHeader("Content-Type", "text/html");
+ response.write(HTML);
+});
+
+const { AllowJavascriptParent } = ChromeUtils.importESModule(
+ "resource://test/AllowJavascriptParent.sys.mjs"
+);
+
+async function assertScriptsAllowed(bc, expectAllowed, desc) {
+ let actor = bc.currentWindowGlobal.getActor(ACTOR);
+ let allowed = await actor.sendQuery("CheckScriptsAllowed");
+ equal(
+ allowed,
+ expectAllowed,
+ `Scripts should be ${expectAllowed ? "" : "dis"}allowed for ${desc}`
+ );
+}
+
+async function assertLoadFired(bc, expectFired, desc) {
+ let actor = bc.currentWindowGlobal.getActor(ACTOR);
+ let fired = await actor.sendQuery("CheckFiredLoadEvent");
+ equal(
+ fired,
+ expectFired,
+ `Should ${expectFired ? "" : "not "}have fired load for ${desc}`
+ );
+}
+
+function createSubframe(bc, url) {
+ let actor = bc.currentWindowGlobal.getActor(ACTOR);
+ return actor.sendQuery("CreateIframe", { url });
+}
+
+add_task(async function () {
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+ ChromeUtils.registerWindowActor(ACTOR, {
+ allFrames: true,
+ child: {
+ esModuleURI: "resource://test/AllowJavascriptChild.sys.mjs",
+ events: { load: { capture: true } },
+ },
+ parent: {
+ esModuleURI: "resource://test/AllowJavascriptParent.sys.mjs",
+ },
+ });
+
+ let page = await XPCShellContentUtils.loadContentPage("http://example.com/", {
+ remote: true,
+ remoteSubframes: true,
+ });
+
+ let bc = page.browsingContext;
+
+ {
+ let oopFrame1 = await createSubframe(bc, "http://example.org/");
+ let inprocFrame1 = await createSubframe(bc, "http://example.com/");
+
+ let oopFrame1OopSub = await createSubframe(
+ oopFrame1,
+ "http://example.com/"
+ );
+ let inprocFrame1OopSub = await createSubframe(
+ inprocFrame1,
+ "http://example.org/"
+ );
+
+ equal(
+ oopFrame1.allowJavascript,
+ true,
+ "OOP BC should inherit allowJavascript from parent"
+ );
+ equal(
+ inprocFrame1.allowJavascript,
+ true,
+ "In-process BC should inherit allowJavascript from parent"
+ );
+ equal(
+ oopFrame1OopSub.allowJavascript,
+ true,
+ "OOP BC child should inherit allowJavascript from parent"
+ );
+ equal(
+ inprocFrame1OopSub.allowJavascript,
+ true,
+ "In-process child BC should inherit allowJavascript from parent"
+ );
+
+ await assertLoadFired(bc, true, "top BC");
+ await assertScriptsAllowed(bc, true, "top BC");
+
+ await assertLoadFired(oopFrame1, true, "OOP frame 1");
+ await assertScriptsAllowed(oopFrame1, true, "OOP frame 1");
+
+ await assertLoadFired(inprocFrame1, true, "In-process frame 1");
+ await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1");
+
+ await assertLoadFired(oopFrame1OopSub, true, "OOP frame 1 subframe");
+ await assertScriptsAllowed(oopFrame1OopSub, true, "OOP frame 1 subframe");
+
+ await assertLoadFired(
+ inprocFrame1OopSub,
+ true,
+ "In-process frame 1 subframe"
+ );
+ await assertScriptsAllowed(
+ inprocFrame1OopSub,
+ true,
+ "In-process frame 1 subframe"
+ );
+
+ bc.allowJavascript = false;
+ await assertScriptsAllowed(bc, false, "top BC with scripts disallowed");
+ await assertScriptsAllowed(
+ oopFrame1,
+ false,
+ "OOP frame 1 with top BC with scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ inprocFrame1,
+ false,
+ "In-process frame 1 with top BC with scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ oopFrame1OopSub,
+ false,
+ "OOP frame 1 subframe with top BC with scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ inprocFrame1OopSub,
+ false,
+ "In-process frame 1 subframe with top BC with scripts disallowed"
+ );
+
+ let oopFrame2 = await createSubframe(bc, "http://example.org/");
+ let inprocFrame2 = await createSubframe(bc, "http://example.com/");
+
+ equal(
+ oopFrame2.allowJavascript,
+ false,
+ "OOP BC 2 should inherit allowJavascript from parent"
+ );
+ equal(
+ inprocFrame2.allowJavascript,
+ false,
+ "In-process BC 2 should inherit allowJavascript from parent"
+ );
+
+ await assertLoadFired(
+ oopFrame2,
+ undefined,
+ "OOP frame 2 with top BC with scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ oopFrame2,
+ false,
+ "OOP frame 2 with top BC with scripts disallowed"
+ );
+ await assertLoadFired(
+ inprocFrame2,
+ undefined,
+ "In-process frame 2 with top BC with scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ inprocFrame2,
+ false,
+ "In-process frame 2 with top BC with scripts disallowed"
+ );
+
+ bc.allowJavascript = true;
+ await assertScriptsAllowed(bc, true, "top BC");
+
+ await assertScriptsAllowed(oopFrame1, true, "OOP frame 1");
+ await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1");
+ await assertScriptsAllowed(oopFrame1OopSub, true, "OOP frame 1 subframe");
+ await assertScriptsAllowed(
+ inprocFrame1OopSub,
+ true,
+ "In-process frame 1 subframe"
+ );
+
+ await assertScriptsAllowed(oopFrame2, false, "OOP frame 2");
+ await assertScriptsAllowed(inprocFrame2, false, "In-process frame 2");
+
+ oopFrame1.currentWindowGlobal.allowJavascript = false;
+ inprocFrame1.currentWindowGlobal.allowJavascript = false;
+
+ await assertScriptsAllowed(
+ oopFrame1,
+ false,
+ "OOP frame 1 with second level WC scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ inprocFrame1,
+ false,
+ "In-process frame 1 with second level WC scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ oopFrame1OopSub,
+ false,
+ "OOP frame 1 subframe second level WC scripts disallowed"
+ );
+ await assertScriptsAllowed(
+ inprocFrame1OopSub,
+ false,
+ "In-process frame 1 subframe with second level WC scripts disallowed"
+ );
+
+ oopFrame1.reload(0);
+ inprocFrame1.reload(0);
+ await Promise.all([
+ AllowJavascriptParent.promiseLoad(oopFrame1),
+ AllowJavascriptParent.promiseLoad(inprocFrame1),
+ ]);
+
+ equal(
+ oopFrame1.currentWindowGlobal.allowJavascript,
+ true,
+ "WindowContext.allowJavascript does not persist after navigation for OOP frame 1"
+ );
+ equal(
+ inprocFrame1.currentWindowGlobal.allowJavascript,
+ true,
+ "WindowContext.allowJavascript does not persist after navigation for in-process frame 1"
+ );
+
+ await assertScriptsAllowed(oopFrame1, true, "OOP frame 1");
+ await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1");
+ }
+
+ bc.allowJavascript = false;
+
+ bc.reload(0);
+ await AllowJavascriptParent.promiseLoad(bc);
+
+ await assertLoadFired(
+ bc,
+ undefined,
+ "top BC with scripts disabled after reload"
+ );
+ await assertScriptsAllowed(
+ bc,
+ false,
+ "top BC with scripts disabled after reload"
+ );
+
+ await page.loadURL("http://example.org/?other");
+ bc = page.browsingContext;
+
+ await assertLoadFired(
+ bc,
+ undefined,
+ "top BC with scripts disabled after navigation"
+ );
+ await assertScriptsAllowed(
+ bc,
+ false,
+ "top BC with scripts disabled after navigation"
+ );
+
+ await page.close();
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/docshell/test/unit/test_browsing_context_structured_clone.js b/docshell/test/unit/test_browsing_context_structured_clone.js
new file mode 100644
index 0000000000..1c91a9b0b2
--- /dev/null
+++ b/docshell/test/unit/test_browsing_context_structured_clone.js
@@ -0,0 +1,70 @@
+"use strict";
+
+add_task(async function test_BrowsingContext_structured_clone() {
+ let browser = Services.appShell.createWindowlessBrowser(false);
+
+ let frame = browser.document.createElement("iframe");
+
+ await new Promise(r => {
+ frame.onload = () => r();
+ browser.document.body.appendChild(frame);
+ });
+
+ let { browsingContext } = frame;
+
+ let sch = new StructuredCloneHolder("debug name", "<anonymized> debug name", {
+ browsingContext,
+ });
+
+ let deserialize = () => sch.deserialize({}, true);
+
+ // Check that decoding a live browsing context produces the correct
+ // object.
+ equal(
+ deserialize().browsingContext,
+ browsingContext,
+ "Got correct browsing context from StructuredClone deserialize"
+ );
+
+ // Check that decoding a second time still succeeds.
+ equal(
+ deserialize().browsingContext,
+ browsingContext,
+ "Got correct browsing context from second StructuredClone deserialize"
+ );
+
+ // Destroy the browsing context and make sure that the decode fails
+ // with a DataCloneError.
+ //
+ // Making sure the BrowsingContext is actually destroyed by the time
+ // we do the second decode is a bit tricky. We obviously have clear
+ // our local references to it, and give the GC a chance to reap them.
+ // And we also, of course, have to destroy the frame that it belongs
+ // to, or its frame loader and window global would hold it alive.
+ //
+ // Beyond that, we don't *have* to reload or destroy the parent
+ // document, but we do anyway just to be safe.
+ //
+
+ frame.remove();
+ frame = null;
+ browsingContext = null;
+
+ browser.document.location.reload();
+ browser.close();
+
+ // We will schedule a precise GC and do both GC and CC a few times, to make
+ // sure we have completely destroyed the WindowGlobal actors (which keep
+ // references to their BrowsingContexts) in order
+ // to allow their (now snow-white) references to be collected.
+ await schedulePreciseGCAndForceCC(3);
+
+ // OK. We can be fairly confident that the BrowsingContext object
+ // stored in our structured clone data has been destroyed. Make sure
+ // that attempting to decode it again leads to the appropriate error.
+ Assert.throws(
+ deserialize,
+ e => e.name === "DataCloneError",
+ "Should get a DataCloneError when trying to decode a dead BrowsingContext"
+ );
+});
diff --git a/docshell/test/unit/test_bug442584.js b/docshell/test/unit/test_bug442584.js
new file mode 100644
index 0000000000..c109557f50
--- /dev/null
+++ b/docshell/test/unit/test_bug442584.js
@@ -0,0 +1,35 @@
+var prefetch = Cc["@mozilla.org/prefetch-service;1"].getService(
+ Ci.nsIPrefetchService
+);
+
+var ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+function run_test() {
+ // Fill up the queue
+ Services.prefs.setBoolPref("network.prefetch-next", true);
+ for (var i = 0; i < 5; i++) {
+ var uri = Services.io.newURI("http://localhost/" + i);
+ var referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
+ prefetch.prefetchURI(uri, referrerInfo, null, true);
+ }
+
+ // Make sure the queue has items in it...
+ Assert.ok(prefetch.hasMoreElements());
+
+ // Now disable the pref to force the queue to empty...
+ Services.prefs.setBoolPref("network.prefetch-next", false);
+ Assert.ok(!prefetch.hasMoreElements());
+
+ // Now reenable the pref, and add more items to the queue.
+ Services.prefs.setBoolPref("network.prefetch-next", true);
+ for (var k = 0; k < 5; k++) {
+ var uri2 = Services.io.newURI("http://localhost/" + k);
+ var referrerInfo2 = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri2);
+ prefetch.prefetchURI(uri2, referrerInfo2, null, true);
+ }
+ Assert.ok(prefetch.hasMoreElements());
+}
diff --git a/docshell/test/unit/test_pb_notification.js b/docshell/test/unit/test_pb_notification.js
new file mode 100644
index 0000000000..51cd3b95ff
--- /dev/null
+++ b/docshell/test/unit/test_pb_notification.js
@@ -0,0 +1,18 @@
+function destroy_transient_docshell() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+ windowlessBrowser.docShell.setOriginAttributes({ privateBrowsingId: 1 });
+ windowlessBrowser.close();
+ do_test_pending();
+ do_timeout(0, Cu.forceGC);
+}
+
+function run_test() {
+ var obs = {
+ observe(aSubject, aTopic, aData) {
+ Assert.equal(aTopic, "last-pb-context-exited");
+ do_test_finished();
+ },
+ };
+ Services.obs.addObserver(obs, "last-pb-context-exited");
+ destroy_transient_docshell();
+}
diff --git a/docshell/test/unit/test_privacy_transition.js b/docshell/test/unit/test_privacy_transition.js
new file mode 100644
index 0000000000..ae1bf71284
--- /dev/null
+++ b/docshell/test/unit/test_privacy_transition.js
@@ -0,0 +1,21 @@
+var gNotifications = 0;
+
+var observer = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPrivacyTransitionObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ privateModeChanged(enabled) {
+ gNotifications++;
+ },
+};
+
+function run_test() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+ windowlessBrowser.docShell.addWeakPrivacyTransitionObserver(observer);
+ windowlessBrowser.docShell.setOriginAttributes({ privateBrowsingId: 1 });
+ windowlessBrowser.docShell.setOriginAttributes({ privateBrowsingId: 0 });
+ windowlessBrowser.close();
+ Assert.equal(gNotifications, 2);
+}
diff --git a/docshell/test/unit/test_subframe_stop_after_parent_error.js b/docshell/test/unit/test_subframe_stop_after_parent_error.js
new file mode 100644
index 0000000000..ee1876b4b9
--- /dev/null
+++ b/docshell/test/unit/test_subframe_stop_after_parent_error.js
@@ -0,0 +1,145 @@
+"use strict";
+// Tests that pending subframe requests for an initial about:blank
+// document do not delay showing load errors (and possibly result in a
+// crash at docShell destruction) for failed document loads.
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/undefined/);
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["example.com"],
+});
+
+// Registers a URL with the HTTP server which will not return a response
+// until we're ready to.
+function registerSlowPage(path) {
+ let result = {
+ url: `http://example.com/${path}`,
+ };
+
+ let finishedPromise = new Promise(resolve => {
+ result.finish = resolve;
+ });
+
+ server.registerPathHandler(`/${path}`, async (request, response) => {
+ response.processAsync();
+
+ response.setHeader("Content-Type", "text/html");
+ response.write("<html><body>Hello.</body></html>");
+
+ await finishedPromise;
+
+ response.finish();
+ });
+
+ return result;
+}
+
+let topFrameRequest = registerSlowPage("top.html");
+let subFrameRequest = registerSlowPage("frame.html");
+
+let thunks = new Set();
+function promiseStateStop(webProgress) {
+ return new Promise(resolve => {
+ let listener = {
+ onStateChange(aWebProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ webProgress.removeProgressListener(listener);
+
+ thunks.delete(listener);
+ resolve();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ // Keep the listener alive, since it's stored as a weak reference.
+ thunks.add(listener);
+ webProgress.addProgressListener(
+ listener,
+ Ci.nsIWebProgress.NOTIFY_STATE_NETWORK
+ );
+ });
+}
+
+async function runTest(waitForErrorPage) {
+ let page = await XPCShellContentUtils.loadContentPage("about:blank");
+
+ // Watch for the HTTP request for the top frame so that we can cancel
+ // it with an error.
+ let requestPromise = TestUtils.topicObserved(
+ "http-on-modify-request",
+ subject => subject.QueryInterface(Ci.nsIRequest).name == topFrameRequest.url
+ );
+
+ await page.spawn(
+ [topFrameRequest.url, subFrameRequest.url],
+ function (topFrameUrl, subFrameRequestUrl) {
+ // Create a frame with a source URL which will not return a response
+ // before we cancel it with an error.
+ let doc = this.content.document;
+ let frame = doc.createElement("iframe");
+ frame.src = topFrameUrl;
+ doc.body.appendChild(frame);
+
+ // Create a subframe in the initial about:blank document for the above
+ // frame which will not return a response before we cancel the
+ // document request.
+ let frameDoc = frame.contentDocument;
+ let subframe = frameDoc.createElement("iframe");
+ subframe.src = subFrameRequestUrl;
+ frameDoc.body.appendChild(subframe);
+ }
+ );
+
+ let [req] = await requestPromise;
+
+ info("Cancel request for parent frame");
+ req.cancel(Cr.NS_ERROR_PROXY_CONNECTION_REFUSED);
+
+ // Request cancelation is not synchronous, so wait for the STATE_STOP
+ // event to fire.
+ await promiseStateStop(page.browsingContext.webProgress);
+
+ // And make a trip through the event loop to give the DocLoader a
+ // chance to update its state.
+ await new Promise(executeSoon);
+
+ if (waitForErrorPage) {
+ // Make sure that canceling the request with an error code actually
+ // shows an error page without waiting for a subframe response.
+ await TestUtils.waitForCondition(() =>
+ page.browsingContext.children[0]?.currentWindowGlobal?.documentURI?.spec.startsWith(
+ "about:neterror?"
+ )
+ );
+ }
+
+ await page.close();
+}
+
+add_task(async function testRemoveFrameImmediately() {
+ await runTest(false);
+});
+
+add_task(async function testRemoveFrameAfterErrorPage() {
+ await runTest(true);
+});
+
+add_task(async function () {
+ // Allow the document requests for the frames to complete.
+ topFrameRequest.finish();
+ subFrameRequest.finish();
+});
diff --git a/docshell/test/unit/xpcshell.toml b/docshell/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..abc775f1ae
--- /dev/null
+++ b/docshell/test/unit/xpcshell.toml
@@ -0,0 +1,43 @@
+[DEFAULT]
+head = "head_docshell.js"
+support-files = [
+ "data/engine.xml",
+ "data/enginePost.xml",
+ "data/enginePrivate.xml",
+]
+
+["test_URIFixup.js"]
+
+["test_URIFixup_check_host.js"]
+
+["test_URIFixup_external_protocol_fallback.js"]
+skip-if = ["os == 'android'"]
+
+["test_URIFixup_forced.js"]
+# Disabled for 1563343 -- URI fixup should be done at the app level in GV.
+skip-if = ["os == 'android'"]
+
+["test_URIFixup_info.js"]
+skip-if = ["os == 'android'"]
+
+["test_URIFixup_search.js"]
+skip-if = ["os == 'android'"]
+
+["test_allowJavascript.js"]
+skip-if = ["os == 'android'"]
+support-files = [
+ "AllowJavascriptChild.sys.mjs",
+ "AllowJavascriptParent.sys.mjs",
+]
+
+["test_browsing_context_structured_clone.js"]
+
+["test_bug442584.js"]
+
+["test_pb_notification.js"]
+# Bug 751575: unrelated JS changes cause timeouts on random platforms
+skip-if = ["true"]
+
+["test_privacy_transition.js"]
+
+["test_subframe_stop_after_parent_error.js"]
diff --git a/docshell/test/unit_ipc/test_pb_notification_ipc.js b/docshell/test/unit_ipc/test_pb_notification_ipc.js
new file mode 100644
index 0000000000..6408e7753e
--- /dev/null
+++ b/docshell/test/unit_ipc/test_pb_notification_ipc.js
@@ -0,0 +1,15 @@
+function run_test() {
+ var notifications = 0;
+ var obs = {
+ observe(aSubject, aTopic, aData) {
+ Assert.equal(aTopic, "last-pb-context-exited");
+ notifications++;
+ },
+ };
+ Services.obs.addObserver(obs, "last-pb-context-exited");
+
+ run_test_in_child("../unit/test_pb_notification.js", function () {
+ Assert.equal(notifications, 1);
+ do_test_finished();
+ });
+}
diff --git a/docshell/test/unit_ipc/xpcshell.toml b/docshell/test/unit_ipc/xpcshell.toml
new file mode 100644
index 0000000000..9ddc971a24
--- /dev/null
+++ b/docshell/test/unit_ipc/xpcshell.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = ""
+skip-if = ["os == 'android'"]
+
+["test_pb_notification_ipc.js"]
+# Bug 751575: Perma-fails with: command timed out: 1200 seconds without output
+skip-if = ["true"]
+