Skip to content

Commit

Permalink
Window Placement: Prototype Fullscreen Capability Delegation - Part 2
Browse files Browse the repository at this point in the history
This completes the prototype started in https://crrev.com/c/3575071

Support Fullscreen Capability Delegation when a flag is enabled:
  --enable-blink-features=CapabilityDelegationFullscreenRequest
Frames can delegate via messaging another frame on activation:
- w.postMessage(m, { targetOrigin: o, delegate: "fullscreen" });
Another frame can use the delegated capability on message receipt:
- window.onmessage = () => { e.requestFullscreen(); }

Activate a browser-side FullscreenRequestToken in:
- RFHI::PostMessageEvent for RemoteFrameHost's RouteMessageEvent
- new ReceivedDelegatedCapability for LocalFrameHost local messages

Focus windows that had delegation to request fullscreen, so:
- user can press [Esc] to exit fullscreen
- another focused window doesn't z-order above/below fullscreen

Add automated tests supporting feature flag and local/remote usage

(cherry picked from commit dbf43d9)

Bug: 1293083
Test: automated; Fullscreen Capability Delegation WAI with flag enabled
Change-Id: Ie46e627cd8df033016f16afbfda4075da81517b2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3642842
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Reviewed-by: Mustaq Ahmed <mustaq@chromium.org>
Commit-Queue: Mike Wasserman <msw@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1005525}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3658340
Auto-Submit: Mike Wasserman <msw@chromium.org>
Commit-Queue: Charlie Reis <creis@chromium.org>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/5060@{#187}
Cr-Branched-From: b83393d-refs/heads/main@{#1002911}
  • Loading branch information
Mike Wasserman authored and Chromium LUCI CQ committed May 23, 2022
1 parent 208206b commit e14a425
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/switches.h"
Expand Down Expand Up @@ -677,6 +678,134 @@ IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
ASSERT_FALSE(IsWindowFullscreenForTabOrPending());
}

// Tests FullscreenController support for fullscreen capability delegation.
// https://wicg.github.io/capability-delegation/spec.html
// See related wpt/fullscreen/api/delegate-request.https.sub.tentative.html
// TODO(crbug.com/1326575): Test opener->popup etc. messaging; add WPT coverage.
class FullscreenCapabilityDelegationFullscreenControllerInteractiveTest
: public FullscreenControllerInteractiveTest,
public testing::WithParamInterface<bool> {
public:
FullscreenCapabilityDelegationFullscreenControllerInteractiveTest() = default;

void SetUpCommandLine(base::CommandLine* command_line) override {
FullscreenControllerInteractiveTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(GetParam()
? switches::kEnableBlinkFeatures
: switches::kDisableBlinkFeatures,
"CapabilityDelegationFullscreenRequest");
}

void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
embedded_test_server()->ServeFilesFromSourceDirectory(
"chrome/test/data/capability_delegation");
}

// Returns a popup with `url`, opened via JS from `browser`'s active tab.
content::WebContents* OpenPopup(Browser* browser, const GURL& url) {
const std::string script = content::JsReplace(
"window.open($1, '', 'width=500,height=500');", url.spec());
content::ExecuteScriptAsync(
browser->tab_strip_model()->GetActiveWebContents(), script);
Browser* popup = ui_test_utils::WaitForBrowserToOpen();
EXPECT_NE(popup, browser);
content::WebContents* popup_contents =
popup->tab_strip_model()->GetActiveWebContents();
EXPECT_TRUE(WaitForRenderFrameReady(popup_contents->GetMainFrame()));
EXPECT_TRUE(content::WaitForLoadStop(popup_contents));
return popup_contents;
}

// Run `script` on `initiator` with `options`, and compares `expected_result`
// with the script result and the `target` browser's fullscreen state.
void ExecScriptAndCheckFullscreen(content::WebContents* initiator,
Browser* target,
const std::string& script,
int options,
bool expected_result) {
FullscreenNotificationObserver fullscreen_observer(target);
EXPECT_EQ(expected_result, EvalJs(initiator, script, options));
if (expected_result)
fullscreen_observer.Wait();
EXPECT_EQ(expected_result, target->window()->IsFullscreen());
}
};

IN_PROC_BROWSER_TEST_P(
FullscreenCapabilityDelegationFullscreenControllerInteractiveTest,
CapabilityDelegationSameOriginPopup) {
EXPECT_TRUE(embedded_test_server()->Start());

// Navigate to a page that requests fullscreen and replies on message receipt.
const GURL receiver_url = embedded_test_server()->GetURL(
"a.com", "/fullscreen_request_delegation_receiver.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), receiver_url));

// Open a same-origin popup that delegates fullscreen and reports the result.
const GURL initiator_url = embedded_test_server()->GetURL(
"a.com", "/fullscreen_request_delegation_initiator.html");
content::WebContents* popup = OpenPopup(browser(), initiator_url);

const std::string targetOrigin =
embedded_test_server()->GetOrigin("a.com").Serialize();
const std::string script_without_delegation = content::JsReplace(
"delegateCapability(window.opener, $1, '')", targetOrigin);
const std::string script_with_delegation = content::JsReplace(
"delegateCapability(window.opener, $1, 'fullscreen')", targetOrigin);

// Fullscreen is only granted with user activation and explicit delegation.
ExecScriptAndCheckFullscreen(popup, browser(), script_without_delegation,
content::EXECUTE_SCRIPT_NO_USER_GESTURE, false);
ExecScriptAndCheckFullscreen(popup, browser(), script_with_delegation,
content::EXECUTE_SCRIPT_NO_USER_GESTURE, false);
ExecScriptAndCheckFullscreen(popup, browser(), script_without_delegation,
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS, false);
ExecScriptAndCheckFullscreen(popup, browser(), script_with_delegation,
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS,
GetParam());
}

IN_PROC_BROWSER_TEST_P(
FullscreenCapabilityDelegationFullscreenControllerInteractiveTest,
CapabilityDelegationCrossOriginPopup) {
EXPECT_TRUE(embedded_test_server()->Start());

// Navigate to a page that requests fullscreen and replies on message receipt.
const GURL receiver_url = embedded_test_server()->GetURL(
"a.com", "/fullscreen_request_delegation_receiver.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), receiver_url));

// Open a cross-origin popup that delegates fullscreen and reports the result.
const GURL initiator_url = embedded_test_server()->GetURL(
"b.com", "/fullscreen_request_delegation_initiator.html");
content::WebContents* popup = OpenPopup(browser(), initiator_url);

const std::string targetOrigin =
embedded_test_server()->GetOrigin("a.com").Serialize();
const std::string script_without_delegation = content::JsReplace(
"delegateCapability(window.opener, $1, '')", targetOrigin);
const std::string script_with_delegation = content::JsReplace(
"delegateCapability(window.opener, $1, 'fullscreen')", targetOrigin);

// Fullscreen is only granted with user activation and explicit delegation.
ExecScriptAndCheckFullscreen(popup, browser(), script_without_delegation,
content::EXECUTE_SCRIPT_NO_USER_GESTURE, false);
ExecScriptAndCheckFullscreen(popup, browser(), script_with_delegation,
content::EXECUTE_SCRIPT_NO_USER_GESTURE, false);
ExecScriptAndCheckFullscreen(popup, browser(), script_without_delegation,
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS, false);
ExecScriptAndCheckFullscreen(popup, browser(), script_with_delegation,
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS,
GetParam());
}

INSTANTIATE_TEST_SUITE_P(
,
FullscreenCapabilityDelegationFullscreenControllerInteractiveTest,
::testing::Bool());

// Tests FullscreenController support of Multi-Screen Window Placement features.
// Sites with the Window Placement permission can request fullscreen on a
// specific screen, move fullscreen windows to different displays, and more.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<!--
Copyright 2022 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fullscreen request delegation initiator</title>
</head>
<script>
// https://wicg.github.io/capability-delegation/spec.html
async function delegateCapability(target, origin, capability) {
const promise = new Promise(resolve => {
window.addEventListener("message", e => resolve(e.data), /*once=*/true);
});
target.postMessage("", { targetOrigin: origin, delegate: capability });
return promise;
}
</script>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<!--
Copyright 2022 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fullscreen request delegation receiver</title>
</head>
<script>
// https://wicg.github.io/capability-delegation/spec.html
async function requestFullscreenAndReport(event) {
document.documentElement.requestFullscreen().finally(() => {
event.source.postMessage(!!document.fullscreenElement, "*");
});
}

window.addEventListener("message", requestFullscreenAndReport);
</script>
</html>
42 changes: 31 additions & 11 deletions content/browser/renderer_host/render_frame_host_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6176,25 +6176,31 @@ void RenderFrameHostImpl::SetIsXrOverlaySetup() {
void RenderFrameHostImpl::EnterFullscreen(
blink::mojom::FullscreenOptionsPtr options,
EnterFullscreenCallback callback) {
// Consume the user activation when entering fullscreen mode in the browser
// side when the renderer is compromised and the fullscreen request is denied.
// Fullscreen can only be triggered by: a user activation, a user-generated
// screen orientation change, or another feature-specific transient allowance.
const bool had_fullscreen_token = fullscreen_request_token_.IsActive();

// Entering fullscreen requires a transient user activation, a fullscreen
// capability delegation token, a user-generated screen orientation change, or
// another feature-specific transient allowance.
// CanEnterFullscreenWithoutUserActivation is only ever true in tests, to
// allow fullscreen when mocking screen orientation changes.
// TODO(lanwei): Investigate whether we can terminate the renderer when the
// user activation has already been consumed.
if (!delegate_->HasSeenRecentScreenOrientationChange() &&
!WindowPlacementAllowsFullscreen() && !HasSeenRecentXrOverlaySetup() &&
!GetContentClient()
->browser()
->CanEnterFullscreenWithoutUserActivation()) {
bool is_consumed = frame_tree_node_->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kConsumeTransientActivation,
blink::mojom::UserActivationNotificationType::kNone);
if (!is_consumed) {
// Consume any transient user activation and delegated fullscreen token.
// Reject requests made without transient user activation or a token.
// TODO(lanwei): Investigate whether we can terminate the renderer when
// transient user activation and the delegated token are both inactive.
const bool consumed_activation =
frame_tree_node_->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kConsumeTransientActivation,
blink::mojom::UserActivationNotificationType::kNone);
const bool consumed_token = fullscreen_request_token_.ConsumeIfActive();
if (!consumed_activation && !consumed_token) {
DLOG(ERROR) << "Cannot enter fullscreen because there is no transient "
<< "user activation, orientation change, or XR overlay.";
<< "user activation, orientation change, XR overlay, nor "
<< "capability delegation.";
std::move(callback).Run(/*granted=*/false);
return;
}
Expand Down Expand Up @@ -6250,6 +6256,9 @@ void RenderFrameHostImpl::EnterFullscreen(
notified_instances.insert(parent_site_instance);
}

// Focus the window if another frame may have delegated the capability.
if (had_fullscreen_token && !GetView()->HasFocus())
GetView()->Focus();
delegate_->EnterFullscreenMode(this, *options);
delegate_->FullscreenStateChanged(this, /*is_fullscreen=*/true,
std::move(options));
Expand Down Expand Up @@ -7288,6 +7297,14 @@ void RenderFrameHostImpl::DidChangeSrcDoc(
child->SetSrcdocValue(srcdoc_value);
}

void RenderFrameHostImpl::ReceivedDelegatedCapability(
blink::mojom::DelegatedCapability delegated_capability) {
if (delegated_capability ==
blink::mojom::DelegatedCapability::kFullscreenRequest) {
fullscreen_request_token_.Activate();
}
}

// TODO(ahemery): Move checks to mojo bad message reporting.
void RenderFrameHostImpl::BeginNavigation(
blink::mojom::CommonNavigationParamsPtr common_params,
Expand Down Expand Up @@ -11885,6 +11902,9 @@ void RenderFrameHostImpl::PostMessageEvent(
blink::TransferableMessage message) {
DCHECK(is_render_frame_created());

if (message.delegated_capability != blink::mojom::DelegatedCapability::kNone)
ReceivedDelegatedCapability(message.delegated_capability);

GetAssociatedLocalFrame()->PostMessageEvent(
source_token, source_origin, target_origin, std::move(message));
}
Expand Down
6 changes: 6 additions & 0 deletions content/browser/renderer_host/render_frame_host_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
#include "services/network/public/mojom/url_loader_network_service_observer.mojom-forward.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/frame/frame_owner_element_type.h"
#include "third_party/blink/public/common/frame/fullscreen_request_token.h"
#include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
#include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
Expand Down Expand Up @@ -2142,6 +2143,8 @@ class CONTENT_EXPORT RenderFrameHostImpl
void FrameSizeChanged(const gfx::Size& frame_size) override;
void DidChangeSrcDoc(const blink::FrameToken& child_frame_token,
const std::string& srcdoc_value) override;
void ReceivedDelegatedCapability(
blink::mojom::DelegatedCapability delegated_capability) override;

// blink::mojom::BackForwardCacheControllerHost:
void EvictFromBackForwardCache(blink::mojom::RendererEvictionReason) override;
Expand Down Expand Up @@ -4224,6 +4227,9 @@ class CONTENT_EXPORT RenderFrameHostImpl
// Manages a transient affordance for this frame or subframes to open a popup.
TransientAllowPopup transient_allow_popup_;

// Manages a transient affordance for this frame to request fullscreen.
blink::FullscreenRequestToken fullscreen_request_token_;

// Used to avoid sending AXTreeData to the renderer if the renderer has not
// been told root ID yet. See UpdateAXTreeData() for more details.
bool needs_ax_root_id_ = true;
Expand Down
1 change: 1 addition & 0 deletions content/browser/web_contents/web_contents_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,7 @@ void WebContentsImpl::EnterFullscreenMode(
OPTIONAL_TRACE_EVENT0("content", "WebContentsImpl::EnterFullscreenMode");
DCHECK(CanEnterFullscreenMode(requesting_frame, options));
DCHECK(requesting_frame->IsActive());
DCHECK(ContainsOrIsFocusedWebContents());

if (delegate_) {
delegate_->EnterFullscreenModeForTab(requesting_frame, options);
Expand Down
7 changes: 7 additions & 0 deletions third_party/blink/public/mojom/frame/frame.mojom
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import "third_party/blink/public/mojom/frame/user_activation_update_types.mojom"
import "third_party/blink/public/mojom/frame/viewport_intersection_state.mojom";
import "third_party/blink/public/mojom/input/focus_type.mojom";
import "third_party/blink/public/mojom/input/scroll_direction.mojom";
import "third_party/blink/public/mojom/messaging/delegated_capability.mojom";
import "third_party/blink/public/mojom/navigation/navigation_api_history_entry_arrays.mojom";
import "third_party/blink/public/mojom/navigation/navigation_policy.mojom";
import "third_party/blink/public/mojom/loader/referrer.mojom";
Expand Down Expand Up @@ -571,6 +572,12 @@ interface LocalFrameHost {
// sends the new value to the browser. Also sent when a subframe's content
// frame is changed.
DidChangeSrcDoc(blink.mojom.FrameToken child_frame_token, string srcdoc_value);

// Notifies the browser that this frame received a message from a local frame
// with a delegated capability (https://wicg.github.io/capability-delegation).
// `delegated_capability` is the capability delegated via postMessage().
// RemoteFrameHost messages already signal the browser via RouteMessageEvent.
ReceivedDelegatedCapability(DelegatedCapability delegated_capability);
};

// Implemented in Blink, this interface defines frame-specific methods that will
Expand Down
10 changes: 9 additions & 1 deletion third_party/blink/renderer/core/frame/local_dom_window.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,15 @@ void LocalDOMWindow::SchedulePostMessage(PostedMessage* posted_message) {
source->GetStorageKey(), UkmSourceID(),
GetStorageKey(), UkmRecorder());

// Notify the host if the message contained a delegated capability. That state
// should be tracked by the browser, and messages from remote hosts already
// signal the browser via RemoteFrameHost's RouteMessageEvent.
if (posted_message->delegated_capability !=
mojom::blink::DelegatedCapability::kNone) {
GetFrame()->GetLocalFrameHostRemote().ReceivedDelegatedCapability(
posted_message->delegated_capability);
}

// Convert the posted message to a MessageEvent so it can be unpacked for
// local dispatch.
MessageEvent* event = MessageEvent::Create(
Expand Down Expand Up @@ -1185,7 +1194,6 @@ void LocalDOMWindow::DispatchMessageEventWithOriginCheck(
mojom::blink::DelegatedCapability::kFullscreenRequest) {
UseCounter::Count(this,
WebFeature::kCapabilityDelegationOfFullscreenRequest);
// TODO(crbug.com/1293083): Activate a corresponding token in the browser.
fullscreen_request_token_.Activate();
}

Expand Down
4 changes: 2 additions & 2 deletions third_party/blink/renderer/core/fullscreen/fullscreen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -709,9 +709,9 @@ ScriptPromise Fullscreen::RequestFullscreen(Element& pending,
LocalFrame& frame = *window.GetFrame();
frame.GetChromeClient().EnterFullscreen(frame, options, request_type);

// After the first fullscreen request, the user activation should be
// consumed, and the following fullscreen requests should receive an error.
if (!for_cross_process_descendant) {
// Consume any transient user activation and delegated fullscreen token.
// AllowedToRequestFullscreen() enforces algorithm requirements earlier.
LocalFrame::ConsumeTransientUserActivation(&frame);
window.ConsumeFullscreenRequestToken();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,7 @@ void FakeLocalFrameHost::DidChangeSrcDoc(
const blink::FrameToken& child_frame_token,
const WTF::String& srcdoc_value) {}

void FakeLocalFrameHost::ReceivedDelegatedCapability(
blink::mojom::DelegatedCapability delegated_capability) {}

} // namespace blink
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ class FakeLocalFrameHost : public mojom::blink::LocalFrameHost {
blink::mojom::PreferredColorScheme preferred_color_scheme) override;
void DidChangeSrcDoc(const blink::FrameToken& child_frame_token,
const WTF::String& srcdoc_value) override;
void ReceivedDelegatedCapability(
blink::mojom::DelegatedCapability delegated_capability) override;

private:
void BindFrameHostReceiver(mojo::ScopedInterfaceEndpointHandle handle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
Verifies that element.requestFullscreen() call from a cross-origin subframe without user
activation works if and only if the top frame has user activation and it delegates the capability
to the subframe.

https://wicg.github.io/capability-delegation/spec.html

See wpt/html/user-activation/propagation*.html for child->parent user activation visibility tests.
TODO: Check same-origin iframes, sibling frames, and popup<->opener delegation.
</div>

<iframe allow="fullscreen" width="300px" height="50px"
Expand Down

0 comments on commit e14a425

Please sign in to comment.