1/*
2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 * Copyright (C) 2015-2017 Apple Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24 * THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27
28#include "config.h"
29#include "JSCustomElementInterface.h"
30
31#include "DOMWrapperWorld.h"
32#include "HTMLUnknownElement.h"
33#include "JSDOMBinding.h"
34#include "JSDOMConvertNullable.h"
35#include "JSDOMConvertStrings.h"
36#include "JSDOMWindow.h"
37#include "JSElement.h"
38#include "JSExecState.h"
39#include "JSExecStateInstrumentation.h"
40#include "JSHTMLElement.h"
41#include "ScriptExecutionContext.h"
42#include <JavaScriptCore/JSLock.h>
43#include <JavaScriptCore/WeakInlines.h>
44
45namespace WebCore {
46using namespace JSC;
47
48JSCustomElementInterface::JSCustomElementInterface(const QualifiedName& name, JSObject* constructor, JSDOMGlobalObject* globalObject)
49 : ActiveDOMCallback(globalObject->scriptExecutionContext())
50 , m_name(name)
51 , m_constructor(constructor)
52 , m_isolatedWorld(globalObject->world())
53{
54}
55
56JSCustomElementInterface::~JSCustomElementInterface() = default;
57
58static RefPtr<Element> constructCustomElementSynchronously(Document&, VM&, ExecState&, JSObject* constructor, const AtomicString& localName);
59
60Ref<Element> JSCustomElementInterface::constructElementWithFallback(Document& document, const AtomicString& localName)
61{
62 if (auto element = tryToConstructCustomElement(document, localName))
63 return element.releaseNonNull();
64
65 auto element = HTMLUnknownElement::create(QualifiedName(nullAtom(), localName, HTMLNames::xhtmlNamespaceURI), document);
66 element->setIsCustomElementUpgradeCandidate();
67 element->setIsFailedCustomElement(*this);
68
69 return element;
70}
71
72Ref<Element> JSCustomElementInterface::constructElementWithFallback(Document& document, const QualifiedName& name)
73{
74 if (auto element = tryToConstructCustomElement(document, name.localName())) {
75 if (!name.prefix().isNull())
76 element->setPrefix(name.prefix());
77 return element.releaseNonNull();
78 }
79
80 auto element = HTMLUnknownElement::create(name, document);
81 element->setIsCustomElementUpgradeCandidate();
82 element->setIsFailedCustomElement(*this);
83
84 return element;
85}
86
87RefPtr<Element> JSCustomElementInterface::tryToConstructCustomElement(Document& document, const AtomicString& localName)
88{
89 if (!canInvokeCallback())
90 return nullptr;
91
92 Ref<JSCustomElementInterface> protectedThis(*this);
93
94 VM& vm = m_isolatedWorld->vm();
95 JSLockHolder lock(vm);
96 auto scope = DECLARE_CATCH_SCOPE(vm);
97
98 if (!m_constructor)
99 return nullptr;
100
101 ASSERT(&document == scriptExecutionContext());
102 auto& state = *document.execState();
103 auto element = constructCustomElementSynchronously(document, vm, state, m_constructor.get(), localName);
104 EXCEPTION_ASSERT(!!scope.exception() == !element);
105 if (!element) {
106 auto* exception = scope.exception();
107 scope.clearException();
108 reportException(&state, exception);
109 return nullptr;
110 }
111
112 return element;
113}
114
115// https://dom.spec.whatwg.org/#concept-create-element
116// 6. 1. If the synchronous custom elements flag is set
117static RefPtr<Element> constructCustomElementSynchronously(Document& document, VM& vm, ExecState& state, JSObject* constructor, const AtomicString& localName)
118{
119 auto scope = DECLARE_THROW_SCOPE(vm);
120 ConstructData constructData;
121 ConstructType constructType = constructor->methodTable(vm)->getConstructData(constructor, constructData);
122 if (constructType == ConstructType::None) {
123 ASSERT_NOT_REACHED();
124 return nullptr;
125 }
126
127 InspectorInstrumentationCookie cookie = JSExecState::instrumentFunctionConstruct(&document, constructType, constructData);
128 MarkedArgumentBuffer args;
129 ASSERT(!args.hasOverflowed());
130 JSValue newElement = construct(&state, constructor, constructType, constructData, args);
131 InspectorInstrumentation::didCallFunction(cookie, &document);
132 RETURN_IF_EXCEPTION(scope, nullptr);
133
134 ASSERT(!newElement.isEmpty());
135 HTMLElement* wrappedElement = JSHTMLElement::toWrapped(vm, newElement);
136 if (!wrappedElement) {
137 throwTypeError(&state, scope, "The result of constructing a custom element must be a HTMLElement"_s);
138 return nullptr;
139 }
140
141 if (wrappedElement->hasAttributes()) {
142 throwNotSupportedError(state, scope, "A newly constructed custom element must not have attributes"_s);
143 return nullptr;
144 }
145 if (wrappedElement->hasChildNodes()) {
146 throwNotSupportedError(state, scope, "A newly constructed custom element must not have child nodes"_s);
147 return nullptr;
148 }
149 if (wrappedElement->parentNode()) {
150 throwNotSupportedError(state, scope, "A newly constructed custom element must not have a parent node"_s);
151 return nullptr;
152 }
153 if (&wrappedElement->document() != &document) {
154 throwNotSupportedError(state, scope, "A newly constructed custom element belongs to a wrong document"_s);
155 return nullptr;
156 }
157 ASSERT(wrappedElement->namespaceURI() == HTMLNames::xhtmlNamespaceURI);
158 if (wrappedElement->localName() != localName) {
159 throwNotSupportedError(state, scope, "A newly constructed custom element has incorrect local name"_s);
160 return nullptr;
161 }
162
163 return wrappedElement;
164}
165
166void JSCustomElementInterface::upgradeElement(Element& element)
167{
168 ASSERT(element.tagQName() == name());
169 ASSERT(element.isCustomElementUpgradeCandidate());
170 if (!canInvokeCallback())
171 return;
172
173 Ref<JSCustomElementInterface> protectedThis(*this);
174 VM& vm = m_isolatedWorld->vm();
175 JSLockHolder lock(vm);
176 auto scope = DECLARE_THROW_SCOPE(vm);
177
178 if (!m_constructor)
179 return;
180
181 auto* context = scriptExecutionContext();
182 if (!context)
183 return;
184 auto* globalObject = toJSDOMWindow(downcast<Document>(*context).frame(), m_isolatedWorld);
185 if (!globalObject)
186 return;
187 ExecState* state = globalObject->globalExec();
188
189 ConstructData constructData;
190 ConstructType constructType = m_constructor->methodTable(vm)->getConstructData(m_constructor.get(), constructData);
191 if (constructType == ConstructType::None) {
192 ASSERT_NOT_REACHED();
193 return;
194 }
195
196 CustomElementReactionQueue::enqueuePostUpgradeReactions(element);
197
198 m_constructionStack.append(&element);
199
200 MarkedArgumentBuffer args;
201 ASSERT(!args.hasOverflowed());
202 InspectorInstrumentationCookie cookie = JSExecState::instrumentFunctionConstruct(context, constructType, constructData);
203 JSValue returnedElement = construct(state, m_constructor.get(), constructType, constructData, args);
204 InspectorInstrumentation::didCallFunction(cookie, context);
205
206 m_constructionStack.removeLast();
207
208 if (UNLIKELY(scope.exception())) {
209 element.setIsFailedCustomElement(*this);
210 reportException(state, scope.exception());
211 return;
212 }
213
214 Element* wrappedElement = JSElement::toWrapped(vm, returnedElement);
215 if (!wrappedElement || wrappedElement != &element) {
216 element.setIsFailedCustomElement(*this);
217 reportException(state, createDOMException(state, TypeError, "Custom element constructor returned a wrong element"));
218 return;
219 }
220 element.setIsDefinedCustomElement(*this);
221}
222
223void JSCustomElementInterface::invokeCallback(Element& element, JSObject* callback, const WTF::Function<void(ExecState*, JSDOMGlobalObject*, MarkedArgumentBuffer&)>& addArguments)
224{
225 if (!canInvokeCallback())
226 return;
227
228 auto* context = scriptExecutionContext();
229 if (!context)
230 return;
231
232 Ref<JSCustomElementInterface> protectedThis(*this);
233 VM& vm = m_isolatedWorld->vm();
234 JSLockHolder lock(vm);
235
236 auto* globalObject = toJSDOMWindow(downcast<Document>(*context).frame(), m_isolatedWorld);
237 if (!globalObject)
238 return;
239 ExecState* state = globalObject->globalExec();
240
241 JSObject* jsElement = asObject(toJS(state, globalObject, element));
242
243 CallData callData;
244 CallType callType = callback->methodTable(vm)->getCallData(callback, callData);
245 ASSERT(callType != CallType::None);
246
247 MarkedArgumentBuffer args;
248 addArguments(state, globalObject, args);
249 RELEASE_ASSERT(!args.hasOverflowed());
250
251 InspectorInstrumentationCookie cookie = JSExecState::instrumentFunctionCall(context, callType, callData);
252
253 NakedPtr<JSC::Exception> exception;
254 JSExecState::call(state, callback, callType, callData, jsElement, args, exception);
255
256 InspectorInstrumentation::didCallFunction(cookie, context);
257
258 if (exception)
259 reportException(state, exception);
260}
261
262void JSCustomElementInterface::setConnectedCallback(JSC::JSObject* callback)
263{
264 m_connectedCallback = callback;
265}
266
267void JSCustomElementInterface::invokeConnectedCallback(Element& element)
268{
269 invokeCallback(element, m_connectedCallback.get());
270}
271
272void JSCustomElementInterface::setDisconnectedCallback(JSC::JSObject* callback)
273{
274 m_disconnectedCallback = callback;
275}
276
277void JSCustomElementInterface::invokeDisconnectedCallback(Element& element)
278{
279 invokeCallback(element, m_disconnectedCallback.get());
280}
281
282void JSCustomElementInterface::setAdoptedCallback(JSC::JSObject* callback)
283{
284 m_adoptedCallback = callback;
285}
286
287void JSCustomElementInterface::invokeAdoptedCallback(Element& element, Document& oldDocument, Document& newDocument)
288{
289 invokeCallback(element, m_adoptedCallback.get(), [&](ExecState* state, JSDOMGlobalObject* globalObject, MarkedArgumentBuffer& args) {
290 args.append(toJS(state, globalObject, oldDocument));
291 args.append(toJS(state, globalObject, newDocument));
292 });
293}
294
295void JSCustomElementInterface::setAttributeChangedCallback(JSC::JSObject* callback, const Vector<String>& observedAttributes)
296{
297 m_attributeChangedCallback = callback;
298 m_observedAttributes.clear();
299 for (auto& name : observedAttributes)
300 m_observedAttributes.add(name);
301}
302
303void JSCustomElementInterface::invokeAttributeChangedCallback(Element& element, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
304{
305 invokeCallback(element, m_attributeChangedCallback.get(), [&](ExecState* state, JSDOMGlobalObject*, MarkedArgumentBuffer& args) {
306 args.append(toJS<IDLDOMString>(*state, attributeName.localName()));
307 args.append(toJS<IDLNullable<IDLDOMString>>(*state, oldValue));
308 args.append(toJS<IDLNullable<IDLDOMString>>(*state, newValue));
309 args.append(toJS<IDLNullable<IDLDOMString>>(*state, attributeName.namespaceURI()));
310 });
311}
312
313void JSCustomElementInterface::didUpgradeLastElementInConstructionStack()
314{
315 m_constructionStack.last() = nullptr;
316}
317
318} // namespace WebCore
319