commit 17c6bdb0c8cf7534561316bce0326aecab9e202e
parent 27ffb7a727e79483d9992944db5eb6477906b6c2
Author: Andreas Farre <farre@mozilla.com>
Date: Wed, 15 Oct 2025 07:44:49 +0000
Bug 1993431 - Only consider cross document navigation for beforeunload. r=smaug
Differential Revision: https://phabricator.services.mozilla.com/D268124
Diffstat:
5 files changed, 91 insertions(+), 41 deletions(-)
diff --git a/docshell/shistory/nsSHistory.cpp b/docshell/shistory/nsSHistory.cpp
@@ -1465,43 +1465,40 @@ static bool MaybeCheckUnloadingIsCanceled(
return false;
}
- // Step 4.3.2
+ RefPtr<CanonicalBrowsingContext> traversable = aTraversable->Canonical();
+
+ RefPtr<WindowGlobalParent> windowGlobalParent =
+ traversable->GetCurrentWindowGlobal();
+ // An efficiency trick. We've set this flag on the window context if we've
+ // seen a "navigate" and/or a "beforeunload" handler set. If not we know we
+ // can skip this.
+ if (!windowGlobalParent || (!windowGlobalParent->NeedsBeforeUnload() &&
+ !windowGlobalParent->GetNeedsTraverse())) {
+ return false;
+ }
+
+ // Step 4.2
auto found =
std::find_if(aLoadResults.begin(), aLoadResults.end(),
- [traversable = RefPtr{aTraversable}](const auto& result) {
+ [traversable](const auto& result) {
return result.mBrowsingContext->Id() == traversable->Id();
});
- // Step 4.3.2
- bool needsBeforeUnload = found != aLoadResults.end();
- // Step 4.2
- // This is a bit fishy since we don't have a direct way of performing
- // the step, but this does its best.
- RefPtr<nsDocShellLoadState> loadState =
- needsBeforeUnload ? found->mLoadState : nullptr;
- RefPtr<CanonicalBrowsingContext> browsingContext = aTraversable->Canonical();
- MOZ_DIAGNOSTIC_ASSERT(!needsBeforeUnload ||
- found->mBrowsingContext == browsingContext);
+ // Step 4.3, since current entry is always different to not finding one.
+ if (found == aLoadResults.end()) {
+ return false;
+ }
- nsCOMPtr<SessionHistoryEntry> currentEntry =
- browsingContext->GetActiveSessionHistoryEntry();
- // If we didn't find a load state, it means that traversable stays at the
- // current entry.
+ // Step 4.2
nsCOMPtr<SessionHistoryEntry> targetEntry =
- loadState ? do_QueryInterface(loadState->SHEntry()) : currentEntry;
+ do_QueryInterface(found->mLoadState->SHEntry());
- // Step 4.3
- if (!currentEntry || currentEntry->GetID() == targetEntry->GetID()) {
- return false;
- }
+ nsCOMPtr<SessionHistoryEntry> currentEntry =
+ traversable->GetActiveSessionHistoryEntry();
- RefPtr<WindowGlobalParent> windowGlobalParent =
- browsingContext->GetCurrentWindowGlobal();
- // An efficiency trick. We've set this flag on the window context if we've
- // seen a "navigate" and/or a "beforeunload" handler set. If not we know we
- // can skip this.
- if (!windowGlobalParent || (!windowGlobalParent->NeedsBeforeUnload() &&
- !windowGlobalParent->GetNeedsTraverse())) {
+ // Step 4.3, but the actual checks in the spec.
+ if (!currentEntry || !targetEntry ||
+ currentEntry->GetID() == targetEntry->GetID()) {
return false;
}
@@ -1518,6 +1515,17 @@ static bool MaybeCheckUnloadingIsCanceled(
return false;
}
+ // Step 4.3.2
+ // If we squint we can see spec here, insofar that for a traversable's
+ // beforeunload handler to fire, the target entry needs to be:
+ // * non-null, i.e. part of navigables being traversed
+ // * different from the current entry
+ // * cross document from the current entry
+ // * have beforeunload handlers
+ bool needsBeforeUnload =
+ windowGlobalParent->NeedsBeforeUnload() &&
+ currentEntry->SharedInfo() != targetEntry->SharedInfo();
+
// Step 4.3.3 isn't needed since that's what PermitUnloadChildNavigables
// achieves by skipping top level navigable.
@@ -1525,10 +1533,10 @@ static bool MaybeCheckUnloadingIsCanceled(
// PermitUnloadTraversable only includes the process of the top level browsing
// context.
- // If we don't have any unload handlers registered, we still need to run
- // navigate event handlers, but we don't need to show the prompt.
+ // If we're not going to run any beforeunload handlers, we still need to run
+ // navigate event handlers for the traversable.
nsIDocumentViewer::PermitUnloadAction action =
- windowGlobalParent->NeedsBeforeUnload()
+ needsBeforeUnload
? nsIDocumentViewer::PermitUnloadAction::ePrompt
: nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload;
windowGlobalParent->PermitUnloadTraversable(
@@ -1536,11 +1544,21 @@ static bool MaybeCheckUnloadingIsCanceled(
[action, loadResults = CopyableTArray(std::move(aLoadResults)),
windowGlobalParent,
aResolver](nsIDocumentViewer::PermitUnloadResult aResult) mutable {
- if (aResult != nsIDocumentViewer::eContinue) {
+ if (aResult != nsIDocumentViewer::PermitUnloadResult::eContinue) {
aResolver(loadResults, aResult);
return;
}
+ // If the traversable didn't have beforeunloadun handlers, we won't run
+ // other navigable's unload handlers either. That will be handled by
+ // regular navigation.
+ if (action ==
+ nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload) {
+ aResolver(loadResults,
+ nsIDocumentViewer::PermitUnloadResult::eContinue);
+ return;
+ }
+
// PermitUnloadTraversable includes everything except the process of the
// top level browsing context.
windowGlobalParent->PermitUnloadChildNavigables(
@@ -1610,7 +1628,7 @@ void nsSHistory::LoadURIs(const nsTArray<LoadEntryResult>& aLoadResults,
return;
}
- // There's no unload handlers, resolve immediately.
+ // There's no beforeunload handlers, resolve immediately.
aResolver(NS_OK);
// And we fall back to the simple case if we shouldn't fire a "traverse"
diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp
@@ -4367,13 +4367,6 @@ mozilla::ipc::IPCResult ContentChild::RecvDispatchBeforeUnloadToSubtree(
return IPC_OK();
}
-mozilla::ipc::IPCResult ContentChild::RecvInitNextGenLocalStorageEnabled(
- const bool& aEnabled) {
- mozilla::dom::RecvInitNextGenLocalStorageEnabled(aEnabled);
-
- return IPC_OK();
-}
-
/* static */ void ContentChild::DispatchBeforeUnloadToSubtree(
BrowsingContext* aStartingAt,
const mozilla::Maybe<SessionHistoryInfo>& aInfo,
@@ -4421,6 +4414,26 @@ mozilla::ipc::IPCResult ContentChild::RecvInitNextGenLocalStorageEnabled(
}
}
+mozilla::ipc::IPCResult ContentChild::RecvDispatchNavigateToTraversable(
+ const MaybeDiscarded<BrowsingContext>& aTraversable,
+ const mozilla::Maybe<SessionHistoryInfo>& aInfo,
+ DispatchNavigateToTraversableResolver&& aResolver) {
+ if (aTraversable.IsNullOrDiscarded() || !aTraversable->GetDocShell()) {
+ aResolver(nsIDocumentViewer::eContinue);
+ } else {
+ RefPtr docShell = nsDocShell::Cast(aTraversable->GetDocShell());
+ aResolver(docShell->MaybeFireTraversableTraverseHistory(*aInfo, Nothing()));
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ContentChild::RecvInitNextGenLocalStorageEnabled(
+ const bool& aEnabled) {
+ mozilla::dom::RecvInitNextGenLocalStorageEnabled(aEnabled);
+
+ return IPC_OK();
+}
+
mozilla::ipc::IPCResult ContentChild::RecvGoBack(
const MaybeDiscarded<BrowsingContext>& aContext,
const Maybe<int32_t>& aCancelContentJSEpoch, bool aRequireUserInteraction,
diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h
@@ -789,6 +789,12 @@ class ContentChild final : public PContentChild,
const mozilla::Maybe<SessionHistoryInfo>& aInfo,
DispatchBeforeUnloadToSubtreeResolver&& aResolver);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ mozilla::ipc::IPCResult RecvDispatchNavigateToTraversable(
+ const MaybeDiscarded<BrowsingContext>& aTraversable,
+ const mozilla::Maybe<SessionHistoryInfo>& aInfo,
+ DispatchNavigateToTraversableResolver&& aResolver);
+
mozilla::ipc::IPCResult RecvInitNextGenLocalStorageEnabled(
const bool& aEnabled);
diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl
@@ -1056,6 +1056,10 @@ child:
async DispatchBeforeUnloadToSubtree(MaybeDiscardedBrowsingContext aStartingAt, SessionHistoryInfo? info)
returns (PermitUnloadResult result);
+ // Only dispatches "navigate" to the traversable. Used in case we don't want to dispatch beforeunload.
+ async DispatchNavigateToTraversable(MaybeDiscardedBrowsingContext aTraversable, SessionHistoryInfo? info)
+ returns (PermitUnloadResult result);
+
// Update the cached list of codec supported in the given process.
async UpdateMediaCodecsSupported(RemoteMediaIn aLocation, MediaCodecsSupported aSupported);
diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp
@@ -41,6 +41,7 @@
#include "mozilla/dom/JSWindowActorBinding.h"
#include "mozilla/dom/JSWindowActorParent.h"
#include "mozilla/dom/MediaController.h"
+#include "mozilla/dom/Navigation.h"
#include "mozilla/dom/NavigatorLogin.h"
#include "mozilla/dom/PBackgroundSessionStorageCache.h"
#include "mozilla/dom/UseCounterMetrics.h"
@@ -833,9 +834,17 @@ class CheckPermitUnloadRequest final : public PromiseNativeHandler,
// If `aInfo` is passed, only dispatch to the content process of the top
// level window.
if (aInfo) {
+ MOZ_DIAGNOSTIC_ASSERT(Navigation::IsAPIEnabled());
ContentParent* cp = mWGP->GetContentParent();
mPendingRequests++;
- cp->SendDispatchBeforeUnloadToSubtree(bc, aInfo, resolve, reject);
+ // Here eDontPromptAndUnload means that we ignore beforeunload handlers,
+ // but we still need to handle the traversable navigate handler.
+ if (mAction ==
+ nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload) {
+ cp->SendDispatchNavigateToTraversable(bc, aInfo, resolve, reject);
+ } else {
+ cp->SendDispatchBeforeUnloadToSubtree(bc, aInfo, resolve, reject);
+ }
} else {
bc->PreOrderWalk([&](dom::BrowsingContext* aBC) {
if (WindowGlobalParent* wgp =