Bug 1959610 - Align CloseOuter with #script-closable session history check. r=smaug
Since https://github.com/whatwg/html/pull/6315, script-closable checks for a history length of one, instead of the session history containing only one document. This commit also adds a wpt to test that windows opened by links and forms count as `created by a script`. Differential Revision: https://phabricator.services.mozilla.com/D245035
This commit is contained in:
@@ -224,9 +224,6 @@ struct EmbedderColorSchemes {
|
||||
* This is only ever set to true on the top BC, so consumers need to get \
|
||||
* the value from the top BC! */ \
|
||||
FIELD(HasSessionHistory, bool) \
|
||||
/* Tracks if this context is the only top-level document in the session \
|
||||
* history of the context. */ \
|
||||
FIELD(IsSingleToplevelInHistory, bool) \
|
||||
FIELD(UseErrorPages, bool) \
|
||||
FIELD(PlatformOverride, nsString) \
|
||||
/* Specifies if this BC has loaded documents besides the initial \
|
||||
|
||||
@@ -1301,7 +1301,6 @@ void CanonicalBrowsingContext::ReplaceActiveSessionHistoryEntry(
|
||||
nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory());
|
||||
if (shistory) {
|
||||
shistory->NotifyOnHistoryReplaceEntry();
|
||||
shistory->UpdateRootBrowsingContextState();
|
||||
}
|
||||
|
||||
ResetSHEntryHasUserInteractionCache();
|
||||
|
||||
@@ -287,8 +287,6 @@ interface nsISHistory: nsISupports
|
||||
[noscript] void AddChildSHEntryHelper(in nsISHEntry aCloneRef, in nsISHEntry aNewEntry,
|
||||
in BrowsingContext aRootBC, in boolean aCloneChildren);
|
||||
|
||||
[noscript, notxpcom] boolean isEmptyOrHasEntriesForSingleTopLevelPage();
|
||||
|
||||
/**
|
||||
* Determine if we can navigate back in history from the entry at aIndex
|
||||
* to an entry that has user interaction.
|
||||
|
||||
@@ -784,17 +784,6 @@ void nsSHistory::HandleEntriesToSwapInDocShell(
|
||||
}
|
||||
}
|
||||
|
||||
void nsSHistory::UpdateRootBrowsingContextState(BrowsingContext* aRootBC) {
|
||||
if (aRootBC && aRootBC->EverAttached()) {
|
||||
bool sameDocument = IsEmptyOrHasEntriesForSingleTopLevelPage();
|
||||
if (sameDocument != aRootBC->GetIsSingleToplevelInHistory()) {
|
||||
// If the browsing context is discarded then its session history is
|
||||
// invalid and will go away.
|
||||
Unused << aRootBC->SetIsSingleToplevelInHistory(sameDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsSHistory::AddToRootSessionHistory(bool aCloneChildren, nsISHEntry* aOSHE,
|
||||
BrowsingContext* aRootBC,
|
||||
@@ -883,7 +872,6 @@ nsSHistory::AddEntry(nsISHEntry* aSHEntry, bool aPersist) {
|
||||
NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
|
||||
aSHEntry->SetPersist(aPersist);
|
||||
mEntries[mIndex] = aSHEntry;
|
||||
UpdateRootBrowsingContextState();
|
||||
return NS_OK;
|
||||
}
|
||||
}
|
||||
@@ -915,8 +903,6 @@ nsSHistory::AddEntry(nsISHEntry* aSHEntry, bool aPersist) {
|
||||
PurgeHistory(Length() - gHistoryMaxSize);
|
||||
}
|
||||
|
||||
UpdateRootBrowsingContextState();
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@@ -1150,8 +1136,6 @@ nsSHistory::PurgeHistory(int32_t aNumEntries) {
|
||||
rootBC->GetDocShell()->HistoryPurged(aNumEntries);
|
||||
}
|
||||
|
||||
UpdateRootBrowsingContextState(rootBC);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@@ -1213,8 +1197,6 @@ nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry) {
|
||||
aReplaceEntry->SetPersist(true);
|
||||
mEntries[aIndex] = aReplaceEntry;
|
||||
|
||||
UpdateRootBrowsingContextState();
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@@ -2026,8 +2008,6 @@ void nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex,
|
||||
}
|
||||
--index;
|
||||
}
|
||||
|
||||
UpdateRootBrowsingContextState();
|
||||
}
|
||||
|
||||
void nsSHistory::RemoveFrameEntries(nsISHEntry* aEntry) {
|
||||
@@ -2417,25 +2397,6 @@ nsSHistory::CreateEntry(nsISHEntry** aEntry) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP_(bool)
|
||||
nsSHistory::IsEmptyOrHasEntriesForSingleTopLevelPage() {
|
||||
if (mEntries.IsEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
nsISHEntry* entry = mEntries[0];
|
||||
size_t length = mEntries.Length();
|
||||
for (size_t i = 1; i < length; ++i) {
|
||||
bool sharesDocument = false;
|
||||
mEntries[i]->SharesDocumentWith(entry, &sharesDocument);
|
||||
if (!sharesDocument) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void CollectEntries(
|
||||
nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable,
|
||||
SessionHistoryEntry* aEntry) {
|
||||
|
||||
@@ -189,7 +189,6 @@ class nsSHistory : public mozilla::LinkedListElement<nsSHistory>,
|
||||
uint64_t newID = aRootBC ? aRootBC->Id() : 0;
|
||||
if (mRootBC != newID) {
|
||||
mRootBC = newID;
|
||||
UpdateRootBrowsingContextState(aRootBC);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +199,6 @@ class nsSHistory : public mozilla::LinkedListElement<nsSHistory>,
|
||||
return mRequestedIndex == -1 ? mIndex : mRequestedIndex;
|
||||
}
|
||||
|
||||
// Update the root browsing context state when adding, removing or
|
||||
// replacing entries.
|
||||
void UpdateRootBrowsingContextState() {
|
||||
RefPtr<mozilla::dom::BrowsingContext> rootBC(GetBrowsingContext());
|
||||
UpdateRootBrowsingContextState(rootBC);
|
||||
}
|
||||
|
||||
void GetEpoch(uint64_t& aEpoch,
|
||||
mozilla::Maybe<mozilla::dom::ContentParentId>& aId) const {
|
||||
aEpoch = mEpoch;
|
||||
@@ -228,9 +220,6 @@ class nsSHistory : public mozilla::LinkedListElement<nsSHistory>,
|
||||
private:
|
||||
friend class nsSHistoryObserver;
|
||||
|
||||
void UpdateRootBrowsingContextState(
|
||||
mozilla::dom::BrowsingContext* aBrowsingContext);
|
||||
|
||||
bool LoadDifferingEntries(nsISHEntry* aPrevEntry, nsISHEntry* aNextEntry,
|
||||
mozilla::dom::BrowsingContext* aParent,
|
||||
long aLoadType,
|
||||
|
||||
@@ -5920,9 +5920,12 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
|
||||
nsresult rv = mDoc->GetURL(url);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
|
||||
RefPtr<ChildSHistory> csh =
|
||||
nsDocShell::Cast(mDocShell)->GetSessionHistory();
|
||||
|
||||
if (!StringBeginsWith(url, u"about:neterror"_ns) &&
|
||||
!mBrowsingContext->GetTopLevelCreatedByWebContent() &&
|
||||
!aTrustedCaller && !IsOnlyTopLevelDocumentInSHistory()) {
|
||||
!aTrustedCaller && csh && csh->Count() > 1) {
|
||||
bool allowClose =
|
||||
mAllowScriptsToClose ||
|
||||
Preferences::GetBool("dom.allow_scripts_to_close_windows", true);
|
||||
@@ -5965,23 +5968,6 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
|
||||
FinalClose();
|
||||
}
|
||||
|
||||
bool nsGlobalWindowOuter::IsOnlyTopLevelDocumentInSHistory() {
|
||||
NS_ENSURE_TRUE(mDocShell && mBrowsingContext, false);
|
||||
// Disabled since IsFrame() is buggy in Fission
|
||||
// MOZ_ASSERT(mBrowsingContext->IsTop());
|
||||
|
||||
if (mozilla::SessionHistoryInParent()) {
|
||||
return mBrowsingContext->GetIsSingleToplevelInHistory();
|
||||
}
|
||||
|
||||
RefPtr<ChildSHistory> csh = nsDocShell::Cast(mDocShell)->GetSessionHistory();
|
||||
if (csh && csh->LegacySHistory()) {
|
||||
return csh->LegacySHistory()->IsEmptyOrHasEntriesForSingleTopLevelPage();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
nsresult nsGlobalWindowOuter::Close() {
|
||||
CloseOuter(/* aTrustedCaller = */ true);
|
||||
return NS_OK;
|
||||
|
||||
@@ -965,8 +965,6 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget,
|
||||
|
||||
void MaybeAllowStorageForOpenedWindow(nsIURI* aURI);
|
||||
|
||||
bool IsOnlyTopLevelDocumentInSHistory();
|
||||
|
||||
void MaybeResetWindowName(Document* aNewDocument);
|
||||
|
||||
public:
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<!doctype html>
|
||||
<script>
|
||||
let chan = new BroadcastChannel("close_noopener_beforeunload" + location.search);
|
||||
onload = function() { window.close(); };
|
||||
onbeforeunload = function() {
|
||||
chan.postMessage({ name: "beforeunload", history: history.length });
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<!doctype html>
|
||||
<script>
|
||||
onload = function() {
|
||||
setTimeout(() => window.location = "close_noopener_beforeunload-1.html" + location.search, 0);
|
||||
}
|
||||
</script>
|
||||
@@ -4,21 +4,31 @@
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
async_test(t => {
|
||||
window.open("close_noopener_beforeunload-2.html?2", "", "noopener");
|
||||
let chan = new BroadcastChannel("close_noopener_beforeunload?2");
|
||||
chan.onmessage = t.step_func_done(function(event) {
|
||||
assert_equals(event.data.name, "beforeunload", "correct message received");
|
||||
assert_equals(event.data.history, 2, "session history has multiple entries");
|
||||
function waitForMessage(channel, name) {
|
||||
return new Promise(resolve => {
|
||||
function listener({ data }) {
|
||||
if (data.name === name) {
|
||||
channel.removeEventListener("message", listener);
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
channel.addEventListener("message", listener);
|
||||
});
|
||||
}
|
||||
|
||||
async_test(t => {
|
||||
let chan = new BroadcastChannel("close_noopener_beforeunload2");
|
||||
waitForMessage(chan, "beforeunload").then(t.step_func_done(data => {
|
||||
assert_equals(data.history, 2, "session history has multiple entries");
|
||||
}));
|
||||
window.open("self-close.html?navs=1&channel=close_noopener_beforeunload2", "", "noopener");
|
||||
}, "closing noopener window with 2 entries");
|
||||
|
||||
async_test(t => {
|
||||
window.open("close_noopener_beforeunload-1.html?1", "", "noopener");
|
||||
let chan = new BroadcastChannel("close_noopener_beforeunload?1");
|
||||
chan.onmessage = t.step_func_done(function(event) {
|
||||
assert_equals(event.data.name, "beforeunload", "correct message received");
|
||||
assert_equals(event.data.history, 1, "session history has a single entry");
|
||||
});
|
||||
let chan = new BroadcastChannel("close_noopener_beforeunload1");
|
||||
waitForMessage(chan, "beforeunload").then(t.step_func_done(data => {
|
||||
assert_equals(data.history, 1, "session history has a single entry");
|
||||
}));
|
||||
window.open("self-close.html?navs=0&channel=close_noopener_beforeunload1", "", "noopener");
|
||||
}, "closing noopener window with 1 entry");
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<!doctype html>
|
||||
<title>Test whether a window is script-closable</title>
|
||||
<link rel="help" href="https://html.spec.whatwg.org/#script-closable"/>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<div id="log"></div>
|
||||
|
||||
<script>
|
||||
let uid = 0;
|
||||
|
||||
async function withChannel(createWindow) {
|
||||
const channel_name = `channel_${++uid}`;
|
||||
const channel = new BroadcastChannel(channel_name);
|
||||
|
||||
const closedPromise = new Promise(resolve => {
|
||||
channel.addEventListener("message", ({data}) => {
|
||||
if (data.name === 'load') {
|
||||
assert_true(true, 'window was loaded');
|
||||
} else {
|
||||
assert_not_equals(data.history, 1, "If script-closable, then not due to history length");
|
||||
resolve(data.closed);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await createWindow(channel_name);
|
||||
return closedPromise;
|
||||
}
|
||||
|
||||
const Actions = {
|
||||
async form(noopener) {
|
||||
return withChannel((channel_name) => {
|
||||
const form = document.createElement("form");
|
||||
form.action = "self-close.html";
|
||||
form.target = '_blank';
|
||||
if (noopener) {
|
||||
form.rel = "noopener";
|
||||
}
|
||||
// ?navs=1
|
||||
const inp1 = document.createElement("input");
|
||||
inp1.name = "navs";
|
||||
inp1.value = "1";
|
||||
form.appendChild(inp1);
|
||||
// ?channel=channel_name
|
||||
const inp2 = document.createElement("input");
|
||||
inp2.name = "channel";
|
||||
inp2.value = channel_name;
|
||||
form.appendChild(inp2);
|
||||
document.body.appendChild(form);
|
||||
|
||||
form.submit();
|
||||
form.remove();
|
||||
});
|
||||
},
|
||||
async link(noopener) {
|
||||
return withChannel((channel_name) => {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = `self-close.html?navs=1&channel=${channel_name}`;
|
||||
anchor.target = '_blank';
|
||||
if (noopener) {
|
||||
anchor.rel = "noopener";
|
||||
}
|
||||
document.body.appendChild(anchor);
|
||||
|
||||
anchor.click();
|
||||
|
||||
anchor.remove();
|
||||
});
|
||||
},
|
||||
async script(noopener) {
|
||||
return withChannel((channel_name) => {
|
||||
const url = `self-close.html?navs=1&channel=${channel_name}`;
|
||||
window.open(url, "_blank", noopener ? 'noopener' : '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const action of [Actions.form, Actions.link, Actions.script]) {
|
||||
for (const noopener of [false, true])
|
||||
promise_test(async () => {
|
||||
const closed = await action(noopener);
|
||||
assert_true(closed, "window closed");
|
||||
}, `Window created by ${action.name} with ${noopener ? 'noopener' : 'opener'} is script-closable`);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<p>self-close.html?navs=n&channel=name will navigate n times, then close and notify the channel.</p>
|
||||
|
||||
<script>
|
||||
window.onload = setTimeout(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let n = parseInt(urlParams.get('navs')) || 0;
|
||||
|
||||
const channel = new BroadcastChannel(urlParams.get('channel'));
|
||||
|
||||
channel.postMessage({ name: 'load', href: window.location.href });
|
||||
|
||||
if (n > 0) {
|
||||
urlParams.set('navs', n-1);
|
||||
window.location.href = `${window.location.pathname}?${urlParams.toString()}#${n}`;
|
||||
} else {
|
||||
window.onbeforeunload = () => {
|
||||
channel.postMessage({ name: 'beforeunload', history: history.length, closed: true });
|
||||
}
|
||||
window.close();
|
||||
if (!window.closed) {
|
||||
channel.postMessage({ name: 'close failed', history: history.length, closed: false });
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
</script>
|
||||
Reference in New Issue
Block a user