Commit b2c90ffa authored by Jeremy Roman's avatar Jeremy Roman Committed by Commit Bot

Cancel rather than block navigations which violate portals 1P rules.

Since this can lead to a WebContents with no committed entries, deal
with that case in the browser-side Activate logic, by rejecting
activations which occur before we can say with certainty that we
will load a page (i.e. navigation commit).

Bug: 1046121,1034740,942198
Change-Id: Icd219dc0bb1e4a9ab6fdc433f8644c14e69964e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2015768Reviewed-by: default avatarKen Buchanan <kenrb@chromium.org>
Reviewed-by: default avatarAlex Moshchuk <alexmos@chromium.org>
Reviewed-by: default avatarLucas Gadani <lfg@chromium.org>
Commit-Queue: Jeremy Roman <jbroman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#736369}
parent 770a49b3
...@@ -1280,6 +1280,8 @@ void RenderFrameHostImpl::OnPortalActivated( ...@@ -1280,6 +1280,8 @@ void RenderFrameHostImpl::OnPortalActivated(
break; break;
case blink::mojom::PortalActivateResult:: case blink::mojom::PortalActivateResult::
kRejectedDueToPredecessorNavigation: kRejectedDueToPredecessorNavigation:
case blink::mojom::PortalActivateResult::
kRejectedDueToPortalNotReady:
case blink::mojom::PortalActivateResult::kDisconnected: case blink::mojom::PortalActivateResult::kDisconnected:
case blink::mojom::PortalActivateResult::kAbortedDueToBug: case blink::mojom::PortalActivateResult::kAbortedDueToBug:
// The renderer is misbehaving. // The renderer is misbehaving.
......
...@@ -320,6 +320,15 @@ void Portal::Activate(blink::TransferableMessage data, ...@@ -320,6 +320,15 @@ void Portal::Activate(blink::TransferableMessage data,
<< "The binding should have been closed when the portal's outer " << "The binding should have been closed when the portal's outer "
"FrameTreeNode was deleted due to swap out."; "FrameTreeNode was deleted due to swap out.";
// If no navigation has yet committed in the portal, it cannot be activated as
// this would lead to an empty tab contents (without even an about:blank).
DCHECK(portal_contents_);
if (portal_contents_->GetController().GetLastCommittedEntryIndex() < 0) {
std::move(callback).Run(
blink::mojom::PortalActivateResult::kRejectedDueToPortalNotReady);
return;
}
// If a navigation in the main frame is occurring, stop it if possible and // If a navigation in the main frame is occurring, stop it if possible and
// reject the activation if it's too late. There are a few cases here: // reject the activation if it's too late. There are a few cases here:
// - a different RenderFrameHost has been assigned to the FrameTreeNode // - a different RenderFrameHost has been assigned to the FrameTreeNode
......
...@@ -75,7 +75,7 @@ PortalNavigationThrottle::WillStartOrRedirectRequest() { ...@@ -75,7 +75,7 @@ PortalNavigationThrottle::WillStartOrRedirectRequest() {
base::StringPrintf("Navigating a portal to cross-origin content (from " base::StringPrintf("Navigating a portal to cross-origin content (from "
"%s) is not currently permitted and was blocked.", "%s) is not currently permitted and was blocked.",
origin.Serialize().c_str())); origin.Serialize().c_str()));
return BLOCK_REQUEST; return CANCEL;
} }
} // namespace content } // namespace content
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
#include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test.h"
#include "content/public/test/test_navigation_observer.h" #include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h" #include "content/shell/browser/shell.h"
#include "content/test/portal/portal_activated_observer.h"
#include "content/test/portal/portal_created_observer.h" #include "content/test/portal/portal_created_observer.h"
#include "net/base/escape.h" #include "net/base/escape.h"
#include "net/dns/mock_host_resolver.h" #include "net/dns/mock_host_resolver.h"
...@@ -70,7 +71,7 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest { ...@@ -70,7 +71,7 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest {
Portal* InsertAndWaitForPortal(const GURL& url, Portal* InsertAndWaitForPortal(const GURL& url,
bool expected_to_succeed = true) { bool expected_to_succeed = true) {
TestNavigationObserver navigation_observer(url); TestNavigationObserver navigation_observer(/*web_contents=*/nullptr, 1);
navigation_observer.StartWatchingNewWebContents(); navigation_observer.StartWatchingNewWebContents();
PortalCreatedObserver portal_created_observer(GetMainFrame()); PortalCreatedObserver portal_created_observer(GetMainFrame());
EXPECT_TRUE(ExecJs( EXPECT_TRUE(ExecJs(
...@@ -84,6 +85,8 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest { ...@@ -84,6 +85,8 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest {
navigation_observer.Wait(); navigation_observer.Wait();
EXPECT_EQ(navigation_observer.last_navigation_succeeded(), EXPECT_EQ(navigation_observer.last_navigation_succeeded(),
expected_to_succeed); expected_to_succeed);
if (expected_to_succeed)
EXPECT_EQ(navigation_observer.last_navigation_url(), url);
return portal; return portal;
} }
...@@ -97,7 +100,7 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest { ...@@ -97,7 +100,7 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest {
base::StringPrintf( base::StringPrintf(
"document.querySelector('body > portal').src = '%s';", "document.querySelector('body > portal').src = '%s';",
url.spec().c_str()))); url.spec().c_str())));
navigation_observer.WaitForNavigationFinished(); navigation_observer.Wait();
return navigation_observer.last_navigation_succeeded(); return navigation_observer.last_navigation_succeeded();
} }
...@@ -109,7 +112,7 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest { ...@@ -109,7 +112,7 @@ class PortalNavigationThrottleBrowserTest : public ContentBrowserTest {
EXPECT_TRUE(ExecJs( EXPECT_TRUE(ExecJs(
portal->GetPortalContents(), portal->GetPortalContents(),
base::StringPrintf("location.href = '%s';", url.spec().c_str()))); base::StringPrintf("location.href = '%s';", url.spec().c_str())));
navigation_observer.WaitForNavigationFinished(); navigation_observer.Wait();
return navigation_observer.last_navigation_succeeded(); return navigation_observer.last_navigation_succeeded();
} }
...@@ -190,14 +193,14 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, ...@@ -190,14 +193,14 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
ASSERT_TRUE(NavigateToURL( ASSERT_TRUE(NavigateToURL(
GetWebContents(), GetWebContents(),
embedded_test_server()->GetURL("portal.test", "/title1.html"))); embedded_test_server()->GetURL("portal.test", "/title1.html")));
Portal* portal = InsertAndWaitForPortal( GURL referrer_url =
embedded_test_server()->GetURL("portal.test", "/title2.html")); embedded_test_server()->GetURL("portal.test", "/title2.html");
GURL destination_url = GURL destination_url =
embedded_test_server()->GetURL("not.portal.test", "/notreached"); embedded_test_server()->GetURL("not.portal.test", "/notreached");
Portal* portal = InsertAndWaitForPortal(referrer_url);
EXPECT_FALSE(NavigatePortalViaSrcAttribute(portal, destination_url, 1)); EXPECT_FALSE(NavigatePortalViaSrcAttribute(portal, destination_url, 1));
EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), referrer_url);
destination_url);
} }
IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
...@@ -205,14 +208,14 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, ...@@ -205,14 +208,14 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
ASSERT_TRUE(NavigateToURL( ASSERT_TRUE(NavigateToURL(
GetWebContents(), GetWebContents(),
embedded_test_server()->GetURL("portal.test", "/title1.html"))); embedded_test_server()->GetURL("portal.test", "/title1.html")));
Portal* portal = InsertAndWaitForPortal( GURL referrer_url =
embedded_test_server()->GetURL("portal.test", "/title2.html")); embedded_test_server()->GetURL("portal.test", "/title2.html");
GURL destination_url = GURL destination_url =
embedded_test_server()->GetURL("not.portal.test", "/notreached"); embedded_test_server()->GetURL("not.portal.test", "/notreached");
Portal* portal = InsertAndWaitForPortal(referrer_url);
EXPECT_FALSE(NavigatePortalViaLocationHref(portal, destination_url, 1)); EXPECT_FALSE(NavigatePortalViaLocationHref(portal, destination_url, 1));
EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), referrer_url);
destination_url);
} }
IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
...@@ -220,16 +223,16 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, ...@@ -220,16 +223,16 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
ASSERT_TRUE(NavigateToURL( ASSERT_TRUE(NavigateToURL(
GetWebContents(), GetWebContents(),
embedded_test_server()->GetURL("portal.test", "/title1.html"))); embedded_test_server()->GetURL("portal.test", "/title1.html")));
Portal* portal = InsertAndWaitForPortal( GURL referrer_url =
embedded_test_server()->GetURL("portal.test", "/title2.html")); embedded_test_server()->GetURL("portal.test", "/title2.html");
GURL destination_url = GURL destination_url =
embedded_test_server()->GetURL("not.portal.test", "/notreached"); embedded_test_server()->GetURL("not.portal.test", "/notreached");
GURL redirect_url = GetServerRedirectURL(embedded_test_server(), GURL redirect_url = GetServerRedirectURL(embedded_test_server(),
"portal.test", destination_url); "portal.test", destination_url);
Portal* portal = InsertAndWaitForPortal(referrer_url);
EXPECT_FALSE(NavigatePortalViaSrcAttribute(portal, redirect_url, 1)); EXPECT_FALSE(NavigatePortalViaSrcAttribute(portal, redirect_url, 1));
EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), referrer_url);
destination_url);
} }
IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
...@@ -237,15 +240,41 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, ...@@ -237,15 +240,41 @@ IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
ASSERT_TRUE(NavigateToURL( ASSERT_TRUE(NavigateToURL(
GetWebContents(), GetWebContents(),
embedded_test_server()->GetURL("portal.test", "/title1.html"))); embedded_test_server()->GetURL("portal.test", "/title1.html")));
Portal* portal = InsertAndWaitForPortal(
embedded_test_server()->GetURL("portal.test", "/title2.html"));
GURL referrer_url =
embedded_test_server()->GetURL("portal.test", "/title2.html");
GURL destination_url = GURL destination_url =
embedded_test_server()->GetURL("portal.test", "/notreached"); embedded_test_server()->GetURL("portal.test", "/notreached");
GURL redirect_url = GetServerRedirectURL(embedded_test_server(), GURL redirect_url = GetServerRedirectURL(embedded_test_server(),
"not.portal.test", destination_url); "not.portal.test", destination_url);
Portal* portal = InsertAndWaitForPortal(referrer_url);
EXPECT_FALSE(NavigatePortalViaSrcAttribute(portal, redirect_url, 1)); EXPECT_FALSE(NavigatePortalViaSrcAttribute(portal, redirect_url, 1));
EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), redirect_url); EXPECT_EQ(portal->GetPortalContents()->GetLastCommittedURL(), referrer_url);
}
IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
ActivateAfterCanceledInitialNavigation) {
ASSERT_TRUE(NavigateToURL(
GetWebContents(),
embedded_test_server()->GetURL("portal.test", "/title1.html")));
GURL referrer_url =
embedded_test_server()->GetURL("portal.test", "/title2.html");
GURL destination_url =
embedded_test_server()->GetURL("not.portal.test", "/notreached");
Portal* portal =
InsertAndWaitForPortal(destination_url, /*expected_to_succeed=*/false);
EXPECT_NE(portal, nullptr);
std::string result =
EvalJs(GetMainFrame(),
"document.querySelector('body > portal').activate()"
".then(() => 'activated', e => e.message)")
.ExtractString();
EXPECT_THAT(result, ::testing::HasSubstr("not yet ready or was blocked"));
EXPECT_EQ(GetWebContents()->GetLastCommittedURL(),
embedded_test_server()->GetURL("portal.test", "/title1.html"));
} }
IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest, IN_PROC_BROWSER_TEST_F(PortalNavigationThrottleBrowserTest,
......
...@@ -19,6 +19,10 @@ enum PortalActivateResult { ...@@ -19,6 +19,10 @@ enum PortalActivateResult {
// The predecessor was adopted by its successor. // The predecessor was adopted by its successor.
kPredecessorWasAdopted, kPredecessorWasAdopted,
// The activation could not proceed because the portal had not yet begun
// to load a document, or its load was canceled.
kRejectedDueToPortalNotReady,
// The activation could not proceed since the predecessor main frame was // The activation could not proceed since the predecessor main frame was
// navigating and the navigation could not be cancelled. // navigating and the navigation could not be cancelled.
kRejectedDueToPredecessorNavigation, kRejectedDueToPredecessorNavigation,
......
...@@ -71,6 +71,23 @@ ScriptPromise PortalContents::Activate(ScriptState* script_state, ...@@ -71,6 +71,23 @@ ScriptPromise PortalContents::Activate(ScriptState* script_state,
void PortalContents::OnActivateResponse( void PortalContents::OnActivateResponse(
mojom::blink::PortalActivateResult result) { mojom::blink::PortalActivateResult result) {
auto reject = [&](DOMExceptionCode code, const char* message) {
if (GetDocument().IsContextDestroyed())
return;
ScriptState* script_state = activate_resolver_->GetScriptState();
ScriptState::Scope scope(script_state);
// TODO(jbroman): It's slightly unfortunate to hard-code the string
// HTMLPortalElement here. Ideally this would be threaded through from
// there and carried with the ScriptPromiseResolver. See
// https://crbug.com/991544.
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext,
"HTMLPortalElement", "activate");
exception_state.ThrowDOMException(code, message);
activate_resolver_->Reject(exception_state);
};
bool should_destroy_contents = false; bool should_destroy_contents = false;
switch (result) { switch (result) {
case mojom::blink::PortalActivateResult::kPredecessorWasAdopted: case mojom::blink::PortalActivateResult::kPredecessorWasAdopted:
...@@ -82,24 +99,14 @@ void PortalContents::OnActivateResponse( ...@@ -82,24 +99,14 @@ void PortalContents::OnActivateResponse(
break; break;
case mojom::blink::PortalActivateResult:: case mojom::blink::PortalActivateResult::
kRejectedDueToPredecessorNavigation: { kRejectedDueToPredecessorNavigation:
if (!GetDocument().IsContextDestroyed()) { reject(DOMExceptionCode::kInvalidStateError,
ScriptState* script_state = activate_resolver_->GetScriptState(); "A top-level navigation is in progress.");
ScriptState::Scope scope(script_state); break;
// TODO(jbroman): It's slightly unfortunate to hard-code the string case mojom::blink::PortalActivateResult::kRejectedDueToPortalNotReady:
// HTMLPortalElement here. Ideally this would be threaded through from reject(DOMExceptionCode::kInvalidStateError,
// there and carried with the ScriptPromiseResolver. See "The portal was not yet ready or was blocked.");
// https://crbug.com/991544.
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext,
"HTMLPortalElement", "activate");
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"A top-level navigation is in progress.");
activate_resolver_->Reject(exception_state);
}
break; break;
}
case mojom::blink::PortalActivateResult::kDisconnected: case mojom::blink::PortalActivateResult::kDisconnected:
// Only called when |remote_portal_| is disconnected. This usually happens // Only called when |remote_portal_| is disconnected. This usually happens
// when the browser/test runner is being shut down. // when the browser/test runner is being shut down.
......
...@@ -17,6 +17,7 @@ promise_test(async () => { ...@@ -17,6 +17,7 @@ promise_test(async () => {
const portal = w.document.createElement('portal'); const portal = w.document.createElement('portal');
portal.src = new URL('resources/simple-portal.html', location.href); portal.src = new URL('resources/simple-portal.html', location.href);
w.document.body.appendChild(portal); w.document.body.appendChild(portal);
await nextEvent(portal, 'load');
const pagehideFired = nextEvent(w, 'pagehide'); const pagehideFired = nextEvent(w, 'pagehide');
const unloadFired = nextEvent(w, 'unload'); const unloadFired = nextEvent(w, 'unload');
await portal.activate(); await portal.activate();
......
...@@ -5,10 +5,11 @@ function nextEvent(target, type) { ...@@ -5,10 +5,11 @@ function nextEvent(target, type) {
return new Promise((resolve, reject) => target.addEventListener(type, e => resolve(e), {once: true})); return new Promise((resolve, reject) => target.addEventListener(type, e => resolve(e), {once: true}));
} }
onload = function() { onload = async function() {
const portal = document.createElement('portal'); const portal = document.createElement('portal');
portal.src = new URL('simple-portal.html', location.href); portal.src = new URL('simple-portal.html', location.href);
document.body.appendChild(portal); document.body.appendChild(portal);
await nextEvent(portal, 'load');
let firedEvents = []; let firedEvents = [];
for (let type of ['pagehide', 'unload']) { for (let type of ['pagehide', 'unload']) {
...@@ -19,6 +20,6 @@ onload = function() { ...@@ -19,6 +20,6 @@ onload = function() {
} }
portal.activate(); portal.activate();
} };
</script> </script>
</body> </body>
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
// This test passes if it does not crash. // This test passes if it does not crash.
// This does surprise the test runner by closing the main web contents. // This does surprise the test runner by closing the main web contents.
var portal = document.createElement("portal"); var portal = document.createElement("portal");
portal.src = '/external/wpt/portals/resources/simple-portal.html';
document.body.appendChild(portal); document.body.appendChild(portal);
portal.activate(); portal.onload = () => portal.activate();
</script> </script>
</body> </body>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment