blob: d782e5a2ab816bd76825718e8cf2c863e1816c2d [file] [log] [blame]
// Copyright 2016 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 "bindings/core/v8/ScriptCustomElementDefinition.h"
#include "bindings/core/v8/ScriptState.h"
#include "bindings/core/v8/V8Binding.h"
#include "bindings/core/v8/V8BindingMacros.h"
#include "bindings/core/v8/V8CustomElementRegistry.h"
#include "bindings/core/v8/V8Element.h"
#include "bindings/core/v8/V8ErrorHandler.h"
#include "bindings/core/v8/V8HiddenValue.h"
#include "bindings/core/v8/V8ScriptRunner.h"
#include "bindings/core/v8/V8ThrowException.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/custom/CustomElement.h"
#include "core/events/ErrorEvent.h"
#include "core/html/HTMLElement.h"
#include "v8.h"
#include "wtf/Allocator.h"
namespace blink {
// Retrieves the custom elements constructor -> name map, creating it
// if necessary. The same map is used to keep prototypes alive.
static v8::Local<v8::Map> ensureCustomElementRegistryMap(
ScriptState* scriptState,
CustomElementRegistry* registry) {
CHECK(scriptState->world().isMainWorld());
v8::Local<v8::String> name =
V8HiddenValue::customElementsRegistryMap(scriptState->isolate());
v8::Local<v8::Object> wrapper = toV8(registry, scriptState).As<v8::Object>();
v8::Local<v8::Value> map =
V8HiddenValue::getHiddenValue(scriptState, wrapper, name);
if (map.IsEmpty()) {
map = v8::Map::New(scriptState->isolate());
V8HiddenValue::setHiddenValue(scriptState, wrapper, name, map);
}
return map.As<v8::Map>();
}
ScriptCustomElementDefinition* ScriptCustomElementDefinition::forConstructor(
ScriptState* scriptState,
CustomElementRegistry* registry,
const v8::Local<v8::Value>& constructor) {
v8::Local<v8::Map> map =
ensureCustomElementRegistryMap(scriptState, registry);
v8::Local<v8::Value> nameValue =
map->Get(scriptState->context(), constructor).ToLocalChecked();
if (!nameValue->IsString())
return nullptr;
AtomicString name = toCoreAtomicString(nameValue.As<v8::String>());
// This downcast is safe because only
// ScriptCustomElementDefinitions have a name associated with a V8
// constructor in the map; see
// ScriptCustomElementDefinition::create. This relies on three
// things:
//
// 1. Only ScriptCustomElementDefinition adds entries to the map.
// Audit the use of V8HiddenValue/hidden values in general and
// how the map is handled--it should never be leaked to script.
//
// 2. CustomElementRegistry does not overwrite definitions with a
// given name--see the CHECK in CustomElementRegistry::define
// --and adds ScriptCustomElementDefinitions to the map without
// fail.
//
// 3. The relationship between the CustomElementRegistry and its
// map is never mixed up; this is guaranteed by the bindings
// system which provides a stable wrapper, and the map hangs
// off the wrapper.
//
// At a meta-level, this downcast is safe because there is
// currently only one implementation of CustomElementDefinition in
// product code and that is ScriptCustomElementDefinition. But
// that may change in the future.
CustomElementDefinition* definition = registry->definitionForName(name);
CHECK(definition);
return static_cast<ScriptCustomElementDefinition*>(definition);
}
template <typename T>
static void keepAlive(v8::Local<v8::Array>& array,
uint32_t index,
const v8::Local<T>& value,
ScopedPersistent<T>& persistent,
ScriptState* scriptState) {
if (value.IsEmpty())
return;
array->Set(scriptState->context(), index, value).ToChecked();
persistent.set(scriptState->isolate(), value);
persistent.setPhantom();
}
ScriptCustomElementDefinition* ScriptCustomElementDefinition::create(
ScriptState* scriptState,
CustomElementRegistry* registry,
const CustomElementDescriptor& descriptor,
const v8::Local<v8::Object>& constructor,
const v8::Local<v8::Object>& prototype,
const v8::Local<v8::Function>& connectedCallback,
const v8::Local<v8::Function>& disconnectedCallback,
const v8::Local<v8::Function>& adoptedCallback,
const v8::Local<v8::Function>& attributeChangedCallback,
const HashSet<AtomicString>& observedAttributes) {
ScriptCustomElementDefinition* definition = new ScriptCustomElementDefinition(
scriptState, descriptor, constructor, prototype, connectedCallback,
disconnectedCallback, adoptedCallback, attributeChangedCallback,
observedAttributes);
// Add a constructor -> name mapping to the registry.
v8::Local<v8::Value> nameValue =
v8String(scriptState->isolate(), descriptor.name());
v8::Local<v8::Map> map =
ensureCustomElementRegistryMap(scriptState, registry);
map->Set(scriptState->context(), constructor, nameValue).ToLocalChecked();
definition->m_constructor.setPhantom();
// We add the prototype and callbacks here to keep them alive. We use the
// name as the key because it is unique per-registry.
v8::Local<v8::Array> array = v8::Array::New(scriptState->isolate(), 5);
keepAlive(array, 0, prototype, definition->m_prototype, scriptState);
keepAlive(array, 1, connectedCallback, definition->m_connectedCallback,
scriptState);
keepAlive(array, 2, disconnectedCallback, definition->m_disconnectedCallback,
scriptState);
keepAlive(array, 3, adoptedCallback, definition->m_adoptedCallback,
scriptState);
keepAlive(array, 4, attributeChangedCallback,
definition->m_attributeChangedCallback, scriptState);
map->Set(scriptState->context(), nameValue, array).ToLocalChecked();
return definition;
}
ScriptCustomElementDefinition::ScriptCustomElementDefinition(
ScriptState* scriptState,
const CustomElementDescriptor& descriptor,
const v8::Local<v8::Object>& constructor,
const v8::Local<v8::Object>& prototype,
const v8::Local<v8::Function>& connectedCallback,
const v8::Local<v8::Function>& disconnectedCallback,
const v8::Local<v8::Function>& adoptedCallback,
const v8::Local<v8::Function>& attributeChangedCallback,
const HashSet<AtomicString>& observedAttributes)
: CustomElementDefinition(descriptor, observedAttributes),
m_scriptState(scriptState),
m_constructor(scriptState->isolate(), constructor) {}
HTMLElement* ScriptCustomElementDefinition::createElementSync(
Document& document,
const QualifiedName& tagName,
ExceptionState& exceptionState) {
DCHECK(ScriptState::current(m_scriptState->isolate()) == m_scriptState);
// Create an element
// https://dom.spec.whatwg.org/#concept-create-element
// 6. If definition is non-null
// 6.1. If the synchronous custom elements flag is set:
// 6.1.2. Set result to Construct(C). Rethrow any exceptions.
// Create an element and push to the construction stack.
// V8HTMLElement::constructorCustom() can only refer to
// window.document(), but it is different from the document here
// when it is an import document. This is not exactly what the
// spec defines, but the public behavior matches to the spec.
Element* element = createElementForConstructor(document);
{
ConstructionStackScope constructionStackScope(this, element);
v8::TryCatch tryCatch(m_scriptState->isolate());
element = runConstructor();
if (tryCatch.HasCaught()) {
exceptionState.rethrowV8Exception(tryCatch.Exception());
return nullptr;
}
}
// 6.1.3. through 6.1.9.
checkConstructorResult(element, document, tagName, exceptionState);
if (exceptionState.hadException())
return nullptr;
DCHECK_EQ(element->getCustomElementState(), CustomElementState::Custom);
return toHTMLElement(element);
}
static void dispatchErrorEvent(v8::Isolate* isolate,
v8::Local<v8::Value> exception,
v8::Local<v8::Object> constructor) {
v8::TryCatch tryCatch(isolate);
tryCatch.SetVerbose(true);
V8ScriptRunner::throwException(
isolate, exception, constructor.As<v8::Function>()->GetScriptOrigin());
}
HTMLElement* ScriptCustomElementDefinition::createElementSync(
Document& document,
const QualifiedName& tagName) {
ScriptState::Scope scope(m_scriptState.get());
v8::Isolate* isolate = m_scriptState->isolate();
// When invoked from "create an element for a token":
// https://html.spec.whatwg.org/multipage/syntax.html#create-an-element-for-the-token
ExceptionState exceptionState(ExceptionState::ConstructionContext,
"CustomElement", constructor(), isolate);
HTMLElement* element = createElementSync(document, tagName, exceptionState);
if (exceptionState.hadException()) {
DCHECK(!element);
// 7. If this step throws an exception, then report the exception, ...
dispatchErrorEvent(isolate, exceptionState.getException(), constructor());
exceptionState.clearException();
// and return HTMLUnknownElement.
return CustomElement::createFailedElement(document, tagName);
}
DCHECK(element);
return element;
}
// https://html.spec.whatwg.org/multipage/scripting.html#upgrades
bool ScriptCustomElementDefinition::runConstructor(Element* element) {
if (!m_scriptState->contextIsValid())
return false;
ScriptState::Scope scope(m_scriptState.get());
v8::Isolate* isolate = m_scriptState->isolate();
// Step 5 says to rethrow the exception; but there is no one to
// catch it. The side effect is to report the error.
v8::TryCatch tryCatch(isolate);
tryCatch.SetVerbose(true);
Element* result = runConstructor();
// To report exception thrown from runConstructor()
if (tryCatch.HasCaught())
return false;
// To report InvalidStateError Exception, when the constructor returns some
// different object
if (result != element) {
const String& message =
"custom element constructors must call super() first and must "
"not return a different object";
v8::Local<v8::Value> exception = V8ThrowException::createDOMException(
m_scriptState->isolate(), InvalidStateError, message);
dispatchErrorEvent(isolate, exception, constructor());
return false;
}
return true;
}
Element* ScriptCustomElementDefinition::runConstructor() {
v8::Isolate* isolate = m_scriptState->isolate();
DCHECK(ScriptState::current(isolate) == m_scriptState);
ExecutionContext* executionContext = m_scriptState->getExecutionContext();
v8::Local<v8::Value> result;
if (!v8Call(V8ScriptRunner::callAsConstructor(isolate, constructor(),
executionContext, 0, nullptr),
result)) {
return nullptr;
}
return V8Element::toImplWithTypeCheck(isolate, result);
}
v8::Local<v8::Object> ScriptCustomElementDefinition::constructor() const {
DCHECK(!m_constructor.isEmpty());
return m_constructor.newLocal(m_scriptState->isolate());
}
v8::Local<v8::Object> ScriptCustomElementDefinition::prototype() const {
DCHECK(!m_prototype.isEmpty());
return m_prototype.newLocal(m_scriptState->isolate());
}
// CustomElementDefinition
ScriptValue ScriptCustomElementDefinition::getConstructorForScript() {
return ScriptValue(m_scriptState.get(), constructor());
}
bool ScriptCustomElementDefinition::hasConnectedCallback() const {
return !m_connectedCallback.isEmpty();
}
bool ScriptCustomElementDefinition::hasDisconnectedCallback() const {
return !m_disconnectedCallback.isEmpty();
}
bool ScriptCustomElementDefinition::hasAdoptedCallback() const {
return !m_adoptedCallback.isEmpty();
}
void ScriptCustomElementDefinition::runCallback(
v8::Local<v8::Function> callback,
Element* element,
int argc,
v8::Local<v8::Value> argv[]) {
DCHECK(ScriptState::current(m_scriptState->isolate()) == m_scriptState);
v8::Isolate* isolate = m_scriptState->isolate();
// Invoke custom element reactions
// https://html.spec.whatwg.org/multipage/scripting.html#invoke-custom-element-reactions
// If this throws any exception, then report the exception.
v8::TryCatch tryCatch(isolate);
tryCatch.SetVerbose(true);
ExecutionContext* executionContext = m_scriptState->getExecutionContext();
v8::Local<v8::Value> elementHandle =
toV8(element, m_scriptState->context()->Global(), isolate);
V8ScriptRunner::callFunction(callback, executionContext, elementHandle, argc,
argv, isolate);
}
void ScriptCustomElementDefinition::runConnectedCallback(Element* element) {
if (!m_scriptState->contextIsValid())
return;
ScriptState::Scope scope(m_scriptState.get());
v8::Isolate* isolate = m_scriptState->isolate();
runCallback(m_connectedCallback.newLocal(isolate), element);
}
void ScriptCustomElementDefinition::runDisconnectedCallback(Element* element) {
if (!m_scriptState->contextIsValid())
return;
ScriptState::Scope scope(m_scriptState.get());
v8::Isolate* isolate = m_scriptState->isolate();
runCallback(m_disconnectedCallback.newLocal(isolate), element);
}
void ScriptCustomElementDefinition::runAdoptedCallback(Element* element,
Document* oldOwner,
Document* newOwner) {
if (!m_scriptState->contextIsValid())
return;
ScriptState::Scope scope(m_scriptState.get());
v8::Isolate* isolate = m_scriptState->isolate();
v8::Local<v8::Value> argv[] = {
toV8(oldOwner, m_scriptState->context()->Global(), isolate),
toV8(newOwner, m_scriptState->context()->Global(), isolate)};
runCallback(m_adoptedCallback.newLocal(isolate), element,
WTF_ARRAY_LENGTH(argv), argv);
}
void ScriptCustomElementDefinition::runAttributeChangedCallback(
Element* element,
const QualifiedName& name,
const AtomicString& oldValue,
const AtomicString& newValue) {
if (!m_scriptState->contextIsValid())
return;
ScriptState::Scope scope(m_scriptState.get());
v8::Isolate* isolate = m_scriptState->isolate();
v8::Local<v8::Value> argv[] = {
v8String(isolate, name.localName()), v8StringOrNull(isolate, oldValue),
v8StringOrNull(isolate, newValue),
v8StringOrNull(isolate, name.namespaceURI()),
};
runCallback(m_attributeChangedCallback.newLocal(isolate), element,
WTF_ARRAY_LENGTH(argv), argv);
}
} // namespace blink