diff options
Diffstat (limited to '')
-rw-r--r-- | dom/base/CustomElementRegistry.h | 599 |
1 files changed, 599 insertions, 0 deletions
diff --git a/dom/base/CustomElementRegistry.h b/dom/base/CustomElementRegistry.h new file mode 100644 index 0000000000..099be3ebef --- /dev/null +++ b/dom/base/CustomElementRegistry.h @@ -0,0 +1,599 @@ +/* -*- 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 |