Skip to content

Commit

Permalink
Bug 1655866: Part 3 - Refactor PermitUnload to simplify handling OOP …
Browse files Browse the repository at this point in the history
…frames. r=nika

Differential Revision: https://phabricator.services.mozilla.com/D88316
  • Loading branch information
kmaglione committed Sep 21, 2020
1 parent cafc571 commit ef7d925
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 105 deletions.
37 changes: 24 additions & 13 deletions docshell/base/nsIContentViewer.idl
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,31 @@ interface nsIContentViewer : nsISupports

[notxpcom,nostdcall] readonly attribute boolean isStopped;

/**
* aPermitUnloadFlags are passed to PermitUnload to indicate what action to take
* if a beforeunload handler wants to prompt the user. It is also used by
* permitUnloadInternal to ensure we only prompt once.
/**
* 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.
*/
const unsigned long ePrompt = 0;
const unsigned long eDontPromptAndDontUnload = 1;
const unsigned long eDontPromptAndUnload = 2;
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
Expand All @@ -77,22 +89,21 @@ interface nsIContentViewer : nsISupports

/**
* Checks if the document wants to prevent unloading by firing beforeunload on
* the document, and if it does, takes action directed by aPermitUnloadFlags.
* the document.
* The result is returned.
*/
boolean permitUnload([optional] in unsigned long aPermitUnloadFlags);
boolean permitUnload([optional] in nsIContentViewer_PermitUnloadAction aAction);

/**
* Exposes whether we're blocked in a call to permitUnload.
*/
readonly attribute boolean inPermitUnload;

/**
* As above, but this passes around the aPermitUnloadFlags argument to keep
* track of whether the user has responded to a prompt.
* Used internally by the scriptable version to ensure we only prompt once.
* Dispatches the "beforeunload" event and returns the result, as documented
* in the `PermitUnloadResult` enum.
*/
[noscript,nostdcall] boolean permitUnloadInternal(inout unsigned long aPermitUnloadFlags);
[noscript,nostdcall,notxpcom] nsIContentViewer_PermitUnloadResult dispatchBeforeUnload();

/**
* Exposes whether we're in the process of firing the beforeunload event.
Expand Down
180 changes: 88 additions & 92 deletions layout/base/nsDocumentViewer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1195,29 +1195,99 @@ bool nsDocumentViewer::GetLoadCompleted() { return mLoaded; }
bool nsDocumentViewer::GetIsStopped() { return mStopped; }

NS_IMETHODIMP
nsDocumentViewer::PermitUnload(uint32_t aPermitUnloadFlags,
nsDocumentViewer::PermitUnload(PermitUnloadAction aAction,
bool* aPermitUnload) {
return PermitUnloadInternal(&aPermitUnloadFlags, aPermitUnload);
}

nsresult nsDocumentViewer::PermitUnloadInternal(uint32_t* aPermitUnloadFlags,
bool* aPermitUnload) {
AutoDontWarnAboutSyncXHR disableSyncXHRWarning;
if (StaticPrefs::dom_disable_beforeunload()) {
aAction = eDontPromptAndUnload;
}

nsresult rv = NS_OK;
*aPermitUnload = true;

if (!mDocument || mInPermitUnload || mInPermitUnloadPrompt) {
RefPtr<BrowsingContext> bc = mContainer->GetBrowsingContext();
if (!bc) {
return NS_OK;
}

// First, get the script global object from the document...
nsPIDOMWindowOuter* window = mDocument->GetWindow();
// Per spec, we need to increase the ignore-opens-during-unload counter while
// dispatching the "beforeunload" event on both the document we're currently
// dispatching the event to and the document that we explicitly asked to
// unload.
IgnoreOpensDuringUnload ignoreOpens(mDocument);

bool foundBlocker = false;
bc->PreOrderWalk([&](BrowsingContext* aBC) {
if (aBC->IsInProcess()) {
nsCOMPtr<nsIContentViewer> contentViewer;
aBC->GetDocShell()->GetContentViewer(getter_AddRefs(contentViewer));
if (contentViewer &&
contentViewer->DispatchBeforeUnload() == eRequestBlockNavigation) {
foundBlocker = true;
}
}
});

// NB: we nullcheck mDocument because it might now be dead as a result of
// the event being dispatched.
if (!mDocument) {
return NS_OK;
}

if (foundBlocker) {
if (aAction == eDontPromptAndUnload) {
// Ask the user if it's ok to unload the current page

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;

// 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
// was active. In those cases we don't want our consumer to also unload
// the page.
//
// 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;
}

mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION,
(*aPermitUnload ? 1 : 0));
}
} else if (aAction == eDontPromptAndDontUnload) {
*aPermitUnload = false;
}
}

return NS_OK;
}

PermitUnloadResult nsDocumentViewer::DispatchBeforeUnload() {
AutoDontWarnAboutSyncXHR disableSyncXHRWarning;

if (!mDocument || mInPermitUnload || mInPermitUnloadPrompt) {
return eAllowNavigation;
}

// First, get the script global object from the document...
auto* window = nsGlobalWindowOuter::Cast(mDocument->GetWindow());
if (!window) {
// This is odd, but not fatal
NS_WARNING("window not set for document!");
return NS_OK;
return eAllowNavigation;
}

NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), "This is unsafe");
Expand All @@ -1244,109 +1314,35 @@ nsresult nsDocumentViewer::PermitUnloadInternal(uint32_t* aPermitUnloadFlags,
// onbeforeunload event, don't let that happen. (see also bug#331040)
RefPtr<nsDocumentViewer> kungFuDeathGrip(this);

bool dialogsAreEnabled = false;
{
// Never permit popups from the beforeunload handler, no matter
// how we get here.
AutoPopupStatePusher popupStatePusher(PopupBlocker::openAbused, true);

// Never permit dialogs from the beforeunload handler
nsGlobalWindowOuter* globalWindow = nsGlobalWindowOuter::Cast(window);
dialogsAreEnabled = globalWindow->AreDialogsEnabled();
nsGlobalWindowOuter::TemporarilyDisableDialogs disableDialogs(globalWindow);
nsGlobalWindowOuter::TemporarilyDisableDialogs disableDialogs(window);

Document::PageUnloadingEventTimeStamp timestamp(mDocument);

mInPermitUnload = true;
EventDispatcher::DispatchDOMEvent(window, nullptr, event, mPresContext,
nullptr);
EventDispatcher::DispatchDOMEvent(ToSupports(window), nullptr, event,
mPresContext, nullptr);
mInPermitUnload = false;
}

nsCOMPtr<nsIDocShell> docShell(mContainer);
nsAutoString text;
event->GetReturnValue(text);

if (StaticPrefs::dom_disable_beforeunload()) {
*aPermitUnloadFlags = eDontPromptAndUnload;
}

// NB: we nullcheck mDocument because it might now be dead as a result of
// the event being dispatched.
if (*aPermitUnloadFlags != eDontPromptAndUnload && dialogsAreEnabled &&
mDocument && !(mDocument->GetSandboxFlags() & SANDBOXED_MODALS) &&
if (window->AreDialogsEnabled() && mDocument &&
!(mDocument->GetSandboxFlags() & SANDBOXED_MODALS) &&
(!StaticPrefs::dom_require_user_interaction_for_beforeunload() ||
mDocument->UserHasInteracted()) &&
(event->WidgetEventPtr()->DefaultPrevented() || !text.IsEmpty())) {
// If the consumer wants prompt requests to just stop unloading, we don't
// need to prompt and can return immediately.
if (*aPermitUnloadFlags == eDontPromptAndDontUnload) {
*aPermitUnload = false;
return NS_OK;
}

// Ask the user if it's ok to unload the current page

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(docShell->GetBrowsingContext(),
aPermitUnload);
mInPermitUnloadPrompt = false;

// 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
// was active. In those cases we don't want our consumer to also unload
// the page.
//
// 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;
}

mozilla::Telemetry::Accumulate(
mozilla::Telemetry::ONBEFOREUNLOAD_PROMPT_ACTION,
(*aPermitUnload ? 1 : 0));
// If the user decided to go ahead, make sure not to prompt the user again
// by toggling the internal prompting bool to false:
if (*aPermitUnload) {
*aPermitUnloadFlags = eDontPromptAndUnload;
}
}
return eRequestBlockNavigation;
}

if (docShell) {
int32_t childCount;
docShell->GetInProcessChildCount(&childCount);

for (int32_t i = 0; i < childCount && *aPermitUnload; ++i) {
nsCOMPtr<nsIDocShellTreeItem> item;
docShell->GetInProcessChildAt(i, getter_AddRefs(item));

nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(item));

if (docShell) {
nsCOMPtr<nsIContentViewer> cv;
docShell->GetContentViewer(getter_AddRefs(cv));

if (cv) {
cv->PermitUnloadInternal(aPermitUnloadFlags, aPermitUnload);
}
}
}
}

return NS_OK;
return eAllowNavigation;
}

NS_IMETHODIMP
Expand Down

0 comments on commit ef7d925

Please sign in to comment.