gecko-dev/dom/base/nsFrameLoaderOwner.cpp
Emilio Cobos Álvarez 7377ea90fb Bug 1936294 - Fix-up mFocusedWindow / mFocusedElement when replacing frameloaders. r=dom-core,sefeng
The issue is that when replacing frame loaders for a non-remote browser,
mFocusedElement is not that <browser> element. It instead points to the
(potentially null) focused element in mFocusedWindow.

If mFocusedWindow is about to be destroyed (or put into the bfcache), clear it
and point mFocusedElement to the <browser> instead, so that we can focus the
new frame loader if needed later.

If we don't do this, the focused window gets cleared when the window is nuked
in nsFocusManager::WasNuked (but by that point it's too late to do any fix up,
as we've lost the context that we're swapping, and of our frameloader owner,
and everything else).

Test in D232437.

Differential Revision: https://phabricator.services.mozilla.com/D233444
2025-01-09 13:44:15 +00:00

402 lines
14 KiB
C++

/* -*- 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 "nsFrameLoaderOwner.h"
#include "mozilla/dom/BrowserParent.h"
#include "nsFrameLoader.h"
#include "nsFocusManager.h"
#include "nsNetUtil.h"
#include "nsSubDocumentFrame.h"
#include "nsQueryObject.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/Logging.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/FrameLoaderBinding.h"
#include "mozilla/dom/HTMLIFrameElement.h"
#include "mozilla/dom/MozFrameLoaderOwnerBinding.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/dom/BrowserBridgeChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/BrowserBridgeHost.h"
#include "mozilla/dom/BrowserHost.h"
#include "mozilla/StaticPrefs_fission.h"
#include "mozilla/EventStateManager.h"
extern mozilla::LazyLogModule gSHIPBFCacheLog;
using namespace mozilla;
using namespace mozilla::dom;
already_AddRefed<nsFrameLoader> nsFrameLoaderOwner::GetFrameLoader() {
return do_AddRef(mFrameLoader);
}
void nsFrameLoaderOwner::SetFrameLoader(nsFrameLoader* aNewFrameLoader) {
mFrameLoader = aNewFrameLoader;
}
mozilla::dom::BrowsingContext* nsFrameLoaderOwner::GetBrowsingContext() {
if (mFrameLoader) {
return mFrameLoader->GetBrowsingContext();
}
return nullptr;
}
mozilla::dom::BrowsingContext* nsFrameLoaderOwner::GetExtantBrowsingContext() {
if (mFrameLoader) {
return mFrameLoader->GetExtantBrowsingContext();
}
return nullptr;
}
bool nsFrameLoaderOwner::UseRemoteSubframes() {
RefPtr<Element> owner = do_QueryObject(this);
nsILoadContext* loadContext = owner->OwnerDoc()->GetLoadContext();
MOZ_DIAGNOSTIC_ASSERT(loadContext);
return loadContext->UseRemoteSubframes();
}
nsFrameLoaderOwner::ChangeRemotenessContextType
nsFrameLoaderOwner::ShouldPreserveBrowsingContext(
bool aIsRemote, bool aReplaceBrowsingContext) {
if (aReplaceBrowsingContext) {
return ChangeRemotenessContextType::DONT_PRESERVE;
}
if (XRE_IsParentProcess()) {
// Don't preserve for remote => parent loads.
if (!aIsRemote) {
return ChangeRemotenessContextType::DONT_PRESERVE;
}
// Don't preserve for parent => remote loads.
if (mFrameLoader && !mFrameLoader->IsRemoteFrame()) {
return ChangeRemotenessContextType::DONT_PRESERVE;
}
}
return ChangeRemotenessContextType::PRESERVE;
}
void nsFrameLoaderOwner::ChangeRemotenessCommon(
const ChangeRemotenessContextType& aContextType,
const NavigationIsolationOptions& aOptions, bool aSwitchingInProgressLoad,
bool aIsRemote, BrowsingContextGroup* aGroup,
std::function<void()>& aFrameLoaderInit, mozilla::ErrorResult& aRv) {
MOZ_ASSERT_IF(aGroup, aContextType != ChangeRemotenessContextType::PRESERVE);
RefPtr<mozilla::dom::BrowsingContext> bc;
bool networkCreated = false;
// In this case, we're not reparenting a frameloader, we're just destroying
// our current one and creating a new one, so we can use ourselves as the
// owner.
RefPtr<Element> owner = do_QueryObject(this);
MOZ_ASSERT(owner);
// When we destroy the original frameloader, it will stop blocking the parent
// document's load event, and immediately trigger the load event if there are
// no other blockers. Since we're going to be adding a new blocker as soon as
// we recreate the frame loader, this is not what we want, so add our own
// blocker until the process is complete.
Document* doc = owner->OwnerDoc();
doc->BlockOnload();
auto cleanup = MakeScopeExit([&]() { doc->UnblockOnload(false); });
// If we store the previous nsFrameLoader in the bfcache, this will be filled
// with the SessionHistoryEntry which now owns the frame.
RefPtr<SessionHistoryEntry> bfcacheEntry;
{
// Introduce a script blocker to ensure no JS is executed during the
// nsFrameLoader teardown & recreation process. Unload listeners will be run
// for the previous document, and the load will be started for the new one,
// at the end of this block.
nsAutoScriptBlocker sb;
// If we already have a Frameloader, destroy it, possibly preserving its
// browsing context.
if (mFrameLoader) {
// Calling `GetBrowsingContext` here will force frameloader
// initialization if it hasn't already happened, which we neither need
// or want, so we use the initial (possibly pending) browsing context
// directly, instead.
bc = mFrameLoader->GetMaybePendingBrowsingContext();
if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) {
fm->FixUpFocusBeforeFrameLoaderChange(*owner, bc);
}
networkCreated = mFrameLoader->IsNetworkCreated();
MOZ_ASSERT_IF(aOptions.mTryUseBFCache, aOptions.mReplaceBrowsingContext);
if (aOptions.mTryUseBFCache && bc) {
bfcacheEntry = bc->Canonical()->GetActiveSessionHistoryEntry();
bool useBFCache = bfcacheEntry &&
bfcacheEntry == aOptions.mActiveSessionHistoryEntry &&
!bfcacheEntry->GetFrameLoader();
if (useBFCache) {
MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
("nsFrameLoaderOwner::ChangeRemotenessCommon: store the old "
"page in bfcache"));
Unused << bc->SetIsInBFCache(true);
bfcacheEntry->SetFrameLoader(mFrameLoader);
// Session history owns now the frameloader.
mFrameLoader = nullptr;
}
}
if (mFrameLoader) {
if (aContextType == ChangeRemotenessContextType::PRESERVE) {
mFrameLoader->SetWillChangeProcess();
}
// Preserve the networkCreated status, as nsDocShells created after a
// process swap may shouldn't change their dynamically-created status.
mFrameLoader->Destroy(aSwitchingInProgressLoad);
mFrameLoader = nullptr;
}
}
mFrameLoader = nsFrameLoader::Recreate(
owner, bc, aGroup, aOptions, aIsRemote, networkCreated,
aContextType == ChangeRemotenessContextType::PRESERVE);
if (NS_WARN_IF(!mFrameLoader)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
// Invoke the frame loader initialization callback to perform setup on our
// new nsFrameLoader. This may cause our ErrorResult to become errored, so
// double-check after calling.
aFrameLoaderInit();
if (NS_WARN_IF(aRv.Failed())) {
return;
}
}
// Now that we have a new FrameLoader, we'll eventually need to reset
// nsSubDocumentFrame to use the new one. We can delay doing this if we're
// keeping our old frameloader around in the BFCache and the new frame hasn't
// presented yet to continue painting the previous document.
const bool retainPaint = bfcacheEntry && mFrameLoader->IsRemoteFrame();
if (!retainPaint) {
MOZ_LOG(
gSHIPBFCacheLog, LogLevel::Debug,
("Previous frameLoader not entering BFCache - not retaining paint data"
"(bfcacheEntry=%p, isRemoteFrame=%d)",
bfcacheEntry.get(), mFrameLoader->IsRemoteFrame()));
}
ChangeFrameLoaderCommon(owner, retainPaint);
UpdateFocusAndMouseEnterStateAfterFrameLoaderChange(owner);
}
void nsFrameLoaderOwner::ChangeFrameLoaderCommon(Element* aOwner,
bool aRetainPaint) {
// Now that we've got a new FrameLoader, we need to reset our
// nsSubDocumentFrame to use the new FrameLoader.
if (nsSubDocumentFrame* ourFrame = do_QueryFrame(aOwner->GetPrimaryFrame())) {
auto retain = aRetainPaint ? nsSubDocumentFrame::RetainPaintData::Yes
: nsSubDocumentFrame::RetainPaintData::No;
ourFrame->ResetFrameLoader(retain);
}
if (aOwner->IsXULElement()) {
// Assuming this element is a XULFrameElement, once we've reset our
// FrameLoader, fire an event to act like we've recreated ourselves, similar
// to what XULFrameElement does after rebinding to the tree.
// ChromeOnlyDispatch is turns on to make sure this isn't fired into
// content.
mozilla::AsyncEventDispatcher::RunDOMEventWhenSafe(
*aOwner, u"XULFrameLoaderCreated"_ns, mozilla::CanBubble::eYes,
mozilla::ChromeOnlyDispatch::eYes);
}
if (mFrameLoader) {
mFrameLoader->PropagateIsUnderHiddenEmbedderElement(
!aOwner->GetPrimaryFrame() ||
!aOwner->GetPrimaryFrame()->StyleVisibility()->IsVisible());
}
}
void nsFrameLoaderOwner::UpdateFocusAndMouseEnterStateAfterFrameLoaderChange() {
RefPtr<Element> owner = do_QueryObject(this);
UpdateFocusAndMouseEnterStateAfterFrameLoaderChange(owner);
}
void nsFrameLoaderOwner::UpdateFocusAndMouseEnterStateAfterFrameLoaderChange(
Element* aOwner) {
// If the element is focused, or the current mouse over target then
// we need to update that state for the new BrowserParent too.
if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
if (fm->GetFocusedElement() == aOwner) {
fm->FixUpFocusAfterFrameLoaderChange(*aOwner);
}
}
if (aOwner->GetPrimaryFrame()) {
EventStateManager* eventManager =
aOwner->GetPrimaryFrame()->PresContext()->EventStateManager();
eventManager->RecomputeMouseEnterStateForRemoteFrame(*aOwner);
}
}
void nsFrameLoaderOwner::ChangeRemoteness(
const mozilla::dom::RemotenessOptions& aOptions, mozilla::ErrorResult& rv) {
bool isRemote = !aOptions.mRemoteType.IsEmpty();
std::function<void()> frameLoaderInit = [&] {
if (isRemote) {
mFrameLoader->ConfigRemoteProcess(aOptions.mRemoteType, nullptr);
}
if (aOptions.mPendingSwitchID.WasPassed()) {
mFrameLoader->ResumeLoad(aOptions.mPendingSwitchID.Value());
} else {
mFrameLoader->LoadFrame(/* aOriginalSrc */ false,
/* aShouldCheckForRecursion */ false);
}
};
auto shouldPreserve = ShouldPreserveBrowsingContext(
isRemote, /* replaceBrowsingContext */ false);
NavigationIsolationOptions options;
ChangeRemotenessCommon(shouldPreserve, options,
aOptions.mSwitchingInProgressLoad, isRemote,
/* group */ nullptr, frameLoaderInit, rv);
}
void nsFrameLoaderOwner::ChangeRemotenessWithBridge(BrowserBridgeChild* aBridge,
mozilla::ErrorResult& rv) {
MOZ_ASSERT(XRE_IsContentProcess());
if (NS_WARN_IF(!mFrameLoader)) {
rv.Throw(NS_ERROR_UNEXPECTED);
return;
}
std::function<void()> frameLoaderInit = [&] {
MOZ_DIAGNOSTIC_ASSERT(!mFrameLoader->mInitialized);
RefPtr<BrowserBridgeHost> host = aBridge->FinishInit(mFrameLoader);
mFrameLoader->mPendingBrowsingContext->SetEmbedderElement(
mFrameLoader->GetOwnerContent());
mFrameLoader->mRemoteBrowser = host;
mFrameLoader->mInitialized = true;
};
NavigationIsolationOptions options;
ChangeRemotenessCommon(ChangeRemotenessContextType::PRESERVE, options,
/* inProgress */ true,
/* isRemote */ true, /* group */ nullptr,
frameLoaderInit, rv);
}
void nsFrameLoaderOwner::ChangeRemotenessToProcess(
ContentParent* aContentParent, const NavigationIsolationOptions& aOptions,
BrowsingContextGroup* aGroup, mozilla::ErrorResult& rv) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT_IF(aGroup, aOptions.mReplaceBrowsingContext);
bool isRemote = aContentParent != nullptr;
std::function<void()> frameLoaderInit = [&] {
if (isRemote) {
mFrameLoader->ConfigRemoteProcess(aContentParent->GetRemoteType(),
aContentParent);
}
};
auto shouldPreserve =
ShouldPreserveBrowsingContext(isRemote, aOptions.mReplaceBrowsingContext);
ChangeRemotenessCommon(shouldPreserve, aOptions, /* inProgress */ true,
isRemote, aGroup, frameLoaderInit, rv);
}
void nsFrameLoaderOwner::SubframeCrashed() {
MOZ_ASSERT(XRE_IsContentProcess());
std::function<void()> frameLoaderInit = [&] {
RefPtr<nsFrameLoader> frameLoader = mFrameLoader;
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
"nsFrameLoaderOwner::SubframeCrashed", [frameLoader]() {
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), "about:blank");
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
RefPtr<nsDocShell> docShell =
frameLoader->GetDocShell(IgnoreErrors());
if (NS_WARN_IF(!docShell)) {
return;
}
bool displayed = false;
docShell->DisplayLoadError(NS_ERROR_FRAME_CRASHED, uri,
u"about:blank", nullptr, &displayed);
}));
};
NavigationIsolationOptions options;
ChangeRemotenessCommon(ChangeRemotenessContextType::PRESERVE, options,
/* inProgress */ false, /* isRemote */ false,
/* group */ nullptr, frameLoaderInit, IgnoreErrors());
}
void nsFrameLoaderOwner::RestoreFrameLoaderFromBFCache(
nsFrameLoader* aNewFrameLoader) {
MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
("nsFrameLoaderOwner::RestoreFrameLoaderFromBFCache: Replace "
"frameloader"));
Maybe<bool> renderLayers;
if (mFrameLoader) {
if (auto* oldParent = mFrameLoader->GetBrowserParent()) {
renderLayers.emplace(oldParent->GetRenderLayers());
}
}
mFrameLoader = aNewFrameLoader;
if (auto* browserParent = mFrameLoader->GetBrowserParent()) {
browserParent->AddWindowListeners();
if (renderLayers.isSome()) {
browserParent->SetRenderLayers(renderLayers.value());
}
}
RefPtr<Element> owner = do_QueryObject(this);
ChangeFrameLoaderCommon(owner, /* aRetainPaint = */ false);
}
void nsFrameLoaderOwner::AttachFrameLoader(nsFrameLoader* aFrameLoader) {
mFrameLoaderList.insertBack(aFrameLoader);
}
void nsFrameLoaderOwner::DetachFrameLoader(nsFrameLoader* aFrameLoader) {
if (aFrameLoader->isInList()) {
MOZ_ASSERT(mFrameLoaderList.contains(aFrameLoader));
aFrameLoader->remove();
}
}
void nsFrameLoaderOwner::FrameLoaderDestroying(nsFrameLoader* aFrameLoader,
bool aDestroyBFCached) {
if (aFrameLoader == mFrameLoader) {
if (aDestroyBFCached) {
while (!mFrameLoaderList.isEmpty()) {
RefPtr<nsFrameLoader> loader = mFrameLoaderList.popFirst();
if (loader != mFrameLoader) {
loader->Destroy();
}
}
}
} else {
DetachFrameLoader(aFrameLoader);
}
}