зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
75bf0090fe
Коммит
b3f08bf9bd
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче