Upgrading custom element should enqueue attributeChanged and connected callbacks
[WebKit-https.git] / Source / WebCore / bindings / js / JSCustomElementInterface.cpp
1 /*
2  * Copyright (C) 2013 Google Inc. All rights reserved.
3  * Copyright (C) 2015-2016 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 #if ENABLE(CUSTOM_ELEMENTS)
32
33 #include "DOMWrapperWorld.h"
34 #include "HTMLUnknownElement.h"
35 #include "JSDOMBinding.h"
36 #include "JSDOMGlobalObject.h"
37 #include "JSElement.h"
38 #include "JSHTMLElement.h"
39 #include "JSMainThreadExecState.h"
40 #include "JSMainThreadExecStateInstrumentation.h"
41 #include "ScriptExecutionContext.h"
42 #include <heap/WeakInlines.h>
43 #include <runtime/JSLock.h>
44
45 using namespace JSC;
46
47 namespace WebCore {
48
49 JSCustomElementInterface::JSCustomElementInterface(const QualifiedName& name, JSObject* constructor, JSDOMGlobalObject* globalObject)
50     : ActiveDOMCallback(globalObject->scriptExecutionContext())
51     , m_name(name)
52     , m_constructor(constructor)
53     , m_isolatedWorld(&globalObject->world())
54 {
55 }
56
57 JSCustomElementInterface::~JSCustomElementInterface()
58 {
59 }
60
61 static RefPtr<Element> constructCustomElementSynchronously(Document&, VM&, ExecState&, JSObject* constructor, const AtomicString& localName);
62
63 Ref<Element> JSCustomElementInterface::constructElementWithFallback(Document& document, const AtomicString& localName)
64 {
65     if (auto element = tryToConstructCustomElement(document, localName))
66         return element.releaseNonNull();
67
68     auto element = HTMLUnknownElement::create(QualifiedName(nullAtom, localName, HTMLNames::xhtmlNamespaceURI), document);
69     element->setIsCustomElementUpgradeCandidate();
70     element->setIsFailedCustomElement(*this);
71
72     return element.get();
73 }
74
75 RefPtr<Element> JSCustomElementInterface::tryToConstructCustomElement(Document& document, const AtomicString& localName)
76 {
77     if (!canInvokeCallback())
78         return nullptr;
79
80     Ref<JSCustomElementInterface> protectedThis(*this);
81
82     VM& vm = m_isolatedWorld->vm();
83     JSLockHolder lock(vm);
84     auto scope = DECLARE_CATCH_SCOPE(vm);
85
86     if (!m_constructor)
87         return nullptr;
88
89     ASSERT(&document == scriptExecutionContext());
90     auto& state = *document.execState();
91     auto element = constructCustomElementSynchronously(document, vm, state, m_constructor.get(), localName);
92     ASSERT(!!scope.exception() == !element);
93     if (!element) {
94         auto* exception = scope.exception();
95         scope.clearException();
96         reportException(&state, exception);
97         return nullptr;
98     }
99
100     return element;
101 }
102
103 // https://dom.spec.whatwg.org/#concept-create-element
104 // 6. 1. If the synchronous custom elements flag is set
105 static RefPtr<Element> constructCustomElementSynchronously(Document& document, VM& vm, ExecState& state, JSObject* constructor, const AtomicString& localName)
106 {
107     auto scope = DECLARE_THROW_SCOPE(vm);
108     ConstructData constructData;
109     ConstructType constructType = constructor->methodTable()->getConstructData(constructor, constructData);
110     if (constructType == ConstructType::None) {
111         ASSERT_NOT_REACHED();
112         return nullptr;
113     }
114
115     MarkedArgumentBuffer args;
116     args.append(jsStringWithCache(&state, localName));
117
118     InspectorInstrumentationCookie cookie = JSMainThreadExecState::instrumentFunctionConstruct(&document, constructType, constructData);
119     JSValue newElement = construct(&state, constructor, constructType, constructData, args);
120     InspectorInstrumentation::didCallFunction(cookie, &document);
121     RETURN_IF_EXCEPTION(scope, nullptr);
122
123     ASSERT(!newElement.isEmpty());
124     HTMLElement* wrappedElement = JSHTMLElement::toWrapped(newElement);
125     if (!wrappedElement) {
126         throwTypeError(&state, scope, ASCIILiteral("The result of constructing a custom element must be a HTMLElement"));
127         return nullptr;
128     }
129
130     if (wrappedElement->hasAttributes()) {
131         throwNotSupportedError(state, scope, ASCIILiteral("A newly constructed custom element must not have attributes"));
132         return nullptr;
133     }
134     if (wrappedElement->hasChildNodes()) {
135         throwNotSupportedError(state, scope, ASCIILiteral("A newly constructed custom element must not have child nodes"));
136         return nullptr;
137     }
138     if (wrappedElement->parentNode()) {
139         throwNotSupportedError(state, scope, ASCIILiteral("A newly constructed custom element must not have a parent node"));
140         return nullptr;
141     }
142     if (&wrappedElement->document() != &document) {
143         throwNotSupportedError(state, scope, ASCIILiteral("A newly constructed custom element belongs to a wrong docuemnt"));
144         return nullptr;
145     }
146     ASSERT(wrappedElement->namespaceURI() == HTMLNames::xhtmlNamespaceURI);
147     if (wrappedElement->localName() != localName) {
148         throwNotSupportedError(state, scope, ASCIILiteral("A newly constructed custom element belongs to a wrong docuemnt"));
149         return nullptr;
150     }
151
152     return wrappedElement;
153 }
154
155 void JSCustomElementInterface::upgradeElement(Element& element)
156 {
157     ASSERT(element.tagQName() == name());
158     ASSERT(element.isCustomElementUpgradeCandidate());
159     if (!canInvokeCallback())
160         return;
161
162     Ref<JSCustomElementInterface> protectedThis(*this);
163     VM& vm = m_isolatedWorld->vm();
164     JSLockHolder lock(vm);
165     auto scope = DECLARE_THROW_SCOPE(vm);
166
167     if (!m_constructor)
168         return;
169
170     ScriptExecutionContext* context = scriptExecutionContext();
171     if (!context)
172         return;
173     ASSERT(context->isDocument());
174     JSDOMGlobalObject* globalObject = toJSDOMGlobalObject(context, *m_isolatedWorld);
175     ExecState* state = globalObject->globalExec();
176     RETURN_IF_EXCEPTION(scope, void());
177
178     ConstructData constructData;
179     ConstructType constructType = m_constructor->methodTable()->getConstructData(m_constructor.get(), constructData);
180     if (constructType == ConstructType::None) {
181         ASSERT_NOT_REACHED();
182         return;
183     }
184
185     CustomElementReactionQueue::enqueuePostUpgradeReactions(element, *this);
186
187     m_constructionStack.append(&element);
188
189     MarkedArgumentBuffer args;
190     InspectorInstrumentationCookie cookie = JSMainThreadExecState::instrumentFunctionConstruct(context, constructType, constructData);
191     JSValue returnedElement = construct(state, m_constructor.get(), constructType, constructData, args);
192     InspectorInstrumentation::didCallFunction(cookie, context);
193
194     m_constructionStack.removeLast();
195
196     if (UNLIKELY(scope.exception())) {
197         element.setIsFailedCustomElement(*this);
198         reportException(state, scope.exception());
199         return;
200     }
201
202     Element* wrappedElement = JSElement::toWrapped(returnedElement);
203     if (!wrappedElement || wrappedElement != &element) {
204         element.setIsFailedCustomElement(*this);
205         reportException(state, createDOMException(state, INVALID_STATE_ERR, "Custom element constructor failed to upgrade an element"));
206         return;
207     }
208     element.setIsDefinedCustomElement(*this);
209 }
210
211 void JSCustomElementInterface::invokeCallback(Element& element, JSObject* callback, const WTF::Function<void(ExecState*, JSDOMGlobalObject*, MarkedArgumentBuffer&)>& addArguments)
212 {
213     if (!canInvokeCallback())
214         return;
215
216     auto* context = scriptExecutionContext();
217     if (!context)
218         return;
219
220     Ref<JSCustomElementInterface> protectedThis(*this);
221     JSLockHolder lock(m_isolatedWorld->vm());
222
223     ASSERT(context);
224     ASSERT(context->isDocument());
225     JSDOMGlobalObject* globalObject = toJSDOMGlobalObject(context, *m_isolatedWorld);
226     ExecState* state = globalObject->globalExec();
227
228     JSObject* jsElement = asObject(toJS(state, globalObject, element));
229
230     CallData callData;
231     CallType callType = callback->methodTable()->getCallData(callback, callData);
232     ASSERT(callType != CallType::None);
233
234     MarkedArgumentBuffer args;
235     addArguments(state, globalObject, args);
236
237     InspectorInstrumentationCookie cookie = JSMainThreadExecState::instrumentFunctionCall(context, callType, callData);
238
239     NakedPtr<JSC::Exception> exception;
240     JSMainThreadExecState::call(state, callback, callType, callData, jsElement, args, exception);
241
242     InspectorInstrumentation::didCallFunction(cookie, context);
243
244     if (exception)
245         reportException(state, exception);
246 }
247
248 void JSCustomElementInterface::setConnectedCallback(JSC::JSObject* callback)
249 {
250     m_connectedCallback = callback;
251 }
252
253 void JSCustomElementInterface::invokeConnectedCallback(Element& element)
254 {
255     invokeCallback(element, m_connectedCallback.get());
256 }
257
258 void JSCustomElementInterface::setDisconnectedCallback(JSC::JSObject* callback)
259 {
260     m_disconnectedCallback = callback;
261 }
262
263 void JSCustomElementInterface::invokeDisconnectedCallback(Element& element)
264 {
265     invokeCallback(element, m_disconnectedCallback.get());
266 }
267
268 void JSCustomElementInterface::setAdoptedCallback(JSC::JSObject* callback)
269 {
270     m_adoptedCallback = callback;
271 }
272
273 void JSCustomElementInterface::invokeAdoptedCallback(Element& element, Document& oldDocument, Document& newDocument)
274 {
275     invokeCallback(element, m_adoptedCallback.get(), [&](ExecState* state, JSDOMGlobalObject* globalObject, MarkedArgumentBuffer& args) {
276         args.append(toJS(state, globalObject, oldDocument));
277         args.append(toJS(state, globalObject, newDocument));
278     });
279 }
280
281 void JSCustomElementInterface::setAttributeChangedCallback(JSC::JSObject* callback, const Vector<String>& observedAttributes)
282 {
283     m_attributeChangedCallback = callback;
284     m_observedAttributes.clear();
285     for (auto& name : observedAttributes)
286         m_observedAttributes.add(name);
287 }
288
289 void JSCustomElementInterface::invokeAttributeChangedCallback(Element& element, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
290 {
291     invokeCallback(element, m_attributeChangedCallback.get(), [&](ExecState* state, JSDOMGlobalObject*, MarkedArgumentBuffer& args) {
292         args.append(jsStringWithCache(state, attributeName.localName()));
293         args.append(jsStringOrNull(state, oldValue));
294         args.append(jsStringOrNull(state, newValue));
295         args.append(jsStringOrNull(state, attributeName.namespaceURI()));
296     });
297 }
298
299 void JSCustomElementInterface::didUpgradeLastElementInConstructionStack()
300 {
301     m_constructionStack.last() = nullptr;
302 }
303
304 } // namespace WebCore
305
306 #endif