blob: 82a47d7a71da7feb1b8e70d85f74d74807127f2f [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 "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/renderer/bindings/core/v8/script_event_listener.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/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_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/frame/window_post_message_options.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_activate_options.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/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/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
HTMLPortalElement::HTMLPortalElement(
Document& document,
const base::UnguessableToken& portal_token,
mojom::blink::PortalAssociatedPtr portal_ptr,
mojom::blink::PortalClientAssociatedRequest portal_client_request)
: HTMLFrameOwnerElement(html_names::kPortalTag, document),
portal_token_(portal_token),
portal_ptr_(std::move(portal_ptr)),
portal_client_binding_(this, std::move(portal_client_request)) {}
HTMLPortalElement::~HTMLPortalElement() {}
void HTMLPortalElement::Trace(Visitor* visitor) {
HTMLFrameOwnerElement::Trace(visitor);
visitor->Trace(portal_frame_);
}
void HTMLPortalElement::Navigate() {
KURL url = GetNonEmptyURLAttribute(html_names::kSrcAttr);
if (!url.IsEmpty() && portal_ptr_) {
portal_ptr_->Navigate(url);
}
}
void HTMLPortalElement::ConsumePortal() {
if (portal_token_) {
DocumentPortals::From(GetDocument()).OnPortalRemoved(this);
portal_token_ = base::UnguessableToken();
}
portal_ptr_.reset();
portal_client_binding_.Close();
}
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.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) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext,
"HTMLPortalElement", "activate");
if (!portal_ptr_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The HTMLPortalElement is not associated with a portal context.");
resolver->Reject(exception_state);
return promise;
}
if (is_activating_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"activate() has already been called on this HTMLPortalElement.");
resolver->Reject(exception_state);
return promise;
}
if (DocumentPortals::From(GetDocument()).IsPortalInDocumentActivating()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"activate() has already been called on another "
"HTMLPortalElement in this document.");
resolver->Reject(exception_state);
return promise;
}
if (GetDocument().GetPage()->InsidePortal()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"Cannot activate a portal that is inside another portal.");
resolver->Reject(exception_state);
return promise;
}
BlinkTransferableMessage data =
ActivateDataAsMessage(script_state, options, exception_state);
if (exception_state.HadException()) {
resolver->Reject(exception_state);
return promise;
}
// The HTMLPortalElement is bound as a persistent so that it won't get
// garbage collected while there is a pending callback.
// We also pass the ownership of the PortalPtr, which guarantees that the
// PortalPtr stays alive until the callback is called.
is_activating_ = true;
auto* raw_portal_ptr = portal_ptr_.get();
raw_portal_ptr->Activate(
std::move(data),
WTF::Bind(
[](HTMLPortalElement* portal,
mojom::blink::PortalAssociatedPtr portal_ptr,
ScriptPromiseResolver* resolver, bool was_adopted) {
if (was_adopted)
portal->GetDocument().GetPage()->SetInsidePortal(true);
resolver->Resolve();
portal->is_activating_ = false;
portal->ConsumePortal();
},
WrapPersistent(this), std::move(portal_ptr_),
WrapPersistent(resolver)));
return promise;
}
void HTMLPortalElement::postMessage(ScriptState* script_state,
const ScriptValue& message,
const String& target_origin,
const Vector<ScriptValue>& transfer,
ExceptionState& exception_state) {
WindowPostMessageOptions* options = WindowPostMessageOptions::Create();
options->setTargetOrigin(target_origin);
if (!transfer.IsEmpty())
options->setTransfer(transfer);
postMessage(script_state, message, options, exception_state);
}
void HTMLPortalElement::postMessage(ScriptState* script_state,
const ScriptValue& message,
const WindowPostMessageOptions* options,
ExceptionState& exception_state) {
if (!portal_ptr_ || is_activating_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The HTMLPortalElement is not associated with a portal context");
return;
}
scoped_refptr<const SecurityOrigin> target_origin =
PostMessageHelper::GetTargetOrigin(options, GetDocument(),
exception_state);
if (exception_state.HadException())
return;
BlinkTransferableMessage transferable_message =
PortalPostMessageHelper::CreateMessage(script_state, message, options,
exception_state);
if (exception_state.HadException())
return;
portal_ptr_->PostMessageToGuest(std::move(transferable_message),
target_origin);
}
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);
}
void HTMLPortalElement::ForwardMessageFromGuest(
BlinkTransferableMessage message,
const scoped_refptr<const SecurityOrigin>& source_origin,
const scoped_refptr<const SecurityOrigin>& target_origin) {
if (!portal_ptr_)
return;
PortalPostMessageHelper::CreateAndDispatchMessageEvent(
this, std::move(message), source_origin, target_origin);
}
void HTMLPortalElement::DispatchLoadEvent() {
if (!portal_ptr_)
return;
DispatchLoad();
GetDocument().CheckCompleted();
}
HTMLPortalElement::InsertionNotificationRequest HTMLPortalElement::InsertedInto(
ContainerNode& node) {
auto result = HTMLFrameOwnerElement::InsertedInto(node);
if (!node.IsInDocumentTree() || !GetDocument().IsHTMLDocument() ||
!GetDocument().GetFrame())
return result;
// We don't support embedding portals in nested browsing contexts.
if (!GetDocument().GetFrame()->IsMainFrame()) {
GetDocument().AddConsoleMessage(ConsoleMessage::Create(
mojom::ConsoleMessageSource::kRendering,
mojom::ConsoleMessageLevel::kWarning,
"Cannot use <portal> in a nested browsing context."));
return result;
}
if (portal_ptr_) {
// The interface is already bound if the HTMLPortalElement is adopting the
// predecessor.
portal_frame_ = GetDocument().GetFrame()->Client()->AdoptPortal(this);
portal_ptr_.set_connection_error_handler(
WTF::Bind(&HTMLPortalElement::ConsumePortal, WrapWeakPersistent(this)));
DocumentPortals::From(GetDocument()).OnPortalInserted(this);
} else {
mojom::blink::PortalClientAssociatedPtr client;
portal_client_binding_.Bind(mojo::MakeRequest(&client));
std::tie(portal_frame_, portal_token_) =
GetDocument().GetFrame()->Client()->CreatePortal(
this, mojo::MakeRequest(&portal_ptr_), client.PassInterface());
portal_ptr_.set_connection_error_handler(
WTF::Bind(&HTMLPortalElement::ConsumePortal, WrapWeakPersistent(this)));
DocumentPortals::From(GetDocument()).OnPortalInserted(this);
Navigate();
}
return result;
}
void HTMLPortalElement::RemovedFrom(ContainerNode& node) {
HTMLFrameOwnerElement::RemovedFrom(node);
Document& document = GetDocument();
if (node.IsInDocumentTree() && document.IsHTMLDocument()) {
ConsumePortal();
}
}
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;
}
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,
CreateAttributeEventListener(this, attribute.name, params.new_value));
return;
}
}
}
LayoutObject* HTMLPortalElement::CreateLayoutObject(const ComputedStyle& style,
LegacyLayout) {
return new LayoutIFrame(this);
}
void HTMLPortalElement::AttachLayoutTree(AttachContext& context) {
HTMLFrameOwnerElement::AttachLayoutTree(context);
if (GetLayoutEmbeddedContent() && ContentFrame())
SetEmbeddedContentView(ContentFrame()->View());
}
} // namespace blink