Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #ifndef mozilla_dom_CustomElementRegistry_h
8 : #define mozilla_dom_CustomElementRegistry_h
9 :
10 : #include "js/GCHashTable.h"
11 : #include "js/TypeDecls.h"
12 : #include "mozilla/Attributes.h"
13 : #include "mozilla/CycleCollectedJSContext.h" // for MicroTaskRunnable
14 : #include "mozilla/ErrorResult.h"
15 : #include "mozilla/dom/BindingDeclarations.h"
16 : #include "mozilla/dom/CustomElementRegistryBinding.h"
17 : #include "mozilla/dom/Element.h"
18 : #include "mozilla/dom/FunctionBinding.h"
19 : #include "mozilla/dom/WebComponentsBinding.h"
20 : #include "nsCycleCollectionParticipant.h"
21 : #include "nsGenericHTMLElement.h"
22 : #include "nsWrapperCache.h"
23 : #include "nsContentUtils.h"
24 :
25 : class nsDocument;
26 :
27 : namespace mozilla {
28 : namespace dom {
29 :
30 : struct CustomElementData;
31 : struct ElementDefinitionOptions;
32 : class CallbackFunction;
33 : class CustomElementReaction;
34 : class DocGroup;
35 : class Function;
36 : class Promise;
37 :
38 0 : struct LifecycleCallbackArgs
39 : {
40 : nsString name;
41 : nsString oldValue;
42 : nsString newValue;
43 : nsString namespaceURI;
44 : };
45 :
46 : struct LifecycleAdoptedCallbackArgs
47 : {
48 : nsCOMPtr<nsIDocument> mOldDocument;
49 : nsCOMPtr<nsIDocument> mNewDocument;
50 : };
51 :
52 : class CustomElementCallback
53 : {
54 : public:
55 : CustomElementCallback(Element* aThisObject,
56 : nsIDocument::ElementCallbackType aCallbackType,
57 : CallbackFunction* aCallback);
58 : void Traverse(nsCycleCollectionTraversalCallback& aCb) const;
59 : void Call();
60 : void SetArgs(LifecycleCallbackArgs& aArgs)
61 : {
62 : MOZ_ASSERT(mType == nsIDocument::eAttributeChanged,
63 : "Arguments are only used by attribute changed callback.");
64 : mArgs = aArgs;
65 : }
66 :
67 : void SetAdoptedCallbackArgs(LifecycleAdoptedCallbackArgs& aAdoptedCallbackArgs)
68 : {
69 : MOZ_ASSERT(mType == nsIDocument::eAdopted,
70 : "Arguments are only used by adopted callback.");
71 : mAdoptedCallbackArgs = aAdoptedCallbackArgs;
72 : }
73 :
74 : private:
75 : // The this value to use for invocation of the callback.
76 : RefPtr<Element> mThisObject;
77 : RefPtr<CallbackFunction> mCallback;
78 : // The type of callback (eCreated, eAttached, etc.)
79 : nsIDocument::ElementCallbackType mType;
80 : // Arguments to be passed to the callback,
81 : // used by the attribute changed callback.
82 : LifecycleCallbackArgs mArgs;
83 : LifecycleAdoptedCallbackArgs mAdoptedCallbackArgs;
84 : };
85 :
86 0 : class CustomElementConstructor final : public CallbackFunction
87 : {
88 : public:
89 : explicit CustomElementConstructor(CallbackFunction* aOther)
90 : : CallbackFunction(aOther)
91 : {
92 : MOZ_ASSERT(JS::IsConstructor(mCallback));
93 : }
94 :
95 : already_AddRefed<Element> Construct(const char* aExecutionReason, ErrorResult& aRv);
96 : };
97 :
98 : // Each custom element has an associated callback queue and an element is
99 : // being created flag.
100 : struct CustomElementData
101 : {
102 0 : NS_INLINE_DECL_REFCOUNTING(CustomElementData)
103 :
104 : // https://dom.spec.whatwg.org/#concept-element-custom-element-state
105 : // CustomElementData is only created on the element which is a custom element
106 : // or an upgrade candidate, so the state of an element without
107 : // CustomElementData is "uncustomized".
108 : enum class State {
109 : eUndefined,
110 : eFailed,
111 : eCustom
112 : };
113 :
114 : explicit CustomElementData(nsAtom* aType);
115 : CustomElementData(nsAtom* aType, State aState);
116 :
117 : // Custom element state as described in the custom element spec.
118 : State mState;
119 : // custom element reaction queue as described in the custom element spec.
120 : // There is 1 reaction in reaction queue, when 1) it becomes disconnected,
121 : // 2) it’s adopted into a new document, 3) its attributes are changed,
122 : // appended, removed, or replaced.
123 : // There are 3 reactions in reaction queue when doing upgrade operation,
124 : // e.g., create an element, insert a node.
125 : AutoTArray<UniquePtr<CustomElementReaction>, 3> mReactionQueue;
126 :
127 : void SetCustomElementDefinition(CustomElementDefinition* aDefinition);
128 : CustomElementDefinition* GetCustomElementDefinition();
129 : nsAtom* GetCustomElementType();
130 :
131 : void Traverse(nsCycleCollectionTraversalCallback& aCb) const;
132 : void Unlink();
133 :
134 : private:
135 0 : virtual ~CustomElementData() {}
136 :
137 : // Custom element type, for <button is="x-button"> or <x-button>
138 : // this would be x-button.
139 : RefPtr<nsAtom> mType;
140 : RefPtr<CustomElementDefinition> mCustomElementDefinition;
141 : };
142 :
143 : #define ALREADY_CONSTRUCTED_MARKER nullptr
144 :
145 : // The required information for a custom element as defined in:
146 : // https://html.spec.whatwg.org/multipage/scripting.html#custom-element-definition
147 : struct CustomElementDefinition
148 : {
149 : NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CustomElementDefinition)
150 0 : NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CustomElementDefinition)
151 :
152 : CustomElementDefinition(nsAtom* aType,
153 : nsAtom* aLocalName,
154 : Function* aConstructor,
155 : nsTArray<RefPtr<nsAtom>>&& aObservedAttributes,
156 : UniquePtr<LifecycleCallbacks>&& aCallbacks);
157 :
158 : // The type (name) for this custom element, for <button is="x-foo"> or <x-foo>
159 : // this would be x-foo.
160 : RefPtr<nsAtom> mType;
161 :
162 : // The localname to (e.g. <button is=type> -- this would be button).
163 : RefPtr<nsAtom> mLocalName;
164 :
165 : // The custom element constructor.
166 : RefPtr<CustomElementConstructor> mConstructor;
167 :
168 : // The list of attributes that this custom element observes.
169 : nsTArray<RefPtr<nsAtom>> mObservedAttributes;
170 :
171 : // The lifecycle callbacks to call for this custom element.
172 : UniquePtr<LifecycleCallbacks> mCallbacks;
173 :
174 : // A construction stack. Use nullptr to represent an "already constructed marker".
175 : nsTArray<RefPtr<Element>> mConstructionStack;
176 :
177 : bool IsCustomBuiltIn()
178 : {
179 156 : return mType != mLocalName;
180 : }
181 :
182 2 : bool IsInObservedAttributeList(nsAtom* aName)
183 : {
184 2 : if (mObservedAttributes.IsEmpty()) {
185 : return false;
186 : }
187 :
188 0 : return mObservedAttributes.Contains(aName);
189 : }
190 :
191 : private:
192 : ~CustomElementDefinition() {}
193 : };
194 :
195 : class CustomElementReaction
196 : {
197 : public:
198 : virtual ~CustomElementReaction() = default;
199 : virtual void Invoke(Element* aElement, ErrorResult& aRv) = 0;
200 : virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const = 0;
201 :
202 : bool IsUpgradeReaction()
203 : {
204 : return mIsUpgradeReaction;
205 : }
206 :
207 : protected:
208 : bool mIsUpgradeReaction = false;
209 : };
210 :
211 : // https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reactions-stack
212 : class CustomElementReactionsStack
213 : {
214 : public:
215 2 : NS_INLINE_DECL_REFCOUNTING(CustomElementReactionsStack)
216 :
217 2 : CustomElementReactionsStack()
218 2 : : mIsBackupQueueProcessing(false)
219 : , mRecursionDepth(0)
220 2 : , mIsElementQueuePushedForCurrentRecursionDepth(false)
221 : {
222 2 : }
223 :
224 : // Hold a strong reference of Element so that it does not get cycle collected
225 : // before the reactions in its reaction queue are invoked.
226 : // The element reaction queues are stored in CustomElementData.
227 : // We need to lookup ElementReactionQueueMap again to get relevant reaction queue.
228 : // The choice of 3 for the auto size here is based on running Custom Elements
229 : // wpt tests.
230 : typedef AutoTArray<RefPtr<Element>, 3> ElementQueue;
231 :
232 : /**
233 : * Enqueue a custom element upgrade reaction
234 : * https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-upgrade-reaction
235 : */
236 : void EnqueueUpgradeReaction(Element* aElement,
237 : CustomElementDefinition* aDefinition);
238 :
239 : /**
240 : * Enqueue a custom element callback reaction
241 : * https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-callback-reaction
242 : */
243 : void EnqueueCallbackReaction(Element* aElement,
244 : UniquePtr<CustomElementCallback> aCustomElementCallback);
245 :
246 : /**
247 : * [CEReactions] Before executing the algorithm's steps.
248 : * Increase the current recursion depth, and the element queue is pushed
249 : * lazily when we really enqueue reactions.
250 : *
251 : * @return true if the element queue is pushed for "previous" recursion depth.
252 : */
253 : bool EnterCEReactions()
254 : {
255 0 : bool temp = mIsElementQueuePushedForCurrentRecursionDepth;
256 0 : mRecursionDepth++;
257 : // The is-element-queue-pushed flag is initially false when entering a new
258 : // recursion level. The original value will be cached in AutoCEReaction
259 : // and restored after leaving this recursion level.
260 0 : mIsElementQueuePushedForCurrentRecursionDepth = false;
261 : return temp;
262 : }
263 :
264 : /**
265 : * [CEReactions] After executing the algorithm's steps.
266 : * Pop and invoke the element queue if it is created and pushed for current
267 : * recursion depth, then decrease the current recursion depth.
268 : *
269 : * @param aCx JSContext used for handling exception thrown by algorithm's
270 : * steps, this could be a nullptr.
271 : * aWasElementQueuePushed used for restoring status after leaving
272 : * current recursion.
273 : */
274 2 : void LeaveCEReactions(JSContext* aCx, bool aWasElementQueuePushed)
275 : {
276 2 : MOZ_ASSERT(mRecursionDepth);
277 :
278 2 : if (mIsElementQueuePushedForCurrentRecursionDepth) {
279 0 : Maybe<JS::AutoSaveExceptionState> ases;
280 0 : if (aCx) {
281 0 : ases.emplace(aCx);
282 : }
283 0 : PopAndInvokeElementQueue();
284 : }
285 2 : mRecursionDepth--;
286 : // Restore the is-element-queue-pushed flag cached in AutoCEReaction when
287 : // leaving the recursion level.
288 2 : mIsElementQueuePushedForCurrentRecursionDepth = aWasElementQueuePushed;
289 :
290 2 : MOZ_ASSERT_IF(!mRecursionDepth, mReactionsStack.IsEmpty());
291 2 : }
292 :
293 : private:
294 0 : ~CustomElementReactionsStack() {};
295 :
296 : /**
297 : * Push a new element queue onto the custom element reactions stack.
298 : */
299 : void CreateAndPushElementQueue();
300 :
301 : /**
302 : * Pop the element queue from the custom element reactions stack, and invoke
303 : * custom element reactions in that queue.
304 : */
305 : void PopAndInvokeElementQueue();
306 :
307 : // The choice of 8 for the auto size here is based on gut feeling.
308 : AutoTArray<UniquePtr<ElementQueue>, 8> mReactionsStack;
309 : ElementQueue mBackupQueue;
310 : // https://html.spec.whatwg.org/#enqueue-an-element-on-the-appropriate-element-queue
311 : bool mIsBackupQueueProcessing;
312 :
313 : void InvokeBackupQueue();
314 :
315 : /**
316 : * Invoke custom element reactions
317 : * https://html.spec.whatwg.org/multipage/scripting.html#invoke-custom-element-reactions
318 : */
319 : void InvokeReactions(ElementQueue* aElementQueue, nsIGlobalObject* aGlobal);
320 :
321 : void Enqueue(Element* aElement, CustomElementReaction* aReaction);
322 :
323 : // Current [CEReactions] recursion depth.
324 : uint32_t mRecursionDepth;
325 : // True if the element queue is pushed into reaction stack for current
326 : // recursion depth. This will be cached in AutoCEReaction when entering a new
327 : // CEReaction recursion and restored after leaving the recursion.
328 : bool mIsElementQueuePushedForCurrentRecursionDepth;
329 :
330 : private:
331 0 : class BackupQueueMicroTask final : public mozilla::MicroTaskRunnable {
332 : public:
333 : explicit BackupQueueMicroTask(
334 : CustomElementReactionsStack* aReactionStack)
335 : : MicroTaskRunnable()
336 : , mReactionStack(aReactionStack)
337 : {
338 : MOZ_ASSERT(!mReactionStack->mIsBackupQueueProcessing,
339 : "mIsBackupQueueProcessing should be initially false");
340 : mReactionStack->mIsBackupQueueProcessing = true;
341 : }
342 :
343 0 : virtual void Run(AutoSlowOperation& aAso) override
344 : {
345 0 : mReactionStack->InvokeBackupQueue();
346 0 : mReactionStack->mIsBackupQueueProcessing = false;
347 0 : }
348 :
349 : private:
350 : RefPtr<CustomElementReactionsStack> mReactionStack;
351 : };
352 : };
353 :
354 : class CustomElementRegistry final : public nsISupports,
355 : public nsWrapperCache
356 : {
357 : // Allow nsDocument to access mCustomDefinitions and mCandidatesMap.
358 : friend class ::nsDocument;
359 :
360 : public:
361 : NS_DECL_CYCLE_COLLECTING_ISUPPORTS
362 : NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CustomElementRegistry)
363 :
364 : public:
365 : static bool IsCustomElementEnabled(JSContext* aCx, JSObject* aObject);
366 : static bool IsCustomElementEnabled(nsIDocument* aDoc);
367 :
368 : explicit CustomElementRegistry(nsPIDOMWindowInner* aWindow);
369 :
370 : private:
371 : class RunCustomElementCreationCallback : public mozilla::Runnable
372 : {
373 : public:
374 : NS_DECL_NSIRUNNABLE
375 : explicit RunCustomElementCreationCallback(CustomElementRegistry* aRegistry,
376 : nsAtom* aAtom,
377 : CustomElementCreationCallback* aCallback)
378 : : mozilla::Runnable("CustomElementRegistry::RunCustomElementCreationCallback")
379 : , mRegistry(aRegistry)
380 : , mAtom(aAtom)
381 : , mCallback(aCallback)
382 : {
383 : }
384 : private:
385 : RefPtr<CustomElementRegistry> mRegistry;
386 : RefPtr<nsAtom> mAtom;
387 : RefPtr<CustomElementCreationCallback> mCallback;
388 : };
389 :
390 : public:
391 : /**
392 : * Looking up a custom element definition.
393 : * https://html.spec.whatwg.org/#look-up-a-custom-element-definition
394 : */
395 : CustomElementDefinition* LookupCustomElementDefinition(
396 : nsAtom* aNameAtom, nsAtom* aTypeAtom);
397 :
398 : CustomElementDefinition* LookupCustomElementDefinition(
399 : JSContext* aCx, JSObject *aConstructor) const;
400 :
401 : static void EnqueueLifecycleCallback(nsIDocument::ElementCallbackType aType,
402 : Element* aCustomElement,
403 : LifecycleCallbackArgs* aArgs,
404 : LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
405 : CustomElementDefinition* aDefinition);
406 :
407 : /**
408 : * Upgrade an element.
409 : * https://html.spec.whatwg.org/multipage/scripting.html#upgrades
410 : */
411 : static void Upgrade(Element* aElement, CustomElementDefinition* aDefinition, ErrorResult& aRv);
412 :
413 : /**
414 : * Registers an unresolved custom element that is a candidate for
415 : * upgrade. |aTypeName| is the name of the custom element type, if it is not
416 : * provided, then element name is used. |aTypeName| should be provided
417 : * when registering a custom element that extends an existing
418 : * element. e.g. <button is="x-button">.
419 : */
420 : void RegisterUnresolvedElement(Element* aElement,
421 : nsAtom* aTypeName = nullptr);
422 :
423 : /**
424 : * Unregister an unresolved custom element that is a candidate for
425 : * upgrade when a custom element is removed from tree.
426 : */
427 : void UnregisterUnresolvedElement(Element* aElement,
428 : nsAtom* aTypeName = nullptr);
429 :
430 : /**
431 : * Register an element to be upgraded when the custom element creation
432 : * callback is executed.
433 : *
434 : * To be used when LookupCustomElementDefinition() didn't return a definition,
435 : * but with the callback scheduled to be run.
436 : */
437 2680 : inline void RegisterCallbackUpgradeElement(Element* aElement,
438 : nsAtom* aTypeName = nullptr)
439 : {
440 5360 : if (mElementCreationCallbacksUpgradeCandidatesMap.IsEmpty()) {
441 2680 : return;
442 : }
443 :
444 0 : RefPtr<nsAtom> typeName = aTypeName;
445 0 : if (!typeName) {
446 0 : typeName = aElement->NodeInfo()->NameAtom();
447 : }
448 :
449 : nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>* elements =
450 0 : mElementCreationCallbacksUpgradeCandidatesMap.Get(typeName);
451 :
452 : // If there isn't a table, there won't be a definition added by the callback.
453 0 : if (!elements) {
454 0 : return;
455 : }
456 :
457 0 : nsWeakPtr elem = do_GetWeakReference(aElement);
458 0 : elements->PutEntry(elem);
459 : }
460 :
461 : private:
462 : ~CustomElementRegistry();
463 :
464 : static UniquePtr<CustomElementCallback> CreateCustomElementCallback(
465 : nsIDocument::ElementCallbackType aType, Element* aCustomElement,
466 : LifecycleCallbackArgs* aArgs,
467 : LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
468 : CustomElementDefinition* aDefinition);
469 :
470 : void UpgradeCandidates(nsAtom* aKey,
471 : CustomElementDefinition* aDefinition,
472 : ErrorResult& aRv);
473 :
474 : typedef nsRefPtrHashtable<nsRefPtrHashKey<nsAtom>, CustomElementDefinition>
475 : DefinitionMap;
476 : typedef nsRefPtrHashtable<nsRefPtrHashKey<nsAtom>, CustomElementCreationCallback>
477 : ElementCreationCallbackMap;
478 : typedef nsClassHashtable<nsRefPtrHashKey<nsAtom>,
479 : nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>>
480 : CandidateMap;
481 : typedef JS::GCHashMap<JS::Heap<JSObject*>,
482 : RefPtr<nsAtom>,
483 : js::MovableCellHasher<JS::Heap<JSObject*>>,
484 : js::SystemAllocPolicy> ConstructorMap;
485 :
486 : // Hashtable for custom element definitions in web components.
487 : // Custom prototypes are stored in the compartment where definition was
488 : // defined.
489 : DefinitionMap mCustomDefinitions;
490 :
491 : // Hashtable for chrome-only callbacks that is called *before* we return
492 : // a CustomElementDefinition, when the typeAtom matches.
493 : // The callbacks are registered with the setElementCreationCallback method.
494 : ElementCreationCallbackMap mElementCreationCallbacks;
495 :
496 : // Hashtable for looking up definitions by using constructor as key.
497 : // Custom elements' name are stored here and we need to lookup
498 : // mCustomDefinitions again to get definitions.
499 : ConstructorMap mConstructors;
500 :
501 : typedef nsRefPtrHashtable<nsRefPtrHashKey<nsAtom>, Promise>
502 : WhenDefinedPromiseMap;
503 : WhenDefinedPromiseMap mWhenDefinedPromiseMap;
504 :
505 : // The "upgrade candidates map" from the web components spec. Maps from a
506 : // namespace id and local name to a list of elements to upgrade if that
507 : // element is registered as a custom element.
508 : CandidateMap mCandidatesMap;
509 :
510 : // If an element creation callback is found, the nsTHashtable for the
511 : // type is created here, and elements will later be upgraded.
512 : CandidateMap mElementCreationCallbacksUpgradeCandidatesMap;
513 :
514 : nsCOMPtr<nsPIDOMWindowInner> mWindow;
515 :
516 : // It is used to prevent reentrant invocations of element definition.
517 : bool mIsCustomDefinitionRunning;
518 :
519 : private:
520 : class MOZ_RAII AutoSetRunningFlag final {
521 : public:
522 : explicit AutoSetRunningFlag(CustomElementRegistry* aRegistry)
523 : : mRegistry(aRegistry)
524 : {
525 : MOZ_ASSERT(!mRegistry->mIsCustomDefinitionRunning,
526 : "IsCustomDefinitionRunning flag should be initially false");
527 : mRegistry->mIsCustomDefinitionRunning = true;
528 : }
529 :
530 : ~AutoSetRunningFlag() {
531 : mRegistry->mIsCustomDefinitionRunning = false;
532 : }
533 :
534 : private:
535 : CustomElementRegistry* mRegistry;
536 : };
537 :
538 : public:
539 : nsISupports* GetParentObject() const;
540 :
541 : DocGroup* GetDocGroup() const;
542 :
543 : virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
544 :
545 : void Define(JSContext* aCx, const nsAString& aName,
546 : Function& aFunctionConstructor,
547 : const ElementDefinitionOptions& aOptions, ErrorResult& aRv);
548 :
549 : void Get(JSContext* cx, const nsAString& name,
550 : JS::MutableHandle<JS::Value> aRetVal);
551 :
552 : already_AddRefed<Promise> WhenDefined(const nsAString& aName, ErrorResult& aRv);
553 :
554 : // Chrome-only method that give JS an opportunity to only load the custom
555 : // element definition script when needed.
556 : void SetElementCreationCallback(const nsAString& aName, CustomElementCreationCallback& aCallback, ErrorResult& aRv);
557 : };
558 :
559 : class MOZ_RAII AutoCEReaction final {
560 : public:
561 : // JSContext is allowed to be a nullptr if we are guaranteeing that we're
562 : // not doing something that might throw but not finish reporting a JS
563 : // exception during the lifetime of the AutoCEReaction.
564 0 : AutoCEReaction(CustomElementReactionsStack* aReactionsStack, JSContext* aCx)
565 0 : : mReactionsStack(aReactionsStack)
566 0 : , mCx(aCx)
567 : {
568 0 : mIsElementQueuePushedForPreviousRecursionDepth =
569 0 : mReactionsStack->EnterCEReactions();
570 0 : }
571 :
572 2 : ~AutoCEReaction()
573 2 : {
574 2 : mReactionsStack->LeaveCEReactions(
575 2 : mCx, mIsElementQueuePushedForPreviousRecursionDepth);
576 : }
577 :
578 : private:
579 : RefPtr<CustomElementReactionsStack> mReactionsStack;
580 : JSContext* mCx;
581 : bool mIsElementQueuePushedForPreviousRecursionDepth;
582 : };
583 :
584 : } // namespace dom
585 : } // namespace mozilla
586 :
587 :
588 : #endif // mozilla_dom_CustomElementRegistry_h
|