blob: b554a4c9021446c2a3c663428612b754e6ab8214 [file] [log] [blame]
// Copyright 2018 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.
#include "third_party/blink/renderer/core/html/portal/html_portal_element.h"
#include <utility>
#include "third_party/blink/public/mojom/loader/referrer.mojom-blink.h"
#include "third_party/blink/public/mojom/web_feature/web_feature.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/js_event_handler_for_content_attribute.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/post_message_helper.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_portal_activate_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_window_post_message_options.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/events/message_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/remote_frame.h"
#include "third_party/blink/renderer/core/html/html_document.h"
#include "third_party/blink/renderer/core/html/html_unknown_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html/portal/document_portals.h"
#include "third_party/blink/renderer/core/html/portal/portal_activation_delegate.h"
#include "third_party/blink/renderer/core/html/portal/portal_contents.h"
#include "third_party/blink/renderer/core/html/portal/portal_post_message_helper.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/inspector/thread_debugger.h"
#include "third_party/blink/renderer/core/layout/layout_iframe.h"
#include "third_party/blink/renderer/core/messaging/message_port.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/public/scheduling_policy.h"
#include "third_party/blink/renderer/platform/weborigin/referrer.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/weborigin/security_policy.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
HTMLPortalElement::HTMLPortalElement(
Document& document,
const PortalToken* portal_token,
mojo::PendingAssociatedRemote<mojom::blink::Portal> remote_portal,
mojo::PendingAssociatedReceiver<mojom::blink::PortalClient>
portal_client_receiver)
: HTMLFrameOwnerElement(html_names::kPortalTag, document),
feature_handle_for_scheduler_(
document.GetExecutionContext()->GetScheduler()->RegisterFeature(
SchedulingPolicy::Feature::kPortal,
{SchedulingPolicy::DisableBackForwardCache()})) {
if (remote_portal) {
DCHECK(portal_token);
was_just_adopted_ = true;
DCHECK(CanHaveGuestContents())
<< "<portal> element was created with an existing contents but is not "
"permitted to have one";
portal_ = MakeGarbageCollected<PortalContents>(
*this, *portal_token, std::move(remote_portal),
std::move(portal_client_receiver));
}
UseCounter::Count(document, WebFeature::kHTMLPortalElement);
}
HTMLPortalElement::~HTMLPortalElement() {}
void HTMLPortalElement::Trace(Visitor* visitor) const {
HTMLFrameOwnerElement::Trace(visitor);
visitor->Trace(portal_);
}
void HTMLPortalElement::ConsumePortal() {
if (portal_)
portal_->Destroy();
DCHECK(!portal_);
}
void HTMLPortalElement::ExpireAdoptionLifetime() {
was_just_adopted_ = false;
// After dispatching the portalactivate event, we check to see if we need to
// cleanup the portal hosting the predecessor. If the portal was created,
// but wasn't inserted or activated, we destroy it.
if (!CanHaveGuestContents())
ConsumePortal();
}
void HTMLPortalElement::PortalContentsWillBeDestroyed(PortalContents* portal) {
DCHECK_EQ(portal_, portal);
portal_ = nullptr;
}
bool HTMLPortalElement::IsCurrentlyWithinFrameLimit() const {
auto* frame = GetDocument().GetFrame();
if (!frame)
return false;
auto* page = frame->GetPage();
if (!page)
return false;
return page->SubframeCount() < Page::MaxNumberOfFrames();
}
String HTMLPortalElement::PreActivateChecksCommon() {
if (!portal_)
return "The HTMLPortalElement is not associated with a portal context.";
if (DocumentPortals::From(GetDocument()).IsPortalInDocumentActivating())
return "Another portal in this document is activating.";
if (GetDocument().GetPage()->InsidePortal())
return "Cannot activate a portal that is inside another portal.";
if (GetDocument().BeforeUnloadStarted()) {
return "Cannot activate portal while document is in beforeunload or has "
"started unloading.";
}
return String();
}
void HTMLPortalElement::ActivateDefault() {
ExecutionContext* context = GetExecutionContext();
if (!CheckPortalsEnabledOrWarn() || !context)
return;
String pre_activate_error = PreActivateChecksCommon();
if (pre_activate_error) {
context->AddConsoleMessage(mojom::blink::ConsoleMessageSource::kRendering,
mojom::blink::ConsoleMessageLevel::kWarning,
pre_activate_error);
return;
}
// Quickly encode undefined without actually invoking script.
BlinkTransferableMessage data;
data.message = SerializedScriptValue::UndefinedValue();
data.message->UnregisterMemoryAllocatedWithCurrentScriptContext();
data.sender_origin =
GetExecutionContext()->GetSecurityOrigin()->IsolatedCopy();
if (ThreadDebugger* debugger =
ThreadDebugger::From(V8PerIsolateData::MainThreadIsolate())) {
data.sender_stack_trace_id =
debugger->StoreCurrentStackTrace("activate (implicit)");
}
PortalContents* portal = std::exchange(portal_, nullptr);
portal->Activate(std::move(data),
PortalActivationDelegate::ForConsole(context));
}
bool HTMLPortalElement::CheckWithinFrameLimitOrWarn() const {
if (IsCurrentlyWithinFrameLimit())
return true;
Document& document = GetDocument();
document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kRendering,
mojom::blink::ConsoleMessageLevel::kWarning,
"An operation was prevented due to too many frames and portals present "
"on the page."));
return false;
}
bool HTMLPortalElement::CheckPortalsEnabledOrWarn() const {
ExecutionContext* context = GetExecutionContext();
if (RuntimeEnabledFeatures::PortalsEnabled(context))
return true;
context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kRendering,
mojom::blink::ConsoleMessageLevel::kWarning,
"An operation was prevented because a <portal> was moved to a document "
"where it is not enabled. See "
"https://www.chromium.org/blink/origin-trials/portals."));
return false;
}
bool HTMLPortalElement::CheckPortalsEnabledOrThrow(
ExceptionState& exception_state) const {
if (RuntimeEnabledFeatures::PortalsEnabled(GetExecutionContext()))
return true;
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"An operation was prevented because a <portal> was moved to a document "
"where it is not enabled. See "
"https://www.chromium.org/blink/origin-trials/portals.");
return false;
}
// https://wicg.github.io/portals/#htmlportalelement-may-have-a-guest-browsing-context
HTMLPortalElement::GuestContentsEligibility
HTMLPortalElement::GetGuestContentsEligibility() const {
// Non-HTML documents aren't eligible at all.
if (!IsA<HTMLDocument>(GetDocument()))
return GuestContentsEligibility::kIneligible;
LocalFrame* frame = GetDocument().GetFrame();
const bool is_connected = frame && isConnected();
if (!is_connected && !was_just_adopted_)
return GuestContentsEligibility::kIneligible;
const bool is_top_level = frame && frame->IsMainFrame();
if (!is_top_level)
return GuestContentsEligibility::kNotTopLevel;
// TODO(crbug.com/1051639): We need to find a long term solution to when/how
// portals should work in sandboxed documents.
if (frame->DomWindow()->GetSandboxFlags() !=
network::mojom::blink::WebSandboxFlags::kNone) {
return GuestContentsEligibility::kSandboxed;
}
if (!GetDocument().Url().ProtocolIsInHTTPFamily())
return GuestContentsEligibility::kNotHTTPFamily;
return GuestContentsEligibility::kEligible;
}
void HTMLPortalElement::Navigate() {
if (!CheckPortalsEnabledOrWarn())
return;
if (!CheckWithinFrameLimitOrWarn())
return;
auto url = GetNonEmptyURLAttribute(html_names::kSrcAttr);
if (url.PotentiallyDanglingMarkup())
return;
if (portal_)
portal_->Navigate(url, ReferrerPolicyAttribute());
}
namespace {
BlinkTransferableMessage ActivateDataAsMessage(
ScriptState* script_state,
PortalActivateOptions* options,
ExceptionState& exception_state) {
v8::Isolate* isolate = script_state->GetIsolate();
Transferables transferables;
if (options->hasTransfer()) {
if (!SerializedScriptValue::ExtractTransferables(
script_state->GetIsolate(), options->transfer(), transferables,
exception_state))
return {};
}
SerializedScriptValue::SerializeOptions serialize_options;
serialize_options.transferables = &transferables;
v8::Local<v8::Value> data = options->hasData()
? options->data().V8Value()
: v8::Null(isolate).As<v8::Value>();
BlinkTransferableMessage msg;
msg.message = SerializedScriptValue::Serialize(
isolate, data, serialize_options, exception_state);
if (!msg.message)
return {};
msg.message->UnregisterMemoryAllocatedWithCurrentScriptContext();
auto* execution_context = ExecutionContext::From(script_state);
msg.ports = MessagePort::DisentanglePorts(
execution_context, transferables.message_ports, exception_state);
if (exception_state.HadException())
return {};
msg.sender_origin = execution_context->GetSecurityOrigin()->IsolatedCopy();
// msg.user_activation is left out; we will probably handle user activation
// explicitly for activate data.
// TODO(crbug.com/936184): Answer this for good.
if (ThreadDebugger* debugger = ThreadDebugger::From(isolate))
msg.sender_stack_trace_id = debugger->StoreCurrentStackTrace("activate");
if (msg.message->IsLockedToAgentCluster()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kDataCloneError,
"Cannot send agent cluster-locked data (e.g. SharedArrayBuffer) "
"through portal activation.");
return {};
}
return msg;
}
} // namespace
ScriptPromise HTMLPortalElement::activate(ScriptState* script_state,
PortalActivateOptions* options,
ExceptionState& exception_state) {
if (!CheckPortalsEnabledOrThrow(exception_state))
return ScriptPromise();
String pre_activate_error = PreActivateChecksCommon();
if (pre_activate_error) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
pre_activate_error);
return ScriptPromise();
}
BlinkTransferableMessage data =
ActivateDataAsMessage(script_state, options, exception_state);
if (exception_state.HadException())
return ScriptPromise();
PortalContents* portal = std::exchange(portal_, nullptr);
ScriptPromiseResolver* resolver =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
portal->Activate(std::move(data), PortalActivationDelegate::ForPromise(
resolver, exception_state));
return promise;
}
void HTMLPortalElement::postMessage(ScriptState* script_state,
const ScriptValue& message,
const PostMessageOptions* options,
ExceptionState& exception_state) {
if (!CheckPortalsEnabledOrThrow(exception_state) || !GetExecutionContext())
return;
if (!portal_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The HTMLPortalElement is not associated with a portal context");
return;
}
BlinkTransferableMessage transferable_message =
PortalPostMessageHelper::CreateMessage(script_state, message, options,
exception_state);
if (exception_state.HadException())
return;
portal_->PostMessageToGuest(std::move(transferable_message));
}
EventListener* HTMLPortalElement::onmessage() {
return GetAttributeEventListener(event_type_names::kMessage);
}
void HTMLPortalElement::setOnmessage(EventListener* listener) {
SetAttributeEventListener(event_type_names::kMessage, listener);
}
EventListener* HTMLPortalElement::onmessageerror() {
return GetAttributeEventListener(event_type_names::kMessageerror);
}
void HTMLPortalElement::setOnmessageerror(EventListener* listener) {
SetAttributeEventListener(event_type_names::kMessageerror, listener);
}
const PortalToken& HTMLPortalElement::GetToken() const {
DCHECK(portal_ && portal_->IsValid());
return portal_->GetToken().value();
}
Node::InsertionNotificationRequest HTMLPortalElement::InsertedInto(
ContainerNode& node) {
auto result = HTMLFrameOwnerElement::InsertedInto(node);
if (!CheckPortalsEnabledOrWarn())
return result;
if (!CheckWithinFrameLimitOrWarn())
return result;
if (!SubframeLoadingDisabler::CanLoadFrame(*this))
return result;
switch (GetGuestContentsEligibility()) {
case GuestContentsEligibility::kIneligible:
return result;
case GuestContentsEligibility::kNotTopLevel:
GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kRendering,
mojom::ConsoleMessageLevel::kWarning,
"Cannot use <portal> in a nested browsing context."));
return result;
case GuestContentsEligibility::kSandboxed:
GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kRendering,
mojom::ConsoleMessageLevel::kWarning,
"Cannot use <portal> in a sandboxed browsing context."));
return result;
case GuestContentsEligibility::kNotHTTPFamily:
GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kRendering,
mojom::ConsoleMessageLevel::kWarning,
"<portal> use is restricted to the HTTP family."));
return result;
case GuestContentsEligibility::kEligible:
break;
};
// When adopting a predecessor, it is possible to insert a portal that's
// eligible to have a guest contents to a node that's not connected. In this
// case, do not create the portal frame yet.
if (!node.isConnected()) {
return result;
}
if (portal_) {
// The interface is already bound if the HTMLPortalElement is adopting the
// predecessor.
GetDocument().GetFrame()->Client()->AdoptPortal(this);
} else {
mojo::PendingAssociatedRemote<mojom::blink::Portal> portal;
mojo::PendingAssociatedReceiver<mojom::blink::Portal> portal_receiver =
portal.InitWithNewEndpointAndPassReceiver();
mojo::PendingAssociatedRemote<mojom::blink::PortalClient> client;
mojo::PendingAssociatedReceiver<mojom::blink::PortalClient>
client_receiver = client.InitWithNewEndpointAndPassReceiver();
RemoteFrame* portal_frame;
PortalToken portal_token;
std::tie(portal_frame, portal_token) =
GetDocument().GetFrame()->Client()->CreatePortal(
this, std::move(portal_receiver), std::move(client));
DCHECK(portal_frame);
portal_ = MakeGarbageCollected<PortalContents>(
*this, portal_token, std::move(portal), std::move(client_receiver));
Navigate();
}
probe::PortalRemoteFrameCreated(&GetDocument(), this);
return result;
}
void HTMLPortalElement::RemovedFrom(ContainerNode& node) {
DCHECK(!portal_) << "This element should have previously dissociated in "
"DisconnectContentFrame";
HTMLFrameOwnerElement::RemovedFrom(node);
}
void HTMLPortalElement::DefaultEventHandler(Event& event) {
// Clicking (or equivalent operations via keyboard and other input modalities)
// a portal element causes it to activate unless prevented.
if (event.type() == event_type_names::kDOMActivate) {
ActivateDefault();
event.SetDefaultHandled();
}
if (HandleKeyboardActivation(event))
return;
HTMLFrameOwnerElement::DefaultEventHandler(event);
}
bool HTMLPortalElement::IsURLAttribute(const Attribute& attribute) const {
return attribute.GetName() == html_names::kSrcAttr ||
HTMLFrameOwnerElement::IsURLAttribute(attribute);
}
void HTMLPortalElement::ParseAttribute(
const AttributeModificationParams& params) {
HTMLFrameOwnerElement::ParseAttribute(params);
if (params.name == html_names::kSrcAttr) {
Navigate();
return;
}
if (params.name == html_names::kReferrerpolicyAttr) {
referrer_policy_ = network::mojom::ReferrerPolicy::kDefault;
if (!params.new_value.IsNull()) {
SecurityPolicy::ReferrerPolicyFromString(
params.new_value, kDoNotSupportReferrerPolicyLegacyKeywords,
&referrer_policy_);
}
return;
}
struct {
const QualifiedName& name;
const AtomicString& event_name;
} event_handler_attributes[] = {
{html_names::kOnmessageAttr, event_type_names::kMessage},
{html_names::kOnmessageerrorAttr, event_type_names::kMessageerror},
};
for (const auto& attribute : event_handler_attributes) {
if (params.name == attribute.name) {
SetAttributeEventListener(
attribute.event_name,
JSEventHandlerForContentAttribute::Create(
GetExecutionContext(), attribute.name, params.new_value));
return;
}
}
}
LayoutObject* HTMLPortalElement::CreateLayoutObject(const ComputedStyle& style,
LegacyLayout) {
return new LayoutIFrame(this);
}
bool HTMLPortalElement::SupportsFocus() const {
return true;
}
void HTMLPortalElement::DisconnectContentFrame() {
HTMLFrameOwnerElement::DisconnectContentFrame();
ConsumePortal();
}
void HTMLPortalElement::AttachLayoutTree(AttachContext& context) {
HTMLFrameOwnerElement::AttachLayoutTree(context);
if (GetLayoutEmbeddedContent() && ContentFrame())
SetEmbeddedContentView(ContentFrame()->View());
}
network::mojom::ReferrerPolicy HTMLPortalElement::ReferrerPolicyAttribute() {
return referrer_policy_;
}
} // namespace blink