/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef mozilla_dom_CustomElementRegistry_h #define mozilla_dom_CustomElementRegistry_h #include "js/GCHashTable.h" #include "js/TypeDecls.h" #include "mozilla/Attributes.h" #include "mozilla/ErrorResult.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/FunctionBinding.h" #include "mozilla/dom/WebComponentsBinding.h" #include "nsCycleCollectionParticipant.h" #include "nsGenericHTMLElement.h" #include "nsWrapperCache.h" class nsDocument; namespace mozilla { namespace dom { struct CustomElementData; struct ElementDefinitionOptions; class CallbackFunction; class CustomElementReaction; class DocGroup; class Function; class Promise; struct LifecycleCallbackArgs { nsString name; nsString oldValue; nsString newValue; nsString namespaceURI; }; struct LifecycleAdoptedCallbackArgs { nsCOMPtr<nsIDocument> mOldDocument; nsCOMPtr<nsIDocument> mNewDocument; }; class CustomElementCallback { public: CustomElementCallback(Element* aThisObject, nsIDocument::ElementCallbackType aCallbackType, CallbackFunction* aCallback); void Traverse(nsCycleCollectionTraversalCallback& aCb) const; void Call(); void SetArgs(LifecycleCallbackArgs& aArgs) { MOZ_ASSERT(mType == nsIDocument::eAttributeChanged, "Arguments are only used by attribute changed callback."); mArgs = aArgs; } void SetAdoptedCallbackArgs(LifecycleAdoptedCallbackArgs& aAdoptedCallbackArgs) { MOZ_ASSERT(mType == nsIDocument::eAdopted, "Arguments are only used by adopted callback."); mAdoptedCallbackArgs = aAdoptedCallbackArgs; } private: // The this value to use for invocation of the callback. RefPtr<Element> mThisObject; RefPtr<CallbackFunction> mCallback; // The type of callback (eCreated, eAttached, etc.) nsIDocument::ElementCallbackType mType; // Arguments to be passed to the callback, // used by the attribute changed callback. LifecycleCallbackArgs mArgs; LifecycleAdoptedCallbackArgs mAdoptedCallbackArgs; }; class CustomElementConstructor final : public CallbackFunction { public: explicit CustomElementConstructor(CallbackFunction* aOther) : CallbackFunction(aOther) { MOZ_ASSERT(JS::IsConstructor(mCallback)); } already_AddRefed<Element> Construct(const char* aExecutionReason, ErrorResult& aRv); }; // Each custom element has an associated callback queue and an element is // being created flag. struct CustomElementData { NS_INLINE_DECL_REFCOUNTING(CustomElementData) // https://dom.spec.whatwg.org/#concept-element-custom-element-state // CustomElementData is only created on the element which is a custom element // or an upgrade candidate, so the state of an element without // CustomElementData is "uncustomized". enum class State { eUndefined, eFailed, eCustom }; explicit CustomElementData(nsIAtom* aType); CustomElementData(nsIAtom* aType, State aState); // Custom element state as described in the custom element spec. State mState; // custom element reaction queue as described in the custom element spec. // There is 1 reaction in reaction queue, when 1) it becomes disconnected, // 2) it’s adopted into a new document, 3) its attributes are changed, // appended, removed, or replaced. // There are 3 reactions in reaction queue when doing upgrade operation, // e.g., create an element, insert a node. AutoTArray<UniquePtr<CustomElementReaction>, 3> mReactionQueue; void SetCustomElementDefinition(CustomElementDefinition* aDefinition); CustomElementDefinition* GetCustomElementDefinition(); nsIAtom* GetCustomElementType(); void Traverse(nsCycleCollectionTraversalCallback& aCb) const; void Unlink(); private: virtual ~CustomElementData() {} // Custom element type, for <button is="x-button"> or <x-button> // this would be x-button. RefPtr<nsIAtom> mType; RefPtr<CustomElementDefinition> mCustomElementDefinition; }; #define ALEADY_CONSTRUCTED_MARKER nullptr // The required information for a custom element as defined in: // https://html.spec.whatwg.org/multipage/scripting.html#custom-element-definition struct CustomElementDefinition { NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CustomElementDefinition) NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CustomElementDefinition) CustomElementDefinition(nsIAtom* aType, nsIAtom* aLocalName, Function* aConstructor, nsCOMArray<nsIAtom>&& aObservedAttributes, JSObject* aPrototype, mozilla::dom::LifecycleCallbacks* aCallbacks, uint32_t aDocOrder); // The type (name) for this custom element, for <button is="x-foo"> or <x-foo> // this would be x-foo. nsCOMPtr<nsIAtom> mType; // The localname to (e.g. <button is=type> -- this would be button). nsCOMPtr<nsIAtom> mLocalName; // The custom element constructor. RefPtr<CustomElementConstructor> mConstructor; // The list of attributes that this custom element observes. nsCOMArray<nsIAtom> mObservedAttributes; // The prototype to use for new custom elements of this type. JS::Heap<JSObject *> mPrototype; // The lifecycle callbacks to call for this custom element. UniquePtr<mozilla::dom::LifecycleCallbacks> mCallbacks; // A construction stack. Use nullptr to represent an "already constructed marker". nsTArray<RefPtr<nsGenericHTMLElement>> mConstructionStack; // The document custom element order. uint32_t mDocOrder; bool IsCustomBuiltIn() { return mType != mLocalName; } bool IsInObservedAttributeList(nsIAtom* aName) { if (mObservedAttributes.IsEmpty()) { return false; } return mObservedAttributes.Contains(aName); } private: ~CustomElementDefinition() {} }; class CustomElementReaction { public: virtual ~CustomElementReaction() = default; virtual void Invoke(Element* aElement, ErrorResult& aRv) = 0; virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const { } #if DEBUG bool IsUpgradeReaction() { return mIsUpgradeReaction; } protected: bool mIsUpgradeReaction = false; #endif }; class CustomElementUpgradeReaction final : public CustomElementReaction { public: explicit CustomElementUpgradeReaction(CustomElementDefinition* aDefinition) : mDefinition(aDefinition) { #if DEBUG mIsUpgradeReaction = true; #endif } private: virtual void Invoke(Element* aElement, ErrorResult& aRv) override; CustomElementDefinition* mDefinition; }; class CustomElementCallbackReaction final : public CustomElementReaction { public: explicit CustomElementCallbackReaction(UniquePtr<CustomElementCallback> aCustomElementCallback) : mCustomElementCallback(Move(aCustomElementCallback)) { } virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const override { mCustomElementCallback->Traverse(aCb); } private: virtual void Invoke(Element* aElement, ErrorResult& aRv) override; UniquePtr<CustomElementCallback> mCustomElementCallback; }; // https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reactions-stack class CustomElementReactionsStack { public: NS_INLINE_DECL_REFCOUNTING(CustomElementReactionsStack) CustomElementReactionsStack() : mIsBackupQueueProcessing(false) , mRecursionDepth(0) , mIsElementQueuePushedForCurrentRecursionDepth(false) { } // Hold a strong reference of Element so that it does not get cycle collected // before the reactions in its reaction queue are invoked. // The element reaction queues are stored in CustomElementData. // We need to lookup ElementReactionQueueMap again to get relevant reaction queue. // The choice of 1 for the auto size here is based on gut feeling. typedef AutoTArray<RefPtr<Element>, 1> ElementQueue; /** * Enqueue a custom element upgrade reaction * https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-upgrade-reaction */ void EnqueueUpgradeReaction(Element* aElement, CustomElementDefinition* aDefinition); /** * Enqueue a custom element callback reaction * https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-callback-reaction */ void EnqueueCallbackReaction(Element* aElement, UniquePtr<CustomElementCallback> aCustomElementCallback); /** * [CEReactions] Before executing the algorithm's steps. * Increase the current recursion depth, and the element queue is pushed * lazily when we really enqueue reactions. * * @return true if the element queue is pushed for "previous" recursion depth. */ bool EnterCEReactions() { bool temp = mIsElementQueuePushedForCurrentRecursionDepth; mRecursionDepth++; // The is-element-queue-pushed flag is initially false when entering a new // recursion level. The original value will be cached in AutoCEReaction // and restored after leaving this recursion level. mIsElementQueuePushedForCurrentRecursionDepth = false; return temp; } /** * [CEReactions] After executing the algorithm's steps. * Pop and invoke the element queue if it is created and pushed for current * recursion depth, then decrease the current recursion depth. * * @param aCx JSContext used for handling exception thrown by algorithm's * steps, this could be a nullptr. * aWasElementQueuePushed used for restoring status after leaving * current recursion. */ void LeaveCEReactions(JSContext* aCx, bool aWasElementQueuePushed) { MOZ_ASSERT(mRecursionDepth); if (mIsElementQueuePushedForCurrentRecursionDepth) { Maybe<JS::AutoSaveExceptionState> ases; if (aCx) { ases.emplace(aCx); } PopAndInvokeElementQueue(); } mRecursionDepth--; // Restore the is-element-queue-pushed flag cached in AutoCEReaction when // leaving the recursion level. mIsElementQueuePushedForCurrentRecursionDepth = aWasElementQueuePushed; MOZ_ASSERT_IF(!mRecursionDepth, mReactionsStack.IsEmpty()); } private: ~CustomElementReactionsStack() {}; /** * Push a new element queue onto the custom element reactions stack. */ void CreateAndPushElementQueue(); /** * Pop the element queue from the custom element reactions stack, and invoke * custom element reactions in that queue. */ void PopAndInvokeElementQueue(); // The choice of 8 for the auto size here is based on gut feeling. AutoTArray<UniquePtr<ElementQueue>, 8> mReactionsStack; ElementQueue mBackupQueue; // https://html.spec.whatwg.org/#enqueue-an-element-on-the-appropriate-element-queue bool mIsBackupQueueProcessing; void InvokeBackupQueue(); /** * Invoke custom element reactions * https://html.spec.whatwg.org/multipage/scripting.html#invoke-custom-element-reactions */ void InvokeReactions(ElementQueue* aElementQueue, nsIGlobalObject* aGlobal); void Enqueue(Element* aElement, CustomElementReaction* aReaction); // Current [CEReactions] recursion depth. uint32_t mRecursionDepth; // True if the element queue is pushed into reaction stack for current // recursion depth. This will be cached in AutoCEReaction when entering a new // CEReaction recursion and restored after leaving the recursion. bool mIsElementQueuePushedForCurrentRecursionDepth; private: class BackupQueueMicroTask final : public mozilla::MicroTaskRunnable { public: explicit BackupQueueMicroTask(CustomElementReactionsStack* aReactionStack) : MicroTaskRunnable() , mReactionStack(aReactionStack) { MOZ_ASSERT(!mReactionStack->mIsBackupQueueProcessing, "mIsBackupQueueProcessing should be initially false"); mReactionStack->mIsBackupQueueProcessing = true; } virtual void Run(AutoSlowOperation& aAso) override { mReactionStack->InvokeBackupQueue(); mReactionStack->mIsBackupQueueProcessing = false; } private: RefPtr<CustomElementReactionsStack> mReactionStack; }; }; class CustomElementRegistry final : public nsISupports, public nsWrapperCache { // Allow nsDocument to access mCustomDefinitions and mCandidatesMap. friend class ::nsDocument; public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CustomElementRegistry) public: static bool IsCustomElementEnabled(JSContext* aCx = nullptr, JSObject* aObject = nullptr); explicit CustomElementRegistry(nsPIDOMWindowInner* aWindow); /** * Looking up a custom element definition. * https://html.spec.whatwg.org/#look-up-a-custom-element-definition */ CustomElementDefinition* LookupCustomElementDefinition( nsIAtom* aNameAtom, nsIAtom* aTypeAtom) const; CustomElementDefinition* LookupCustomElementDefinition( JSContext* aCx, JSObject *aConstructor) const; static void EnqueueLifecycleCallback(nsIDocument::ElementCallbackType aType, Element* aCustomElement, LifecycleCallbackArgs* aArgs, LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs, CustomElementDefinition* aDefinition); /** * Upgrade an element. * https://html.spec.whatwg.org/multipage/scripting.html#upgrades */ static void Upgrade(Element* aElement, CustomElementDefinition* aDefinition, ErrorResult& aRv); /** * Registers an unresolved custom element that is a candidate for * upgrade. |aTypeName| is the name of the custom element type, if it is not * provided, then element name is used. |aTypeName| should be provided * when registering a custom element that extends an existing * element. e.g. <button is="x-button">. */ void RegisterUnresolvedElement(Element* aElement, nsIAtom* aTypeName = nullptr); /** * Unregister an unresolved custom element that is a candidate for * upgrade when a custom element is removed from tree. */ void UnregisterUnresolvedElement(Element* aElement, nsIAtom* aTypeName = nullptr); private: ~CustomElementRegistry(); static UniquePtr<CustomElementCallback> CreateCustomElementCallback( nsIDocument::ElementCallbackType aType, Element* aCustomElement, LifecycleCallbackArgs* aArgs, LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs, CustomElementDefinition* aDefinition); void UpgradeCandidates(nsIAtom* aKey, CustomElementDefinition* aDefinition, ErrorResult& aRv); typedef nsRefPtrHashtable<nsISupportsHashKey, CustomElementDefinition> DefinitionMap; typedef nsClassHashtable<nsISupportsHashKey, nsTArray<nsWeakPtr>> CandidateMap; typedef JS::GCHashMap<JS::Heap<JSObject*>, nsCOMPtr<nsIAtom>, js::MovableCellHasher<JS::Heap<JSObject*>>, js::SystemAllocPolicy> ConstructorMap; // Hashtable for custom element definitions in web components. // Custom prototypes are stored in the compartment where definition was // defined. DefinitionMap mCustomDefinitions; // Hashtable for looking up definitions by using constructor as key. // Custom elements' name are stored here and we need to lookup // mCustomDefinitions again to get definitions. ConstructorMap mConstructors; typedef nsRefPtrHashtable<nsISupportsHashKey, Promise> WhenDefinedPromiseMap; WhenDefinedPromiseMap mWhenDefinedPromiseMap; // The "upgrade candidates map" from the web components spec. Maps from a // namespace id and local name to a list of elements to upgrade if that // element is registered as a custom element. CandidateMap mCandidatesMap; nsCOMPtr<nsPIDOMWindowInner> mWindow; // It is used to prevent reentrant invocations of element definition. bool mIsCustomDefinitionRunning; private: class MOZ_RAII AutoSetRunningFlag final { public: explicit AutoSetRunningFlag(CustomElementRegistry* aRegistry) : mRegistry(aRegistry) { MOZ_ASSERT(!mRegistry->mIsCustomDefinitionRunning, "IsCustomDefinitionRunning flag should be initially false"); mRegistry->mIsCustomDefinitionRunning = true; } ~AutoSetRunningFlag() { mRegistry->mIsCustomDefinitionRunning = false; } private: CustomElementRegistry* mRegistry; }; public: nsISupports* GetParentObject() const; DocGroup* GetDocGroup() const; virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; void Define(const nsAString& aName, Function& aFunctionConstructor, const ElementDefinitionOptions& aOptions, ErrorResult& aRv); void Get(JSContext* cx, const nsAString& name, JS::MutableHandle<JS::Value> aRetVal); already_AddRefed<Promise> WhenDefined(const nsAString& aName, ErrorResult& aRv); }; class MOZ_RAII AutoCEReaction final { public: // JSContext is allowed to be a nullptr if we are guaranteeing that we're // not doing something that might throw but not finish reporting a JS // exception during the lifetime of the AutoCEReaction. AutoCEReaction(CustomElementReactionsStack* aReactionsStack, JSContext* aCx) : mReactionsStack(aReactionsStack) , mCx(aCx) { mIsElementQueuePushedForPreviousRecursionDepth = mReactionsStack->EnterCEReactions(); } ~AutoCEReaction() { mReactionsStack->LeaveCEReactions( mCx, mIsElementQueuePushedForPreviousRecursionDepth); } private: RefPtr<CustomElementReactionsStack> mReactionsStack; JSContext* mCx; bool mIsElementQueuePushedForPreviousRecursionDepth; }; } // namespace dom } // namespace mozilla #endif // mozilla_dom_CustomElementRegistry_h