Bug 1710887 - Make modal state handling in window.print() more reliable. r=smaug

If afterprint removes the window we're printing, we'd call
LeaveModalState() without a top window, and never leave the modal state.

Differential Revision: https://phabricator.services.mozilla.com/D115007
This commit is contained in:
Emilio Cobos Álvarez 2021-05-12 19:26:20 +00:00
parent e2075d6bdd
commit d973da3734
6 changed files with 118 additions and 23 deletions

View File

@ -5237,6 +5237,20 @@ void nsGlobalWindowOuter::PrintOuter(ErrorResult& aError) {
#endif
}
class MOZ_RAII AutoModalState {
public:
explicit AutoModalState(nsGlobalWindowOuter& aWin)
: mModalStateWin(aWin.EnterModalState()) {}
~AutoModalState() {
if (mModalStateWin) {
mModalStateWin->LeaveModalState();
}
}
RefPtr<nsGlobalWindowOuter> mModalStateWin;
};
Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
nsIPrintSettings* aPrintSettings, nsIWebProgressListener* aListener,
nsIDocShell* aDocShellToCloneInto, IsPreview aIsPreview,
@ -5265,8 +5279,7 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
}
nsAutoSyncOperation sync(docToPrint, SyncOperationBehavior::eAllowInput);
EnterModalState();
auto exitModal = MakeScopeExit([&] { LeaveModalState(); });
AutoModalState modalState(*this);
nsCOMPtr<nsIContentViewer> cv;
RefPtr<BrowsingContext> bc;
@ -6327,14 +6340,14 @@ void nsGlobalWindowOuter::ReallyCloseWindow() {
CleanUp();
}
void nsGlobalWindowOuter::EnterModalState() {
nsGlobalWindowOuter* nsGlobalWindowOuter::EnterModalState() {
// GetInProcessScriptableTop, not GetInProcessTop, so that EnterModalState
// works properly with <iframe mozbrowser>.
nsGlobalWindowOuter* topWin = GetInProcessScriptableTopInternal();
if (!topWin) {
NS_ERROR("Uh, EnterModalState() called w/o a reachable top window?");
return;
return nullptr;
}
// If there is an active ESM in this window, clear it. Otherwise, this can
@ -6389,33 +6402,38 @@ void nsGlobalWindowOuter::EnterModalState() {
}
}
topWin->mModalStateDepth++;
return topWin;
}
void nsGlobalWindowOuter::LeaveModalState() {
nsGlobalWindowOuter* topWin = GetInProcessScriptableTopInternal();
{
nsGlobalWindowOuter* topWin = GetInProcessScriptableTopInternal();
if (!topWin) {
NS_WARNING("Uh, LeaveModalState() called w/o a reachable top window?");
return;
}
if (!topWin) {
NS_WARNING("Uh, LeaveModalState() called w/o a reachable top window?");
return;
if (topWin != this) {
MOZ_ASSERT(IsSuspended());
return topWin->LeaveModalState();
}
}
MOZ_ASSERT(topWin->mModalStateDepth != 0);
MOZ_ASSERT(mModalStateDepth != 0);
MOZ_ASSERT(IsSuspended());
MOZ_ASSERT(topWin->IsSuspended());
topWin->mModalStateDepth--;
mModalStateDepth--;
nsGlobalWindowInner* inner = topWin->GetCurrentInnerWindowInternal();
if (topWin->mModalStateDepth == 0) {
nsGlobalWindowInner* inner = GetCurrentInnerWindowInternal();
if (mModalStateDepth == 0) {
if (inner) {
inner->Resume();
}
if (topWin->mSuspendedDoc) {
nsCOMPtr<Document> currentDoc = topWin->GetExtantDoc();
topWin->mSuspendedDoc->UnsuppressEventHandlingAndFireEvents(
currentDoc == topWin->mSuspendedDoc);
topWin->mSuspendedDoc = nullptr;
if (mSuspendedDoc) {
nsCOMPtr<Document> currentDoc = GetExtantDoc();
mSuspendedDoc->UnsuppressEventHandlingAndFireEvents(currentDoc ==
mSuspendedDoc);
mSuspendedDoc = nullptr;
}
}
@ -6424,12 +6442,12 @@ void nsGlobalWindowOuter::LeaveModalState() {
inner->mLastDialogQuitTime = TimeStamp::Now();
}
if (topWin->mModalStateDepth == 0) {
if (mModalStateDepth == 0) {
RefPtr<Event> event = NS_NewDOMEvent(inner, nullptr, nullptr);
event->InitEvent(u"endmodalstate"_ns, true, false);
event->SetTrusted(true);
event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
topWin->DispatchEvent(*event);
DispatchEvent(*event);
}
}

View File

@ -328,7 +328,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget,
// Outer windows only.
virtual void EnsureSizeAndPositionUpToDate() override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void EnterModalState() override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual nsGlobalWindowOuter* EnterModalState() override;
virtual void LeaveModalState() override;
// Outer windows only.

View File

@ -913,8 +913,12 @@ class nsPIDOMWindowOuter : public mozIDOMWindowProxy {
/**
* Callback for notifying a window about a modal dialog being
* opened/closed with the window as a parent.
*
* If any script can run between the enter and leave modal states, and the
* window isn't top, the LeaveModalState() should be called on the window
* returned by EnterModalState().
*/
virtual void EnterModalState() = 0;
virtual nsPIDOMWindowOuter* EnterModalState() = 0;
virtual void LeaveModalState() = 0;
virtual bool CanClose() = 0;

View File

@ -45,6 +45,7 @@ support-files =
file_window_print_delayed_during_load.html
file_window_print_sandboxed_iframe.html
file_window_print_another_iframe_and_remove.html
file_window_print_iframe_remove_on_afterprint.html
file_coop_header2.html
file_coop_header2.html^headers^

View File

@ -212,6 +212,48 @@ add_task(async function test_window_print_coop_site() {
}
});
add_task(async function test_window_print_iframe_remove_on_afterprint() {
await SpecialPowers.pushPrefEnv({
set: [["print.tab_modal.enabled", true]],
});
ok(
!document.querySelector(".printPreviewBrowser"),
"There shouldn't be any print preview browser"
);
await BrowserTestUtils.withNewTab(
`${TEST_PATH}file_window_print_iframe_remove_on_afterprint.html`,
async function(browser) {
info("Waiting for dialog");
await BrowserTestUtils.waitForCondition(
() => !!document.querySelector(".printPreviewBrowser")
);
let modalBefore = await SpecialPowers.spawn(browser, [], () => {
return content.windowUtils.isInModalState();
});
ok(modalBefore, "The tab should be in modal state");
// Clear the dialog.
gBrowser.getTabDialogBox(browser).abortAllDialogs();
let [modalAfter, hasIframe] = await SpecialPowers.spawn(
browser,
[],
() => {
return [
content.windowUtils.isInModalState(),
!!content.document.querySelector("iframe"),
];
}
);
ok(!modalAfter, "Should've cleared the modal state properly");
ok(!hasIframe, "Iframe should've been removed from the DOM");
}
);
});
// FIXME(emilio): This test doesn't use window.print(), why is it on this file?
add_task(async function test_focused_browsing_context() {
await SpecialPowers.pushPrefEnv({

View File

@ -0,0 +1,30 @@
<!doctype html>
<script>
function closePrint() {
document.documentElement.removeChild(this.__container__);
}
function setPrint() {
this.contentWindow.__container__ = this;
this.contentWindow.onbeforeunload = closePrint;
this.contentWindow.onafterprint = closePrint;
this.contentWindow.print();
}
function printPage(content) {
var frame = document.createElement("iframe");
frame.onload = setPrint;
frame.style.position = "fixed";
frame.style.right = "0";
frame.style.bottom = "0";
frame.style.width = "0";
frame.style.height = "0";
frame.style.border = "0";
frame.srcdoc = content;
document.documentElement.appendChild(frame);
}
onload = function() {
printPage("Something");
}
</script>