Bug 1763672 - Introduce targetTopLevelLinkClicksToBlank on BrowsingContext. r=nika

This property defaults to false. When set to true, user-initiated link clicks in
the top-level BrowsingContext will default target to _blank. This is similar to what
we do for App Tabs, but is slightly more aggressive in that in will also occur for
same-origin navigations.

Differential Revision: https://phabricator.services.mozilla.com/D143374
This commit is contained in:
Mike Conley 2022-04-13 16:00:27 +00:00
Родитель 75bf0090fe
Коммит b3f08bf9bd
9 изменённых файлов: 355 добавлений и 13 удалений

Просмотреть файл

@ -3076,6 +3076,10 @@ mozilla::dom::TouchEventsOverride BrowsingContext::TouchEventsOverride() const {
return mozilla::dom::TouchEventsOverride::None;
}
bool BrowsingContext::TargetTopLevelLinkClicksToBlank() const {
return Top()->GetTargetTopLevelLinkClicksToBlankInternal();
}
// We map `watchedByDevTools` WebIDL attribute to `watchedByDevToolsInternal`
// BC field. And we map it to the top level BrowsingContext.
bool BrowsingContext::WatchedByDevTools() {
@ -3475,6 +3479,13 @@ void BrowsingContext::DidSet(FieldIndex<IDX_HasSessionHistory>,
}
}
bool BrowsingContext::CanSet(
FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>,
const bool& aTargetTopLevelLinkClicksToBlankInternal,
ContentParent* aSource) {
return XRE_IsParentProcess() && !aSource && IsTop();
}
bool BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
ContentParent* aSource) {
// We should only be able to set this for toplevel contexts which don't have

Просмотреть файл

@ -159,6 +159,7 @@ enum class ExplicitActiveStatus : uint8_t {
FIELD(AllowContentRetargetingOnChildren, bool) \
FIELD(ForceEnableTrackingProtection, bool) \
FIELD(UseGlobalHistory, bool) \
FIELD(TargetTopLevelLinkClicksToBlankInternal, bool) \
FIELD(FullscreenAllowedByOwner, bool) \
/* \
* "is popup" in the spec. \
@ -557,6 +558,7 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
void SetWatchedByDevTools(bool aWatchedByDevTools, ErrorResult& aRv);
dom::TouchEventsOverride TouchEventsOverride() const;
bool TargetTopLevelLinkClicksToBlank() const;
bool FullscreenAllowed() const;
@ -1139,6 +1141,10 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
bool CanSet(FieldIndex<IDX_UseGlobalHistory>, const bool& aUseGlobalHistory,
ContentParent* aSource);
bool CanSet(FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>,
const bool& aTargetTopLevelLinkClicksToBlankInternal,
ContentParent* aSource);
void DidSet(FieldIndex<IDX_HasSessionHistory>, bool aOldValue);
bool CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,

Просмотреть файл

@ -317,6 +317,8 @@ void CanonicalBrowsingContext::ReplacedBy(
// about:blank documents created in it.
txn.SetSandboxFlags(GetSandboxFlags());
txn.SetInitialSandboxFlags(GetSandboxFlags());
txn.SetTargetTopLevelLinkClicksToBlankInternal(
TargetTopLevelLinkClicksToBlank());
if (aNewContext->EverAttached()) {
MOZ_ALWAYS_SUCCEEDS(txn.Commit(aNewContext));
} else {
@ -2678,6 +2680,12 @@ void CanonicalBrowsingContext::SetTouchEventsOverride(
SetTouchEventsOverrideInternal(aOverride, aRv);
}
void CanonicalBrowsingContext::SetTargetTopLevelLinkClicksToBlank(
bool aTargetTopLevelLinkClicksToBlank, ErrorResult& aRv) {
SetTargetTopLevelLinkClicksToBlankInternal(aTargetTopLevelLinkClicksToBlank,
aRv);
}
void CanonicalBrowsingContext::AddPageAwakeRequest() {
MOZ_ASSERT(IsTop());
auto count = GetPageAwakeRequestCount();

Просмотреть файл

@ -322,6 +322,8 @@ class CanonicalBrowsingContext final : public BrowsingContext {
}
void SetTouchEventsOverride(dom::TouchEventsOverride, ErrorResult& aRv);
void SetTargetTopLevelLinkClicksToBlank(bool aTargetTopLevelLinkClicksToBlank,
ErrorResult& aRv);
bool IsReplaced() const { return mIsReplaced; }

Просмотреть файл

@ -12860,7 +12860,8 @@ nsresult nsDocShell::OnLinkClick(
bool noOpenerImplied = false;
nsAutoString target(aTargetSpec);
if (ShouldOpenInBlankTarget(aTargetSpec, aURI, aContent)) {
if (aFileName.IsVoid() &&
ShouldOpenInBlankTarget(aTargetSpec, aURI, aContent, aIsUserTriggered)) {
target = u"_blank";
if (!aTargetSpec.Equals(target)) {
noOpenerImplied = true;
@ -12886,17 +12887,9 @@ nsresult nsDocShell::OnLinkClick(
}
bool nsDocShell::ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
nsIURI* aLinkURI,
nsIContent* aContent) {
// Don't modify non-default targets.
if (!aOriginalTarget.IsEmpty()) {
return false;
}
// Only check targets that are in extension panels or app tabs.
// (isAppTab will be false for app tab subframes).
nsString mmGroup = mBrowsingContext->Top()->GetMessageManagerGroup();
if (!mmGroup.EqualsLiteral("webext-browsers") && !mIsAppTab) {
nsIURI* aLinkURI, nsIContent* aContent,
bool aIsUserTriggered) {
if (net::SchemeIsJavascript(aLinkURI)) {
return false;
}
@ -12909,6 +12902,29 @@ bool nsDocShell::ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
return false;
}
// The targetTopLevelLinkClicksToBlank property on BrowsingContext allows
// privileged code to change the default targeting behaviour. In particular,
// if a user-initiated link click for the (or targetting the) top-level frame
// is detected, we default the target to "_blank" to give it a new
// top-level BrowsingContext.
if (mBrowsingContext->TargetTopLevelLinkClicksToBlank() && aIsUserTriggered &&
((aOriginalTarget.IsEmpty() && mBrowsingContext->IsTop()) ||
aOriginalTarget == u"_top"_ns)) {
return true;
}
// Don't modify non-default targets.
if (!aOriginalTarget.IsEmpty()) {
return false;
}
// Only check targets that are in extension panels or app tabs.
// (isAppTab will be false for app tab subframes).
nsString mmGroup = mBrowsingContext->Top()->GetMessageManagerGroup();
if (!mmGroup.EqualsLiteral("webext-browsers") && !mIsAppTab) {
return false;
}
nsCOMPtr<nsIURI> docURI = aContent->OwnerDoc()->GetDocumentURIObject();
if (!docURI) {
return false;

Просмотреть файл

@ -1114,7 +1114,8 @@ class nsDocShell final : public nsDocLoader,
bool NoopenerForceEnabled();
bool ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
nsIURI* aLinkURI, nsIContent* aContent);
nsIURI* aLinkURI, nsIContent* aContent,
bool aIsUserTriggered);
void RecordSingleChannelId(bool aStartRequest, nsIRequest* aRequest);

Просмотреть файл

@ -158,6 +158,7 @@ https_first_disabled = true
support-files =
file_backforward_restore_scroll.html
file_backforward_restore_scroll.html^headers^
[browser_targetTopLevelLinkClicksToBlank.js]
[browser_title_in_session_history.js]
skip-if = !sessionHistoryInParent
[browser_uriFixupIntegration.js]

Просмотреть файл

@ -0,0 +1,285 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test exercises the behaviour where user-initiated link clicks on
* the top-level document result in pageloads in a _blank target in a new
* browser window.
*/
const TEST_PAGE = "https://example.com/browser/";
const TEST_PAGE_2 = "https://example.com/browser/components/";
const TEST_IFRAME_PAGE =
getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"https://example.com"
) + "dummy_iframe_page.html";
// There is an <a> element with this href=".." in the TEST_PAGE
// that we will click, which should take us up a level.
const LINK_URL = "https://example.com/";
/**
* Test that a user-initiated link click results in targeting to a new
* <browser> element, and that this properly sets the referrer on the newly
* loaded document.
*/
add_task(async function target_to_new_blank_browser() {
let win = await BrowserTestUtils.openNewBrowserWindow();
let originalTab = win.gBrowser.selectedTab;
let originalBrowser = originalTab.linkedBrowser;
BrowserTestUtils.loadURI(originalBrowser, TEST_PAGE);
await BrowserTestUtils.browserLoaded(originalBrowser, false, TEST_PAGE);
// Now set the targetTopLevelLinkClicksToBlank property to true, since it
// defaults to false.
originalBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true;
let newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL);
await SpecialPowers.spawn(originalBrowser, [], async () => {
let anchor = content.document.querySelector(`a[href=".."]`);
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
anchor.click();
} finally {
userInput.destruct();
}
});
let newTab = await newTabPromise;
let newBrowser = newTab.linkedBrowser;
Assert.ok(
originalBrowser !== newBrowser,
"A new browser should have been created."
);
await SpecialPowers.spawn(newBrowser, [TEST_PAGE], async referrer => {
Assert.equal(
content.document.referrer,
referrer,
"Should have gotten the right referrer set"
);
});
await BrowserTestUtils.switchTab(win.gBrowser, originalTab);
BrowserTestUtils.removeTab(newTab);
// Now do the same thing with a subframe targeting "_top". This should also
// get redirected to "_blank".
BrowserTestUtils.loadURI(originalBrowser, TEST_IFRAME_PAGE);
await BrowserTestUtils.browserLoaded(
originalBrowser,
false,
TEST_IFRAME_PAGE
);
newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL);
let frameBC1 = originalBrowser.browsingContext.children[0];
Assert.ok(frameBC1, "Should have found a subframe BrowsingContext");
await SpecialPowers.spawn(frameBC1, [LINK_URL], async linkUrl => {
let anchor = content.document.createElement("a");
anchor.setAttribute("href", linkUrl);
anchor.setAttribute("target", "_top");
content.document.body.appendChild(anchor);
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
anchor.click();
} finally {
userInput.destruct();
}
});
newTab = await newTabPromise;
newBrowser = newTab.linkedBrowser;
Assert.ok(
originalBrowser !== newBrowser,
"A new browser should have been created."
);
await SpecialPowers.spawn(
newBrowser,
[frameBC1.currentURI.spec],
async referrer => {
Assert.equal(
content.document.referrer,
referrer,
"Should have gotten the right referrer set"
);
}
);
await BrowserTestUtils.switchTab(win.gBrowser, originalTab);
BrowserTestUtils.removeTab(newTab);
await BrowserTestUtils.closeWindow(win);
});
/**
* Test that we don't target to _blank loads caused by:
* 1. POST requests
* 2. Any load that isn't "normal" (in the nsIDocShell.LOAD_CMD_NORMAL sense)
* 3. Any loads that are caused by location.replace
* 4. Any loads that were caused by setting location.href
* 5. Link clicks fired without user interaction.
*/
add_task(async function skip_blank_target_for_some_loads() {
let win = await BrowserTestUtils.openNewBrowserWindow();
let currentBrowser = win.gBrowser.selectedBrowser;
BrowserTestUtils.loadURI(currentBrowser, TEST_PAGE);
await BrowserTestUtils.browserLoaded(currentBrowser, false, TEST_PAGE);
// Now set the targetTopLevelLinkClicksToBlank property to true, since it
// defaults to false.
currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true;
let ensureSingleBrowser = () => {
Assert.equal(
win.gBrowser.browsers.length,
1,
"There should only be 1 browser."
);
Assert.ok(
currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank,
"Should still be targeting top-level clicks to _blank"
);
};
// First we'll test a POST request
let sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
TEST_PAGE
);
await SpecialPowers.spawn(currentBrowser, [], async () => {
let doc = content.document;
let form = doc.createElement("form");
form.setAttribute("method", "post");
doc.body.appendChild(form);
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
form.submit();
} finally {
userInput.destruct();
}
});
await sameBrowserLoad;
ensureSingleBrowser();
// Next, we'll try a non-normal load - specifically, we'll try a reload.
// Since we've got a page loaded via a POST request, an attempt to reload
// will cause the "repost" dialog to appear, so we temporarily allow the
// repost to go through with the always_accept testing pref.
await SpecialPowers.pushPrefEnv({
set: [["dom.confirm_repost.testing.always_accept", true]],
});
sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
TEST_PAGE
);
await SpecialPowers.spawn(currentBrowser, [], async () => {
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
content.location.reload();
} finally {
userInput.destruct();
}
});
await sameBrowserLoad;
ensureSingleBrowser();
await SpecialPowers.popPrefEnv();
// Next, we'll try a location.replace
sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
TEST_PAGE_2
);
await SpecialPowers.spawn(currentBrowser, [TEST_PAGE_2], async page2 => {
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
content.location.replace(page2);
} finally {
userInput.destruct();
}
});
await sameBrowserLoad;
ensureSingleBrowser();
// Finally we'll try setting location.href
sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
TEST_PAGE
);
await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async page1 => {
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
content.location.href = page1;
} finally {
userInput.destruct();
}
});
await sameBrowserLoad;
ensureSingleBrowser();
// Now that we're back at TEST_PAGE, let's try a scripted link click. This
// shouldn't target to _blank.
sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
LINK_URL
);
await SpecialPowers.spawn(currentBrowser, [], async () => {
let anchor = content.document.querySelector(`a[href=".."]`);
anchor.click();
});
await sameBrowserLoad;
ensureSingleBrowser();
// A javascript:void(0); link should also not target to _blank.
sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
TEST_PAGE
);
await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => {
let anchor = content.document.querySelector(`a[href=".."]`);
anchor.href = "javascript:void(0);";
anchor.addEventListener("click", e => {
content.location.href = newPageURL;
});
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
anchor.click();
} finally {
userInput.destruct();
}
});
await sameBrowserLoad;
ensureSingleBrowser();
// Let's also try a non-void javascript: location.
sameBrowserLoad = BrowserTestUtils.browserLoaded(
currentBrowser,
false,
TEST_PAGE
);
await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => {
let anchor = content.document.querySelector(`a[href=".."]`);
anchor.href = `javascript:"string-to-navigate-to"`;
anchor.addEventListener("click", e => {
content.location.href = newPageURL;
});
let userInput = content.windowUtils.setHandlingUserInput(true);
try {
anchor.click();
} finally {
userInput.destruct();
}
});
await sameBrowserLoad;
ensureSingleBrowser();
await BrowserTestUtils.closeWindow(win);
});

Просмотреть файл

@ -204,6 +204,12 @@ interface BrowsingContext {
*/
readonly attribute TouchEventsOverride touchEventsOverride;
/**
* Returns true if the top-level BrowsingContext has been configured to
* default-target user-initiated link clicks to _blank.
*/
readonly attribute boolean targetTopLevelLinkClicksToBlank;
/**
* Partially determines whether script execution is allowed in this
* BrowsingContext. Script execution will be permitted only if this
@ -323,6 +329,12 @@ interface CanonicalBrowsingContext : BrowsingContext {
*/
[SetterThrows] inherit attribute TouchEventsOverride touchEventsOverride;
/**
* Set to true to configure the top-level BrowsingContext to default-target
* user-initiated link clicks to _blank.
*/
[SetterThrows] inherit attribute boolean targetTopLevelLinkClicksToBlank;
/**
* Set the cross-group opener of this BrowsingContext. This is used to
* retarget the download dialog to an opener window, and close this