/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/CycleCollectedJSContext.h" // for MicroTaskRunnable #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/CustomElementRegistryBinding.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ElementInternals.h" #include "mozilla/dom/ElementInternalsBinding.h" #include "mozilla/dom/HTMLFormElement.h" #include "mozilla/RefPtr.h" #include "nsCycleCollectionParticipant.h" #include "nsWrapperCache.h" #include "nsTHashSet.h" #include "nsAtomHashKeys.h" namespace mozilla { class ErrorResult; namespace dom { struct CustomElementData; struct ElementDefinitionOptions; class CallbackFunction; class CustomElementCallback; class CustomElementReaction; class DocGroup; class Promise; enum class ElementCallbackType { eConnected, eDisconnected, eAdopted, eAttributeChanged, eFormAssociated, eFormReset, eFormDisabled, eFormStateRestore, eGetCustomInterface }; struct LifecycleCallbackArgs { // Used by the attribute changed callback. RefPtr<nsAtom> mName; nsString mOldValue; nsString mNewValue; nsString mNamespaceURI; // Used by the adopted callback. RefPtr<Document> mOldDocument; RefPtr<Document> mNewDocument; // Used by the form associated callback. RefPtr<HTMLFormElement> mForm; // Used by the form disabled callback. bool mDisabled; // Used by the form state restore callback. Nullable<OwningFileOrUSVStringOrFormData> mState; RestoreReason mReason; size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const; }; // Each custom element has an associated callback queue and an element is // being created flag. struct 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, ePrecustomized }; explicit CustomElementData(nsAtom* aType); CustomElementData(nsAtom* aType, State aState); ~CustomElementData() = default; // 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() const; nsAtom* GetCustomElementType() const { return mType; } void AttachedInternals(); bool HasAttachedInternals() const { return mIsAttachedInternals; } bool IsFormAssociated() const; void Traverse(nsCycleCollectionTraversalCallback& aCb) const; void Unlink(); size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; nsAtom* GetIs(const Element* aElement) const { // If mType isn't the same as name atom, this is a customized built-in // element, which has 'is' value set. return aElement->NodeInfo()->NameAtom() == mType ? nullptr : mType.get(); } ElementInternals* GetElementInternals() const { return mElementInternals; } ElementInternals* GetOrCreateElementInternals(HTMLElement* aTarget) { if (!mElementInternals) { mElementInternals = MakeAndAddRef<ElementInternals>(aTarget); } return mElementInternals; } private: // Custom element type, for <button is="x-button"> or <x-button> // this would be x-button. RefPtr<nsAtom> mType; RefPtr<CustomElementDefinition> mCustomElementDefinition; RefPtr<ElementInternals> mElementInternals; bool mIsAttachedInternals = false; }; #define ALREADY_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_NATIVE_CLASS(CustomElementDefinition) NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CustomElementDefinition) CustomElementDefinition( nsAtom* aType, nsAtom* aLocalName, int32_t aNamespaceID, CustomElementConstructor* aConstructor, nsTArray<RefPtr<nsAtom>>&& aObservedAttributes, UniquePtr<LifecycleCallbacks>&& aCallbacks, UniquePtr<FormAssociatedLifecycleCallbacks>&& aFormAssociatedCallbacks, bool aFormAssociated, bool aDisableInternals, bool aDisableShadow); // The type (name) for this custom element, for <button is="x-foo"> or <x-foo> // this would be x-foo. RefPtr<nsAtom> mType; // The localname to (e.g. <button is=type> -- this would be button). RefPtr<nsAtom> mLocalName; // The namespace for this custom element int32_t mNamespaceID; // The custom element constructor. RefPtr<CustomElementConstructor> mConstructor; // The list of attributes that this custom element observes. nsTArray<RefPtr<nsAtom>> mObservedAttributes; // The lifecycle callbacks to call for this custom element. UniquePtr<LifecycleCallbacks> mCallbacks; UniquePtr<FormAssociatedLifecycleCallbacks> mFormAssociatedCallbacks; // If this is true, user agent treats elements associated to this custom // element definition as form-associated custom elements. bool mFormAssociated = false; // Determine whether to allow to attachInternals() for this custom element. bool mDisableInternals = false; // Determine whether to allow to attachShadow() for this custom element. bool mDisableShadow = false; // A construction stack. Use nullptr to represent an "already constructed // marker". nsTArray<RefPtr<Element>> mConstructionStack; // See step 6.1.10 of https://dom.spec.whatwg.org/#concept-create-element // which set up the prefix after a custom element is created. However, In // Gecko, the prefix isn't allowed to be changed in NodeInfo, so we store the // prefix information here and propagate to where NodeInfo is assigned to a // custom element instead. nsTArray<RefPtr<nsAtom>> mPrefixStack; // This basically is used for distinguishing the custom element constructor // is invoked from document.createElement or directly from JS, i.e. // `new CustomElementConstructor()`. uint32_t mConstructionDepth = 0; bool IsCustomBuiltIn() { return mType != mLocalName; } bool IsInObservedAttributeList(nsAtom* aName) { if (mObservedAttributes.IsEmpty()) { return false; } return mObservedAttributes.Contains(aName); } private: ~CustomElementDefinition() = default; }; class CustomElementReaction { public: virtual ~CustomElementReaction() = default; MOZ_CAN_RUN_SCRIPT virtual void Invoke(Element* aElement, ErrorResult& aRv) = 0; virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const = 0; virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const = 0; bool IsUpgradeReaction() { return mIsUpgradeReaction; } protected: bool mIsUpgradeReaction = false; }; // 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 3 for the auto size here is based on running Custom // Elements wpt tests. typedef AutoTArray<RefPtr<Element>, 3> 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. */ MOZ_CAN_RUN_SCRIPT 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()); } bool IsElementQueuePushedForCurrentRecursionDepth() { MOZ_ASSERT_IF(mIsElementQueuePushedForCurrentRecursionDepth, !mReactionsStack.IsEmpty() && !mReactionsStack.LastElement()->IsEmpty()); return mIsElementQueuePushedForCurrentRecursionDepth; } private: ~CustomElementReactionsStack() = default; ; /** * 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. */ MOZ_CAN_RUN_SCRIPT 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; MOZ_CAN_RUN_SCRIPT void InvokeBackupQueue(); /** * Invoke custom element reactions * https://html.spec.whatwg.org/multipage/scripting.html#invoke-custom-element-reactions */ MOZ_CAN_RUN_SCRIPT 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; } MOZ_CAN_RUN_SCRIPT virtual void Run(AutoSlowOperation& aAso) override { mReactionStack->InvokeBackupQueue(); mReactionStack->mIsBackupQueueProcessing = false; } private: const RefPtr<CustomElementReactionsStack> mReactionStack; }; }; class CustomElementRegistry final : public nsISupports, public nsWrapperCache { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CustomElementRegistry) public: explicit CustomElementRegistry(nsPIDOMWindowInner* aWindow); private: class RunCustomElementCreationCallback : public mozilla::Runnable { public: // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. // See bug 1535398. MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_DECL_NSIRUNNABLE explicit RunCustomElementCreationCallback( CustomElementRegistry* aRegistry, nsAtom* aAtom, CustomElementCreationCallback* aCallback) : mozilla::Runnable( "CustomElementRegistry::RunCustomElementCreationCallback"), mRegistry(aRegistry), mAtom(aAtom), mCallback(aCallback) {} private: RefPtr<CustomElementRegistry> mRegistry; RefPtr<nsAtom> mAtom; RefPtr<CustomElementCreationCallback> mCallback; }; public: /** * Looking up a custom element definition. * https://html.spec.whatwg.org/#look-up-a-custom-element-definition */ CustomElementDefinition* LookupCustomElementDefinition(nsAtom* aNameAtom, int32_t aNameSpaceID, nsAtom* aTypeAtom); CustomElementDefinition* LookupCustomElementDefinition( JSContext* aCx, JSObject* aConstructor) const; static void EnqueueLifecycleCallback(ElementCallbackType aType, Element* aCustomElement, const LifecycleCallbackArgs& aArgs, CustomElementDefinition* aDefinition); /** * Upgrade an element. * https://html.spec.whatwg.org/multipage/scripting.html#upgrades */ MOZ_CAN_RUN_SCRIPT static void Upgrade(Element* aElement, CustomElementDefinition* aDefinition, ErrorResult& aRv); /** * To allow native code to call methods of chrome-implemented custom elements, * a helper method may be defined in the custom element called * 'getCustomInterfaceCallback'. This method takes an IID and returns an * object which implements an XPCOM interface. * * This returns null if aElement is not from a chrome document. */ static already_AddRefed<nsISupports> CallGetCustomInterface( Element* aElement, const nsIID& aIID); /** * 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, nsAtom* 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, nsAtom* aTypeName = nullptr); /** * Register an element to be upgraded when the custom element creation * callback is executed. * * To be used when LookupCustomElementDefinition() didn't return a definition, * but with the callback scheduled to be run. */ inline void RegisterCallbackUpgradeElement(Element* aElement, nsAtom* aTypeName = nullptr) { if (mElementCreationCallbacksUpgradeCandidatesMap.IsEmpty()) { return; } RefPtr<nsAtom> typeName = aTypeName; if (!typeName) { typeName = aElement->NodeInfo()->NameAtom(); } nsTHashSet<RefPtr<nsIWeakReference>>* elements = mElementCreationCallbacksUpgradeCandidatesMap.Get(typeName); // If there isn't a table, there won't be a definition added by the // callback. if (!elements) { return; } nsWeakPtr elem = do_GetWeakReference(aElement); elements->Insert(elem); } void TraceDefinitions(JSTracer* aTrc); private: ~CustomElementRegistry(); bool JSObjectToAtomArray(JSContext* aCx, JS::Handle<JSObject*> aConstructor, const nsString& aName, nsTArray<RefPtr<nsAtom>>& aArray, ErrorResult& aRv); void UpgradeCandidates(nsAtom* aKey, CustomElementDefinition* aDefinition, ErrorResult& aRv); using DefinitionMap = nsRefPtrHashtable<nsAtomHashKey, CustomElementDefinition>; using ElementCreationCallbackMap = nsRefPtrHashtable<nsAtomHashKey, CustomElementCreationCallback>; using CandidateMap = nsClassHashtable<nsAtomHashKey, nsTHashSet<RefPtr<nsIWeakReference>>>; using ConstructorMap = JS::GCHashMap<JS::Heap<JSObject*>, RefPtr<nsAtom>, js::StableCellHasher<JS::Heap<JSObject*>>, js::SystemAllocPolicy>; // Hashtable for custom element definitions in web components. // Custom prototypes are stored in the compartment where definition was // defined. DefinitionMap mCustomDefinitions; // Hashtable for chrome-only callbacks that is called *before* we return // a CustomElementDefinition, when the typeAtom matches. // The callbacks are registered with the setElementCreationCallback method. ElementCreationCallbackMap mElementCreationCallbacks; // 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; using WhenDefinedPromiseMap = nsRefPtrHashtable<nsAtomHashKey, Promise>; 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; // If an element creation callback is found, the nsTHashtable for the // type is created here, and elements will later be upgraded. CandidateMap mElementCreationCallbacksUpgradeCandidatesMap; nsCOMPtr<nsPIDOMWindowInner> mWindow; // It is used to prevent reentrant invocations of element definition. bool mIsCustomDefinitionRunning; private: int32_t InferNamespace(JSContext* aCx, JS::Handle<JSObject*> constructor); public: nsISupports* GetParentObject() const; DocGroup* GetDocGroup() const; virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; void Define(JSContext* aCx, const nsAString& aName, CustomElementConstructor& aFunctionConstructor, const ElementDefinitionOptions& aOptions, ErrorResult& aRv); void Get(const nsAString& name, OwningCustomElementConstructorOrUndefined& aRetVal); void GetName(JSContext* aCx, CustomElementConstructor& aConstructor, nsAString& aResult); already_AddRefed<Promise> WhenDefined(const nsAString& aName, ErrorResult& aRv); // Chrome-only method that give JS an opportunity to only load the custom // element definition script when needed. void SetElementCreationCallback(const nsAString& aName, CustomElementCreationCallback& aCallback, ErrorResult& aRv); void Upgrade(nsINode& aRoot); }; 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(); } // MOZ_CAN_RUN_SCRIPT_BOUNDARY because this is called from Maybe<>.reset(). MOZ_CAN_RUN_SCRIPT_BOUNDARY ~AutoCEReaction() { mReactionsStack->LeaveCEReactions( mCx, mIsElementQueuePushedForPreviousRecursionDepth); } private: const RefPtr<CustomElementReactionsStack> mReactionsStack; JSContext* mCx; bool mIsElementQueuePushedForPreviousRecursionDepth; }; } // namespace dom } // namespace mozilla #endif // mozilla_dom_CustomElementRegistry_h