1/*
2 * Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "CustomElementReactionQueue.h"
28
29#include "CustomElementRegistry.h"
30#include "DOMWindow.h"
31#include "Document.h"
32#include "Element.h"
33#include "HTMLNames.h"
34#include "JSCustomElementInterface.h"
35#include "JSDOMBinding.h"
36#include "Microtasks.h"
37#include <JavaScriptCore/CatchScope.h>
38#include <JavaScriptCore/Heap.h>
39#include <wtf/NeverDestroyed.h>
40#include <wtf/Optional.h>
41#include <wtf/Ref.h>
42#include <wtf/SetForScope.h>
43
44namespace WebCore {
45
46class CustomElementReactionQueueItem {
47public:
48 enum class Type {
49 ElementUpgrade,
50 Connected,
51 Disconnected,
52 Adopted,
53 AttributeChanged,
54 };
55
56 CustomElementReactionQueueItem(Type type)
57 : m_type(type)
58 { }
59
60 CustomElementReactionQueueItem(Document& oldDocument, Document& newDocument)
61 : m_type(Type::Adopted)
62 , m_oldDocument(&oldDocument)
63 , m_newDocument(&newDocument)
64 { }
65
66 CustomElementReactionQueueItem(const QualifiedName& attributeName, const AtomString& oldValue, const AtomString& newValue)
67 : m_type(Type::AttributeChanged)
68 , m_attributeName(attributeName)
69 , m_oldValue(oldValue)
70 , m_newValue(newValue)
71 { }
72
73 Type type() const { return m_type; }
74
75 void invoke(Element& element, JSCustomElementInterface& elementInterface)
76 {
77 switch (m_type) {
78 case Type::ElementUpgrade:
79 elementInterface.upgradeElement(element);
80 break;
81 case Type::Connected:
82 elementInterface.invokeConnectedCallback(element);
83 break;
84 case Type::Disconnected:
85 elementInterface.invokeDisconnectedCallback(element);
86 break;
87 case Type::Adopted:
88 elementInterface.invokeAdoptedCallback(element, *m_oldDocument, *m_newDocument);
89 break;
90 case Type::AttributeChanged:
91 ASSERT(m_attributeName);
92 elementInterface.invokeAttributeChangedCallback(element, m_attributeName.value(), m_oldValue, m_newValue);
93 break;
94 }
95 }
96
97private:
98 Type m_type;
99 RefPtr<Document> m_oldDocument;
100 RefPtr<Document> m_newDocument;
101 Optional<QualifiedName> m_attributeName;
102 AtomString m_oldValue;
103 AtomString m_newValue;
104};
105
106CustomElementReactionQueue::CustomElementReactionQueue(JSCustomElementInterface& elementInterface)
107 : m_interface(elementInterface)
108{ }
109
110CustomElementReactionQueue::~CustomElementReactionQueue()
111{
112 ASSERT(m_items.isEmpty());
113}
114
115void CustomElementReactionQueue::clear()
116{
117 m_items.clear();
118}
119
120void CustomElementReactionQueue::enqueueElementUpgrade(Element& element, bool alreadyScheduledToUpgrade)
121{
122 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
123 ASSERT(element.reactionQueue());
124 auto& queue = *element.reactionQueue();
125 if (alreadyScheduledToUpgrade) {
126 ASSERT(queue.m_items.size() == 1);
127 ASSERT(queue.m_items[0].type() == CustomElementReactionQueueItem::Type::ElementUpgrade);
128 } else {
129 queue.m_items.append({CustomElementReactionQueueItem::Type::ElementUpgrade});
130 enqueueElementOnAppropriateElementQueue(element);
131 }
132}
133
134void CustomElementReactionQueue::enqueueElementUpgradeIfDefined(Element& element)
135{
136 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
137 ASSERT(element.isCustomElementUpgradeCandidate());
138 auto* window = element.document().domWindow();
139 if (!window)
140 return;
141
142 auto* registry = window->customElementRegistry();
143 if (!registry)
144 return;
145
146 auto* elementInterface = registry->findInterface(element);
147 if (!elementInterface)
148 return;
149
150 element.enqueueToUpgrade(*elementInterface);
151}
152
153void CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded(Element& element)
154{
155 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
156 ASSERT(element.isDefinedCustomElement());
157 ASSERT(element.document().refCount() > 0);
158 ASSERT(element.reactionQueue());
159 auto& queue = *element.reactionQueue();
160 if (!queue.m_interface->hasConnectedCallback())
161 return;
162 queue.m_items.append({CustomElementReactionQueueItem::Type::Connected});
163 enqueueElementOnAppropriateElementQueue(element);
164}
165
166void CustomElementReactionQueue::enqueueDisconnectedCallbackIfNeeded(Element& element)
167{
168 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
169 ASSERT(element.isDefinedCustomElement());
170 if (element.document().refCount() <= 0)
171 return; // Don't enqueue disconnectedCallback if the entire document is getting destructed.
172 ASSERT(element.reactionQueue());
173 auto& queue = *element.reactionQueue();
174 if (!queue.m_interface->hasDisconnectedCallback())
175 return;
176 queue.m_items.append({CustomElementReactionQueueItem::Type::Disconnected});
177 enqueueElementOnAppropriateElementQueue(element);
178}
179
180void CustomElementReactionQueue::enqueueAdoptedCallbackIfNeeded(Element& element, Document& oldDocument, Document& newDocument)
181{
182 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
183 ASSERT(element.isDefinedCustomElement());
184 ASSERT(element.document().refCount() > 0);
185 ASSERT(element.reactionQueue());
186 auto& queue = *element.reactionQueue();
187 if (!queue.m_interface->hasAdoptedCallback())
188 return;
189 queue.m_items.append({oldDocument, newDocument});
190 enqueueElementOnAppropriateElementQueue(element);
191}
192
193void CustomElementReactionQueue::enqueueAttributeChangedCallbackIfNeeded(Element& element, const QualifiedName& attributeName, const AtomString& oldValue, const AtomString& newValue)
194{
195 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
196 ASSERT(element.isDefinedCustomElement());
197 ASSERT(element.document().refCount() > 0);
198 ASSERT(element.reactionQueue());
199 auto& queue = *element.reactionQueue();
200 if (!queue.m_interface->observesAttribute(attributeName.localName()))
201 return;
202 queue.m_items.append({attributeName, oldValue, newValue});
203 enqueueElementOnAppropriateElementQueue(element);
204}
205
206void CustomElementReactionQueue::enqueuePostUpgradeReactions(Element& element)
207{
208 ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
209 ASSERT(element.isCustomElementUpgradeCandidate());
210 if (!element.hasAttributes() && !element.isConnected())
211 return;
212
213 ASSERT(element.reactionQueue());
214 auto& queue = *element.reactionQueue();
215
216 if (element.hasAttributes()) {
217 for (auto& attribute : element.attributesIterator()) {
218 if (queue.m_interface->observesAttribute(attribute.localName()))
219 queue.m_items.append({attribute.name(), nullAtom(), attribute.value()});
220 }
221 }
222
223 if (element.isConnected() && queue.m_interface->hasConnectedCallback())
224 queue.m_items.append({CustomElementReactionQueueItem::Type::Connected});
225}
226
227bool CustomElementReactionQueue::observesStyleAttribute() const
228{
229 return m_interface->observesAttribute(HTMLNames::styleAttr->localName());
230}
231
232void CustomElementReactionQueue::invokeAll(Element& element)
233{
234 while (!m_items.isEmpty()) {
235 Vector<CustomElementReactionQueueItem> items = WTFMove(m_items);
236 for (auto& item : items)
237 item.invoke(element, m_interface.get());
238 }
239}
240
241inline void CustomElementReactionQueue::ElementQueue::add(Element& element)
242{
243 ASSERT(!m_invoking);
244 // FIXME: Avoid inserting the same element multiple times.
245 m_elements.append(element);
246}
247
248inline void CustomElementReactionQueue::ElementQueue::invokeAll()
249{
250 RELEASE_ASSERT(!m_invoking);
251 SetForScope<bool> invoking(m_invoking, true);
252 unsigned originalSize = m_elements.size();
253 // It's possible for more elements to be enqueued if some IDL attributes were missing CEReactions.
254 // Invoke callbacks slightly later here instead of crashing / ignoring those cases.
255 for (unsigned i = 0; i < m_elements.size(); ++i) {
256 auto& element = m_elements[i].get();
257 auto* queue = element.reactionQueue();
258 ASSERT(queue);
259 queue->invokeAll(element);
260 }
261 ASSERT_UNUSED(originalSize, m_elements.size() == originalSize);
262 m_elements.clear();
263}
264
265inline void CustomElementReactionQueue::ElementQueue::processQueue(JSC::ExecState* state)
266{
267 if (!state) {
268 invokeAll();
269 return;
270 }
271
272 auto& vm = state->vm();
273 JSC::JSLockHolder lock(vm);
274
275 JSC::Exception* previousException = nullptr;
276 {
277 auto catchScope = DECLARE_CATCH_SCOPE(vm);
278 previousException = catchScope.exception();
279 if (previousException)
280 catchScope.clearException();
281 }
282
283 invokeAll();
284
285 if (previousException) {
286 auto throwScope = DECLARE_THROW_SCOPE(vm);
287 throwException(state, throwScope, previousException);
288 }
289}
290
291// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-an-element-on-the-appropriate-element-queue
292void CustomElementReactionQueue::enqueueElementOnAppropriateElementQueue(Element& element)
293{
294 ASSERT(element.reactionQueue());
295 if (!CustomElementReactionStack::s_currentProcessingStack) {
296 auto& queue = ensureBackupQueue();
297 queue.add(element);
298 return;
299 }
300
301 auto*& queue = CustomElementReactionStack::s_currentProcessingStack->m_queue;
302 if (!queue) // We use a raw pointer to avoid genearing code to delete it in ~CustomElementReactionStack.
303 queue = new ElementQueue;
304 queue->add(element);
305}
306
307#if !ASSERT_DISABLED
308unsigned CustomElementReactionDisallowedScope::s_customElementReactionDisallowedCount = 0;
309#endif
310
311CustomElementReactionStack* CustomElementReactionStack::s_currentProcessingStack = nullptr;
312
313void CustomElementReactionStack::processQueue(JSC::ExecState* state)
314{
315 ASSERT(m_queue);
316 m_queue->processQueue(state);
317 delete m_queue;
318 m_queue = nullptr;
319}
320
321class BackupElementQueueMicrotask final : public Microtask {
322 WTF_MAKE_FAST_ALLOCATED;
323private:
324 Result run() final
325 {
326 CustomElementReactionQueue::processBackupQueue();
327 return Result::Done;
328 }
329};
330
331static bool s_processingBackupElementQueue = false;
332
333CustomElementReactionQueue::ElementQueue& CustomElementReactionQueue::ensureBackupQueue()
334{
335 if (!s_processingBackupElementQueue) {
336 s_processingBackupElementQueue = true;
337 MicrotaskQueue::mainThreadQueue().append(std::make_unique<BackupElementQueueMicrotask>());
338 }
339 return backupElementQueue();
340}
341
342void CustomElementReactionQueue::processBackupQueue()
343{
344 backupElementQueue().processQueue(nullptr);
345 s_processingBackupElementQueue = false;
346}
347
348CustomElementReactionQueue::ElementQueue& CustomElementReactionQueue::backupElementQueue()
349{
350 static NeverDestroyed<ElementQueue> queue;
351 return queue.get();
352}
353
354}
355