Bug 1655866: Part 4 - Handle OOP beforeunload listeners in PermitUnload(). r=nika

Differential Revision: https://phabricator.services.mozilla.com/D88317
This commit is contained in:
Kris Maglione 2020-09-21 22:41:01 +00:00
parent aa7e3fbdb4
commit 0ae5bf64c5
10 changed files with 322 additions and 32 deletions

View File

@ -842,14 +842,21 @@ void BrowsingContext::UnregisterWindowContext(WindowContext* aWindow) {
void BrowsingContext::PreOrderWalk(
const std::function<void(BrowsingContext*)>& aCallback) {
aCallback(this);
for (auto& child : Children()) {
AutoTArray<RefPtr<BrowsingContext>, 8> children;
children.AppendElements(Children());
for (auto& child : children) {
child->PreOrderWalk(aCallback);
}
}
void BrowsingContext::PostOrderWalk(
const std::function<void(BrowsingContext*)>& aCallback) {
for (auto& child : Children()) {
AutoTArray<RefPtr<BrowsingContext>, 8> children;
children.AppendElements(Children());
for (auto& child : children) {
child->PostOrderWalk(aCallback);
}

View File

@ -338,3 +338,14 @@ interface nsIContentViewer : nsISupports
[noscript, notxpcom] Encoding getHintCharset();
[noscript, notxpcom] void setHintCharset(in Encoding aEncoding);
};
%{C++
namespace mozilla {
namespace dom {
using XPCOMPermitUnloadAction = nsIContentViewer::PermitUnloadAction;
using PermitUnloadResult = nsIContentViewer::PermitUnloadResult;
} // namespace dom
} // namespace mozilla
%}

View File

@ -4217,6 +4217,44 @@ mozilla::ipc::IPCResult ContentChild::RecvDispatchLocationChangeEvent(
return IPC_OK();
}
mozilla::ipc::IPCResult ContentChild::RecvDispatchBeforeUnloadToSubtree(
const MaybeDiscarded<BrowsingContext>& aStartingAt,
DispatchBeforeUnloadToSubtreeResolver&& aResolver) {
if (aStartingAt.IsNullOrDiscarded()) {
aResolver(nsIContentViewer::eAllowNavigation);
} else {
DispatchBeforeUnloadToSubtree(aStartingAt.get(), std::move(aResolver));
}
return IPC_OK();
}
/* static */ void ContentChild::DispatchBeforeUnloadToSubtree(
BrowsingContext* aStartingAt,
const DispatchBeforeUnloadToSubtreeResolver& aResolver) {
bool resolved = false;
aStartingAt->PreOrderWalk([&](dom::BrowsingContext* aBC) {
if (aBC->IsInProcess()) {
nsCOMPtr<nsIContentViewer> contentViewer;
aBC->GetDocShell()->GetContentViewer(getter_AddRefs(contentViewer));
if (contentViewer &&
contentViewer->DispatchBeforeUnload() ==
nsIContentViewer::eRequestBlockNavigation &&
!resolved) {
// Send our response as soon as we find any blocker, so that we can show
// the permit unload prompt as soon as possible, without giving
// subsequent handlers a chance to delay it.
aResolver(nsIContentViewer::eRequestBlockNavigation);
resolved = true;
}
}
});
if (!resolved) {
aResolver(nsIContentViewer::eAllowNavigation);
}
}
mozilla::ipc::IPCResult ContentChild::RecvGoBack(
const MaybeDiscarded<BrowsingContext>& aContext,
const Maybe<int32_t>& aCancelContentJSEpoch, bool aRequireUserInteraction) {

View File

@ -814,9 +814,18 @@ class ContentChild final : public PContentChild,
mozilla::ipc::IPCResult RecvDispatchLocationChangeEvent(
const MaybeDiscarded<BrowsingContext>& aContext);
mozilla::ipc::IPCResult RecvFlushFOGData(FlushFOGDataResolver&& aResolver);
mozilla::ipc::IPCResult RecvDispatchBeforeUnloadToSubtree(
const MaybeDiscarded<BrowsingContext>& aStartingAt,
DispatchBeforeUnloadToSubtreeResolver&& aResolver);
public:
static void DispatchBeforeUnloadToSubtree(
BrowsingContext* aStartingAt,
const DispatchBeforeUnloadToSubtreeResolver& aResolver);
private:
mozilla::ipc::IPCResult RecvFlushFOGData(FlushFOGDataResolver&& aResolver);
#ifdef NIGHTLY_BUILD
virtual PContentChild::Result OnMessageReceived(const Message& aMsg) override;
#else

View File

@ -10,6 +10,7 @@
#include "ipc/IPCMessageUtils.h"
#include "nsCOMPtr.h"
#include "nsDocShellLoadState.h"
#include "nsIContentViewer.h"
#include "mozilla/ScrollbarPreferences.h"
namespace mozilla {
@ -34,6 +35,20 @@ struct ParamTraits<mozilla::ScrollbarPreference>
mozilla::ScrollbarPreference, mozilla::ScrollbarPreference::Auto,
mozilla::ScrollbarPreference::LAST> {};
template <>
struct ParamTraits<mozilla::dom::PermitUnloadResult>
: public ContiguousEnumSerializerInclusive<
mozilla::dom::PermitUnloadResult,
mozilla::dom::PermitUnloadResult::eAllowNavigation,
mozilla::dom::PermitUnloadResult::eRequestBlockNavigation> {};
template <>
struct ParamTraits<mozilla::dom::XPCOMPermitUnloadAction>
: public ContiguousEnumSerializerInclusive<
mozilla::dom::XPCOMPermitUnloadAction,
mozilla::dom::XPCOMPermitUnloadAction::ePrompt,
mozilla::dom::XPCOMPermitUnloadAction::eDontPromptAndUnload> {};
} // namespace IPC
#endif // mozilla_dom_docshell_message_utils_h__

View File

@ -105,6 +105,7 @@ using mozilla::CrossProcessMutexHandle from "mozilla/ipc/CrossProcessMutex.h";
using mozilla::dom::MaybeDiscardedBrowsingContext from "mozilla/dom/BrowsingContext.h";
using mozilla::dom::BrowsingContextTransaction from "mozilla/dom/BrowsingContext.h";
using mozilla::dom::BrowsingContextInitializer from "mozilla/dom/BrowsingContext.h";
using mozilla::dom::PermitUnloadResult from "mozilla/dom/DocShellMessageUtils.h";
using mozilla::dom::MaybeDiscardedWindowContext from "mozilla/dom/WindowContext.h";
using mozilla::dom::WindowContextTransaction from "mozilla/dom/WindowContext.h";
using base::SharedMemoryHandle from "base/shared_memory.h";
@ -922,6 +923,12 @@ child:
async DispatchLocationChangeEvent(MaybeDiscardedBrowsingContext aContext);
// Dispatches a "beforeunload" event to each in-process content window in the
// subtree beginning at `aStartingAt`, and returns the result as documented in
// the `PermitUnloadResult` enum.
async DispatchBeforeUnloadToSubtree(MaybeDiscardedBrowsingContext aStartingAt)
returns (PermitUnloadResult result);
parent:
/**
* This is a temporary way to pass index and length through parent process.

View File

@ -20,6 +20,7 @@ using mozilla::gfx::IntRect from "mozilla/gfx/Rect.h";
using moveonly mozilla::gfx::PaintFragment from "mozilla/gfx/CrossProcessPaint.h";
using nscolor from "nsColor.h";
using refcounted class nsDocShellLoadState from "nsDocShellLoadState.h";
using mozilla::dom::XPCOMPermitUnloadAction from "mozilla/dom/DocShellMessageUtils.h";
using mozilla::dom::TabId from "mozilla/dom/ipc/IdType.h";
using mozilla::layers::LayersId from "mozilla/layers/LayersTypes.h";
using refcounted class nsITransportSecurityInfo from "nsITransportSecurityInfo.h";
@ -145,6 +146,7 @@ parent:
TimeStamp aLoadStart,
TimeStamp aLoadEnd);
/**
* Submit Time-to-First-Interaction telemetry correlated with whether or not
* the document tree preloaded any resources.
@ -157,6 +159,15 @@ parent:
*/
async SubmitLoadInputEventResponsePreloadTelemetry(uint32_t aMillis);
// Checks whether any "beforeunload" event listener in the document subtree
// wants to block unload, and prompts the user to allow if any does (depending
// on the action specified, using nsIContentViewer::PermitUnloadAction
// values). The sender is responsible for checking documents in its own
// process, and passing true for `aHasInProcessBlocker` if any exist. Windows
// hosted outside of the caller process will be checked automatically.
async CheckPermitUnload(bool aHasInProcessBlocker, XPCOMPermitUnloadAction aAction)
returns (bool permitUnload);
async Destroy();
};

View File

@ -12,9 +12,11 @@
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/InProcessParent.h"
#include "mozilla/dom/BrowserBridgeParent.h"
#include "mozilla/dom/BrowsingContextGroup.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/ClientInfo.h"
#include "mozilla/dom/ClientIPCTypes.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/BrowserHost.h"
#include "mozilla/dom/BrowserParent.h"
@ -39,6 +41,7 @@
#include "nsFrameLoaderOwner.h"
#include "nsSerializationHelper.h"
#include "nsIBrowser.h"
#include "nsIPromptCollection.h"
#include "nsITransportSecurityInfo.h"
#include "nsISharePicker.h"
#include "mozilla/Telemetry.h"
@ -669,6 +672,185 @@ WindowGlobalParent::RecvSubmitLoadInputEventResponsePreloadTelemetry(
return IPC_OK();
}
namespace {
class CheckPermitUnloadRequest final : public PromiseNativeHandler {
public:
CheckPermitUnloadRequest(WindowGlobalParent* aWGP, bool aHasInProcessBlocker,
nsIContentViewer::PermitUnloadAction aAction,
std::function<void(bool)>&& aResolver)
: mResolver(std::move(aResolver)),
mWGP(aWGP),
mAction(aAction),
mFoundBlocker(aHasInProcessBlocker) {}
void Run(ContentParent* aIgnoreProcess = nullptr) {
MOZ_ASSERT(mState == State::UNINITIALIZED);
mState = State::WAITING;
RefPtr<CheckPermitUnloadRequest> self(this);
AutoTArray<ContentParent*, 8> seen;
if (aIgnoreProcess) {
seen.AppendElement(aIgnoreProcess);
}
BrowsingContext* bc = mWGP->GetBrowsingContext();
bc->PreOrderWalk([&](dom::BrowsingContext* aBC) {
if (WindowGlobalParent* wgp =
aBC->Canonical()->GetCurrentWindowGlobal()) {
ContentParent* cp = wgp->GetContentParent();
if (wgp->HasBeforeUnload() && !seen.ContainsSorted(cp)) {
seen.InsertElementSorted(cp);
mPendingRequests++;
auto resolve = [self](bool blockNavigation) {
if (blockNavigation) {
self->mFoundBlocker = true;
}
self->ResolveRequest();
};
if (cp) {
cp->SendDispatchBeforeUnloadToSubtree(
bc, resolve, [self](auto) { self->ResolveRequest(); });
} else {
ContentChild::DispatchBeforeUnloadToSubtree(bc, resolve);
}
}
}
});
CheckDoneWaiting();
}
void ResolveRequest() {
mPendingRequests--;
CheckDoneWaiting();
}
void CheckDoneWaiting() {
// If we've found a blocker, we prompt immediately without waiting for
// further responses. The user's response applies to the entire navigation
// attempt, regardless of how many "beforeunload" listeners we call.
if (mState != State::WAITING || (mPendingRequests && !mFoundBlocker)) {
return;
}
mState = State::PROMPTING;
if (!mFoundBlocker) {
SendReply(true);
return;
}
auto action = mAction;
if (StaticPrefs::dom_disable_beforeunload()) {
action = nsIContentViewer::eDontPromptAndUnload;
}
if (action != nsIContentViewer::ePrompt) {
SendReply(action == nsIContentViewer::eDontPromptAndUnload);
return;
}
// Handle any failure in prompting by aborting the navigation. See comment
// in nsContentViewer::PermitUnload for reasoning.
auto cleanup = MakeScopeExit([&]() { SendReply(false); });
if (nsCOMPtr<nsIPromptCollection> prompt =
do_GetService("@mozilla.org/embedcomp/prompt-collection;1")) {
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_COUNT, 1);
RefPtr<Promise> promise;
prompt->AsyncBeforeUnloadCheck(mWGP->GetBrowsingContext(),
getter_AddRefs(promise));
if (!promise) {
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION, 2);
return;
}
promise->AppendNativeHandler(this);
cleanup.release();
}
}
void SendReply(bool aAllow) {
MOZ_ASSERT(mState != State::REPLIED);
mResolver(aAllow);
mState = State::REPLIED;
}
void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override {
MOZ_ASSERT(mState == State::PROMPTING);
bool allow = JS::ToBoolean(aValue);
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION, (allow ? 1 : 0));
SendReply(allow);
}
void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override {
MOZ_ASSERT(mState == State::PROMPTING);
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION, 2);
SendReply(false);
}
NS_DECL_ISUPPORTS
private:
~CheckPermitUnloadRequest() {
// We may get here without having sent a reply if the promise we're waiting
// on is destroyed without being resolved or rejected.
if (mState != State::REPLIED) {
SendReply(false);
}
}
enum class State : uint8_t {
UNINITIALIZED,
WAITING,
PROMPTING,
REPLIED,
};
std::function<void(bool)> mResolver;
RefPtr<WindowGlobalParent> mWGP;
uint32_t mPendingRequests = 0;
nsIContentViewer::PermitUnloadAction mAction;
State mState = State::UNINITIALIZED;
bool mFoundBlocker = false;
};
NS_IMPL_ISUPPORTS0(CheckPermitUnloadRequest)
} // namespace
mozilla::ipc::IPCResult WindowGlobalParent::RecvCheckPermitUnload(
bool aHasInProcessBlocker, XPCOMPermitUnloadAction aAction,
CheckPermitUnloadResolver&& aResolver) {
if (!IsCurrentGlobal()) {
aResolver(false);
return IPC_OK();
}
auto request = MakeRefPtr<CheckPermitUnloadRequest>(
this, aHasInProcessBlocker, aAction, std::move(aResolver));
request->Run(/* aIgnoreProcess */ GetContentParent());
return IPC_OK();
}
already_AddRefed<mozilla::dom::Promise> WindowGlobalParent::DrawSnapshot(
const DOMRect* aRect, double aScale, const nsACString& aBackgroundColor,
mozilla::ErrorResult& aRv) {

View File

@ -254,6 +254,10 @@ class WindowGlobalParent final : public WindowContext,
mozilla::ipc::IPCResult RecvSubmitLoadInputEventResponsePreloadTelemetry(
uint32_t aMillis);
mozilla::ipc::IPCResult RecvCheckPermitUnload(
bool aHasInProcessBlocker, XPCOMPermitUnloadAction aAction,
CheckPermitUnloadResolver&& aResolver);
private:
WindowGlobalParent(CanonicalBrowsingContext* aBrowsingContext,
uint64_t aInnerWindowId, uint64_t aOuterWindowId,

View File

@ -1201,7 +1201,6 @@ nsDocumentViewer::PermitUnload(PermitUnloadAction aAction,
aAction = eDontPromptAndUnload;
}
nsresult rv = NS_OK;
*aPermitUnload = true;
RefPtr<BrowsingContext> bc = mContainer->GetBrowsingContext();
@ -1216,6 +1215,7 @@ nsDocumentViewer::PermitUnload(PermitUnloadAction aAction,
IgnoreOpensDuringUnload ignoreOpens(mDocument);
bool foundBlocker = false;
bool foundOOPListener = false;
bc->PreOrderWalk([&](BrowsingContext* aBC) {
if (aBC->IsInProcess()) {
nsCOMPtr<nsIContentViewer> contentViewer;
@ -1224,30 +1224,45 @@ nsDocumentViewer::PermitUnload(PermitUnloadAction aAction,
contentViewer->DispatchBeforeUnload() == eRequestBlockNavigation) {
foundBlocker = true;
}
} else {
WindowContext* wc = aBC->GetCurrentWindowContext();
if (wc && wc->HasBeforeUnload()) {
foundOOPListener = true;
}
}
});
if (!foundOOPListener) {
if (!foundBlocker) {
return NS_OK;
}
if (aAction != ePrompt) {
*aPermitUnload = aAction == eDontPromptAndUnload;
return NS_OK;
}
}
// NB: we nullcheck mDocument because it might now be dead as a result of
// the event being dispatched.
if (!mDocument) {
RefPtr<WindowGlobalChild> wgc(mDocument ? mDocument->GetWindowGlobalChild()
: nullptr);
if (!wgc) {
return NS_OK;
}
if (foundBlocker) {
if (aAction == eDontPromptAndUnload) {
// Ask the user if it's ok to unload the current page
nsAutoSyncOperation sync(mDocument);
AutoSuppressEventHandlingAndSuspend seh(bc->Group());
nsCOMPtr<nsIPromptCollection> prompt =
do_GetService("@mozilla.org/embedcomp/prompt-collection;1");
if (prompt) {
nsAutoSyncOperation sync(mDocument);
mInPermitUnloadPrompt = true;
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_COUNT, 1);
rv = prompt->BeforeUnloadCheck(bc, aPermitUnload);
mInPermitUnloadPrompt = false;
mInPermitUnloadPrompt = true;
bool done = false;
wgc->SendCheckPermitUnload(
foundBlocker, aAction,
[&](bool aPermit) {
done = true;
*aPermitUnload = aPermit;
},
[&](auto) {
// If the prompt aborted, we tell our consumer that it is not allowed
// to unload the page. One reason that prompts abort is that the user
// performed some action that caused the page to unload while our prompt
@ -1256,22 +1271,13 @@ nsDocumentViewer::PermitUnload(PermitUnloadAction aAction,
//
// XXX: Are there other cases where prompts can abort? Is it ok to
// prevent unloading the page in those cases?
if (NS_FAILED(rv)) {
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION, 2);
*aPermitUnload = false;
return NS_OK;
}
done = true;
*aPermitUnload = false;
});
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION,
(*aPermitUnload ? 1 : 0));
}
} else if (aAction == eDontPromptAndDontUnload) {
*aPermitUnload = false;
}
}
SpinEventLoopUntil([&]() { return done; });
mInPermitUnloadPrompt = false;
return NS_OK;
}