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