diff options
Diffstat (limited to 'dom/base/CustomElementRegistry.cpp')
-rw-r--r-- | dom/base/CustomElementRegistry.cpp | 1587 |
1 files changed, 1587 insertions, 0 deletions
diff --git a/dom/base/CustomElementRegistry.cpp b/dom/base/CustomElementRegistry.cpp new file mode 100644 index 0000000000..d8b40b5bc5 --- /dev/null +++ b/dom/base/CustomElementRegistry.cpp @@ -0,0 +1,1587 @@ +/* -*- 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/. */ + +#include "mozilla/dom/CustomElementRegistry.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/CustomElementRegistryBinding.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/HTMLElement.h" +#include "mozilla/dom/HTMLElementBinding.h" +#include "mozilla/dom/PrimitiveConversions.h" +#include "mozilla/dom/ShadowIncludingTreeIterator.h" +#include "mozilla/dom/XULElementBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/ShadowRoot.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/UseCounter.h" +#include "nsContentUtils.h" +#include "nsHTMLTags.h" +#include "jsapi.h" +#include "js/ForOfIterator.h" // JS::ForOfIterator +#include "js/PropertyAndElement.h" // JS_GetProperty, JS_GetUCProperty +#include "xpcprivate.h" +#include "nsGlobalWindow.h" +#include "nsNameSpaceManager.h" + +namespace mozilla::dom { + +//----------------------------------------------------- +// CustomElementUpgradeReaction + +class CustomElementUpgradeReaction final : public CustomElementReaction { + public: + explicit CustomElementUpgradeReaction(CustomElementDefinition* aDefinition) + : mDefinition(aDefinition) { + mIsUpgradeReaction = true; + } + + virtual void Traverse( + nsCycleCollectionTraversalCallback& aCb) const override { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(aCb, "mDefinition"); + aCb.NoteNativeChild( + mDefinition, NS_CYCLE_COLLECTION_PARTICIPANT(CustomElementDefinition)); + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + // We don't really own mDefinition. + return aMallocSizeOf(this); + } + + private: + MOZ_CAN_RUN_SCRIPT + virtual void Invoke(Element* aElement, ErrorResult& aRv) override { + CustomElementRegistry::Upgrade(aElement, mDefinition, aRv); + } + + const RefPtr<CustomElementDefinition> mDefinition; +}; + +//----------------------------------------------------- +// CustomElementCallbackReaction + +class CustomElementCallback { + public: + CustomElementCallback(Element* aThisObject, ElementCallbackType aCallbackType, + CallbackFunction* aCallback, + const LifecycleCallbackArgs& aArgs); + void Traverse(nsCycleCollectionTraversalCallback& aCb) const; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + void Call(); + + static UniquePtr<CustomElementCallback> Create( + ElementCallbackType aType, Element* aCustomElement, + const LifecycleCallbackArgs& aArgs, CustomElementDefinition* aDefinition); + + private: + // The this value to use for invocation of the callback. + RefPtr<Element> mThisObject; + RefPtr<CallbackFunction> mCallback; + // The type of callback (eCreated, eAttached, etc.) + ElementCallbackType mType; + // Arguments to be passed to the callback, + LifecycleCallbackArgs mArgs; +}; + +class CustomElementCallbackReaction final : public CustomElementReaction { + public: + explicit CustomElementCallbackReaction( + UniquePtr<CustomElementCallback> aCustomElementCallback) + : mCustomElementCallback(std::move(aCustomElementCallback)) {} + + virtual void Traverse( + nsCycleCollectionTraversalCallback& aCb) const override { + mCustomElementCallback->Traverse(aCb); + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t n = aMallocSizeOf(this); + + n += mCustomElementCallback->SizeOfIncludingThis(aMallocSizeOf); + + return n; + } + + private: + virtual void Invoke(Element* aElement, ErrorResult& aRv) override { + mCustomElementCallback->Call(); + } + + UniquePtr<CustomElementCallback> mCustomElementCallback; +}; + +//----------------------------------------------------- +// CustomElementCallback + +size_t LifecycleCallbackArgs::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t n = mName.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += mOldValue.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += mNewValue.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += mNamespaceURI.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + return n; +} + +/* static */ +UniquePtr<CustomElementCallback> CustomElementCallback::Create( + ElementCallbackType aType, Element* aCustomElement, + const LifecycleCallbackArgs& aArgs, CustomElementDefinition* aDefinition) { + MOZ_ASSERT(aDefinition, "CustomElementDefinition should not be null"); + MOZ_ASSERT(aCustomElement->GetCustomElementData(), + "CustomElementData should exist"); + + // Let CALLBACK be the callback associated with the key NAME in CALLBACKS. + CallbackFunction* func = nullptr; + switch (aType) { + case ElementCallbackType::eConnected: + if (aDefinition->mCallbacks->mConnectedCallback.WasPassed()) { + func = aDefinition->mCallbacks->mConnectedCallback.Value(); + } + break; + + case ElementCallbackType::eDisconnected: + if (aDefinition->mCallbacks->mDisconnectedCallback.WasPassed()) { + func = aDefinition->mCallbacks->mDisconnectedCallback.Value(); + } + break; + + case ElementCallbackType::eAdopted: + if (aDefinition->mCallbacks->mAdoptedCallback.WasPassed()) { + func = aDefinition->mCallbacks->mAdoptedCallback.Value(); + } + break; + + case ElementCallbackType::eAttributeChanged: + if (aDefinition->mCallbacks->mAttributeChangedCallback.WasPassed()) { + func = aDefinition->mCallbacks->mAttributeChangedCallback.Value(); + } + break; + + case ElementCallbackType::eFormAssociated: + if (aDefinition->mFormAssociatedCallbacks->mFormAssociatedCallback + .WasPassed()) { + func = aDefinition->mFormAssociatedCallbacks->mFormAssociatedCallback + .Value(); + } + break; + + case ElementCallbackType::eFormReset: + if (aDefinition->mFormAssociatedCallbacks->mFormResetCallback + .WasPassed()) { + func = + aDefinition->mFormAssociatedCallbacks->mFormResetCallback.Value(); + } + break; + + case ElementCallbackType::eFormDisabled: + if (aDefinition->mFormAssociatedCallbacks->mFormDisabledCallback + .WasPassed()) { + func = aDefinition->mFormAssociatedCallbacks->mFormDisabledCallback + .Value(); + } + break; + + case ElementCallbackType::eGetCustomInterface: + MOZ_ASSERT_UNREACHABLE("Don't call GetCustomInterface through callback"); + break; + } + + // If there is no such callback, stop. + if (!func) { + return nullptr; + } + + // Add CALLBACK to ELEMENT's callback queue. + return MakeUnique<CustomElementCallback>(aCustomElement, aType, func, aArgs); +} + +void CustomElementCallback::Call() { + switch (mType) { + case ElementCallbackType::eConnected: + static_cast<LifecycleConnectedCallback*>(mCallback.get()) + ->Call(mThisObject); + break; + case ElementCallbackType::eDisconnected: + static_cast<LifecycleDisconnectedCallback*>(mCallback.get()) + ->Call(mThisObject); + break; + case ElementCallbackType::eAdopted: + static_cast<LifecycleAdoptedCallback*>(mCallback.get()) + ->Call(mThisObject, mArgs.mOldDocument, mArgs.mNewDocument); + break; + case ElementCallbackType::eAttributeChanged: + static_cast<LifecycleAttributeChangedCallback*>(mCallback.get()) + ->Call(mThisObject, mArgs.mName, mArgs.mOldValue, mArgs.mNewValue, + mArgs.mNamespaceURI); + break; + case ElementCallbackType::eFormAssociated: + static_cast<LifecycleFormAssociatedCallback*>(mCallback.get()) + ->Call(mThisObject, mArgs.mForm); + break; + case ElementCallbackType::eFormReset: + static_cast<LifecycleFormResetCallback*>(mCallback.get()) + ->Call(mThisObject); + break; + case ElementCallbackType::eFormDisabled: + static_cast<LifecycleFormDisabledCallback*>(mCallback.get()) + ->Call(mThisObject, mArgs.mDisabled); + break; + case ElementCallbackType::eGetCustomInterface: + MOZ_ASSERT_UNREACHABLE("Don't call GetCustomInterface through callback"); + break; + } +} + +void CustomElementCallback::Traverse( + nsCycleCollectionTraversalCallback& aCb) const { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(aCb, "mThisObject"); + aCb.NoteXPCOMChild(mThisObject); + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(aCb, "mCallback"); + aCb.NoteXPCOMChild(mCallback); +} + +size_t CustomElementCallback::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + + // We don't uniquely own mThisObject. + + // We own mCallback but it doesn't have any special memory reporting we can do + // for it other than report its own size. + n += aMallocSizeOf(mCallback); + + n += mArgs.SizeOfExcludingThis(aMallocSizeOf); + + return n; +} + +CustomElementCallback::CustomElementCallback( + Element* aThisObject, ElementCallbackType aCallbackType, + mozilla::dom::CallbackFunction* aCallback, + const LifecycleCallbackArgs& aArgs) + : mThisObject(aThisObject), + mCallback(aCallback), + mType(aCallbackType), + mArgs(aArgs) {} + +//----------------------------------------------------- +// CustomElementData + +CustomElementData::CustomElementData(nsAtom* aType) + : CustomElementData(aType, CustomElementData::State::eUndefined) {} + +CustomElementData::CustomElementData(nsAtom* aType, State aState) + : mState(aState), mType(aType) {} + +void CustomElementData::SetCustomElementDefinition( + CustomElementDefinition* aDefinition) { + // Only allow reset definition to nullptr if the custom element state is + // "failed". + MOZ_ASSERT(aDefinition ? !mCustomElementDefinition + : mState == State::eFailed); + MOZ_ASSERT_IF(aDefinition, aDefinition->mType == mType); + + mCustomElementDefinition = aDefinition; +} + +void CustomElementData::AttachedInternals() { + MOZ_ASSERT(!mIsAttachedInternals); + + mIsAttachedInternals = true; +} + +CustomElementDefinition* CustomElementData::GetCustomElementDefinition() const { + // Per spec, if there is a definition, the custom element state should be + // either "failed" (during upgrade) or "customized". + MOZ_ASSERT_IF(mCustomElementDefinition, mState != State::eUndefined); + + return mCustomElementDefinition; +} + +bool CustomElementData::IsFormAssociated() const { + // https://html.spec.whatwg.org/#form-associated-custom-element + return mCustomElementDefinition && + !mCustomElementDefinition->IsCustomBuiltIn() && + mCustomElementDefinition->mFormAssociated; +} + +void CustomElementData::Traverse( + nsCycleCollectionTraversalCallback& aCb) const { + for (uint32_t i = 0; i < mReactionQueue.Length(); i++) { + if (mReactionQueue[i]) { + mReactionQueue[i]->Traverse(aCb); + } + } + + if (mCustomElementDefinition) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(aCb, "mCustomElementDefinition"); + aCb.NoteNativeChild( + mCustomElementDefinition, + NS_CYCLE_COLLECTION_PARTICIPANT(CustomElementDefinition)); + } + + if (mElementInternals) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(aCb, "mElementInternals"); + aCb.NoteXPCOMChild(ToSupports(mElementInternals.get())); + } +} + +void CustomElementData::Unlink() { + mReactionQueue.Clear(); + if (mElementInternals) { + mElementInternals->Unlink(); + mElementInternals = nullptr; + } + mCustomElementDefinition = nullptr; +} + +size_t CustomElementData::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + + n += mReactionQueue.ShallowSizeOfExcludingThis(aMallocSizeOf); + + for (auto& reaction : mReactionQueue) { + // "reaction" can be null if we're being called indirectly from + // InvokeReactions (e.g. due to a reaction causing a memory report to be + // captured somehow). + if (reaction) { + n += reaction->SizeOfIncludingThis(aMallocSizeOf); + } + } + + return n; +} + +//----------------------------------------------------- +// CustomElementRegistry + +namespace { + +class MOZ_RAII AutoConstructionStackEntry final { + public: + AutoConstructionStackEntry(nsTArray<RefPtr<Element>>& aStack, + Element* aElement) + : mStack(aStack) { + MOZ_ASSERT(aElement->IsHTMLElement() || aElement->IsXULElement()); + +#ifdef DEBUG + mIndex = mStack.Length(); +#endif + mStack.AppendElement(aElement); + } + + ~AutoConstructionStackEntry() { + MOZ_ASSERT(mIndex == mStack.Length() - 1, + "Removed element should be the last element"); + mStack.RemoveLastElement(); + } + + private: + nsTArray<RefPtr<Element>>& mStack; +#ifdef DEBUG + uint32_t mIndex; +#endif +}; + +} // namespace + +NS_IMPL_CYCLE_COLLECTION_CLASS(CustomElementRegistry) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CustomElementRegistry) + tmp->mConstructors.clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCustomDefinitions) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWhenDefinedPromiseMap) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mElementCreationCallbacks) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CustomElementRegistry) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCustomDefinitions) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWhenDefinedPromiseMap) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElementCreationCallbacks) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CustomElementRegistry) + for (auto iter = tmp->mConstructors.iter(); !iter.done(); iter.next()) { + aCallbacks.Trace(&iter.get().mutableKey(), "mConstructors key", aClosure); + } + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(CustomElementRegistry) +NS_IMPL_CYCLE_COLLECTING_RELEASE(CustomElementRegistry) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CustomElementRegistry) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +CustomElementRegistry::CustomElementRegistry(nsPIDOMWindowInner* aWindow) + : mWindow(aWindow), mIsCustomDefinitionRunning(false) { + MOZ_ASSERT(aWindow); + + mozilla::HoldJSObjects(this); +} + +CustomElementRegistry::~CustomElementRegistry() { + mozilla::DropJSObjects(this); +} + +NS_IMETHODIMP +CustomElementRegistry::RunCustomElementCreationCallback::Run() { + ErrorResult er; + nsDependentAtomString value(mAtom); + mCallback->Call(value, er); + MOZ_ASSERT(NS_SUCCEEDED(er.StealNSResult()), + "chrome JavaScript error in the callback."); + + RefPtr<CustomElementDefinition> definition = + mRegistry->mCustomDefinitions.Get(mAtom); + MOZ_ASSERT(definition, "Callback should define the definition of type."); + MOZ_ASSERT(!mRegistry->mElementCreationCallbacks.GetWeak(mAtom), + "Callback should be removed."); + + mozilla::UniquePtr<nsTHashSet<RefPtr<nsIWeakReference>>> elements; + mRegistry->mElementCreationCallbacksUpgradeCandidatesMap.Remove(mAtom, + &elements); + MOZ_ASSERT(elements, "There should be a list"); + + for (const auto& key : *elements) { + nsCOMPtr<Element> elem = do_QueryReferent(key); + if (!elem) { + continue; + } + + CustomElementRegistry::Upgrade(elem, definition, er); + MOZ_ASSERT(NS_SUCCEEDED(er.StealNSResult()), + "chrome JavaScript error in custom element construction."); + } + + return NS_OK; +} + +CustomElementDefinition* CustomElementRegistry::LookupCustomElementDefinition( + nsAtom* aNameAtom, int32_t aNameSpaceID, nsAtom* aTypeAtom) { + CustomElementDefinition* data = mCustomDefinitions.GetWeak(aTypeAtom); + + if (!data) { + RefPtr<CustomElementCreationCallback> callback; + mElementCreationCallbacks.Get(aTypeAtom, getter_AddRefs(callback)); + if (callback) { + mElementCreationCallbacks.Remove(aTypeAtom); + mElementCreationCallbacksUpgradeCandidatesMap.GetOrInsertNew(aTypeAtom); + RefPtr<Runnable> runnable = + new RunCustomElementCreationCallback(this, aTypeAtom, callback); + nsContentUtils::AddScriptRunner(runnable.forget()); + data = mCustomDefinitions.GetWeak(aTypeAtom); + } + } + + if (data && data->mLocalName == aNameAtom && + data->mNamespaceID == aNameSpaceID) { + return data; + } + + return nullptr; +} + +CustomElementDefinition* CustomElementRegistry::LookupCustomElementDefinition( + JSContext* aCx, JSObject* aConstructor) const { + // We're looking up things that tested true for JS::IsConstructor, + // so doing a CheckedUnwrapStatic is fine here. + JS::Rooted<JSObject*> constructor(aCx, js::CheckedUnwrapStatic(aConstructor)); + + const auto& ptr = mConstructors.lookup(constructor); + if (!ptr) { + return nullptr; + } + + CustomElementDefinition* definition = + mCustomDefinitions.GetWeak(ptr->value()); + MOZ_ASSERT(definition, "Definition must be found in mCustomDefinitions"); + + return definition; +} + +void CustomElementRegistry::RegisterUnresolvedElement(Element* aElement, + nsAtom* aTypeName) { + // We don't have a use-case for a Custom Element inside NAC, and continuing + // here causes performance issues for NAC + XBL anonymous content. + if (aElement->IsInNativeAnonymousSubtree()) { + return; + } + + mozilla::dom::NodeInfo* info = aElement->NodeInfo(); + + // Candidate may be a custom element through extension, + // in which case the custom element type name will not + // match the element tag name. e.g. <button is="x-button">. + RefPtr<nsAtom> typeName = aTypeName; + if (!typeName) { + typeName = info->NameAtom(); + } + + if (mCustomDefinitions.GetWeak(typeName)) { + return; + } + + nsTHashSet<RefPtr<nsIWeakReference>>* unresolved = + mCandidatesMap.GetOrInsertNew(typeName); + nsWeakPtr elem = do_GetWeakReference(aElement); + unresolved->Insert(elem); +} + +void CustomElementRegistry::UnregisterUnresolvedElement(Element* aElement, + nsAtom* aTypeName) { + nsIWeakReference* weak = aElement->GetExistingWeakReference(); + if (!weak) { + return; + } + +#ifdef DEBUG + { + nsWeakPtr weakPtr = do_GetWeakReference(aElement); + MOZ_ASSERT( + weak == weakPtr.get(), + "do_GetWeakReference should reuse the existing nsIWeakReference."); + } +#endif + + nsTHashSet<RefPtr<nsIWeakReference>>* candidates = nullptr; + if (mCandidatesMap.Get(aTypeName, &candidates)) { + MOZ_ASSERT(candidates); + candidates->Remove(weak); + } +} + +// https://html.spec.whatwg.org/commit-snapshots/65f39c6fc0efa92b0b2b23b93197016af6ac0de6/#enqueue-a-custom-element-callback-reaction +/* static */ +void CustomElementRegistry::EnqueueLifecycleCallback( + ElementCallbackType aType, Element* aCustomElement, + const LifecycleCallbackArgs& aArgs, CustomElementDefinition* aDefinition) { + CustomElementDefinition* definition = aDefinition; + if (!definition) { + definition = aCustomElement->GetCustomElementDefinition(); + if (!definition || + definition->mLocalName != aCustomElement->NodeInfo()->NameAtom()) { + return; + } + + if (!definition->mCallbacks && !definition->mFormAssociatedCallbacks) { + // definition has been unlinked. Don't try to mess with it. + return; + } + } + + auto callback = + CustomElementCallback::Create(aType, aCustomElement, aArgs, definition); + if (!callback) { + return; + } + + DocGroup* docGroup = aCustomElement->OwnerDoc()->GetDocGroup(); + if (!docGroup) { + return; + } + + if (aType == ElementCallbackType::eAttributeChanged) { + RefPtr<nsAtom> attrName = NS_Atomize(aArgs.mName); + if (definition->mObservedAttributes.IsEmpty() || + !definition->mObservedAttributes.Contains(attrName)) { + return; + } + } + + CustomElementReactionsStack* reactionsStack = + docGroup->CustomElementReactionsStack(); + reactionsStack->EnqueueCallbackReaction(aCustomElement, std::move(callback)); +} + +namespace { + +class CandidateFinder { + public: + CandidateFinder(nsTHashSet<RefPtr<nsIWeakReference>>& aCandidates, + Document* aDoc); + nsTArray<nsCOMPtr<Element>> OrderedCandidates(); + + private: + nsCOMPtr<Document> mDoc; + nsInterfaceHashtable<nsPtrHashKey<Element>, Element> mCandidates; +}; + +CandidateFinder::CandidateFinder( + nsTHashSet<RefPtr<nsIWeakReference>>& aCandidates, Document* aDoc) + : mDoc(aDoc), mCandidates(aCandidates.Count()) { + MOZ_ASSERT(mDoc); + for (const auto& candidate : aCandidates) { + nsCOMPtr<Element> elem = do_QueryReferent(candidate); + if (!elem) { + continue; + } + + Element* key = elem.get(); + mCandidates.InsertOrUpdate(key, elem.forget()); + } +} + +nsTArray<nsCOMPtr<Element>> CandidateFinder::OrderedCandidates() { + if (mCandidates.Count() == 1) { + // Fast path for one candidate. + auto iter = mCandidates.Iter(); + nsTArray<nsCOMPtr<Element>> rval({std::move(iter.Data())}); + iter.Remove(); + return rval; + } + + nsTArray<nsCOMPtr<Element>> orderedElements(mCandidates.Count()); + for (nsINode* node : ShadowIncludingTreeIterator(*mDoc)) { + Element* element = Element::FromNode(node); + if (!element) { + continue; + } + + nsCOMPtr<Element> elem; + if (mCandidates.Remove(element, getter_AddRefs(elem))) { + orderedElements.AppendElement(std::move(elem)); + if (mCandidates.Count() == 0) { + break; + } + } + } + + return orderedElements; +} + +} // namespace + +void CustomElementRegistry::UpgradeCandidates( + nsAtom* aKey, CustomElementDefinition* aDefinition, ErrorResult& aRv) { + DocGroup* docGroup = mWindow->GetDocGroup(); + if (!docGroup) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + mozilla::UniquePtr<nsTHashSet<RefPtr<nsIWeakReference>>> candidates; + if (mCandidatesMap.Remove(aKey, &candidates)) { + MOZ_ASSERT(candidates); + CustomElementReactionsStack* reactionsStack = + docGroup->CustomElementReactionsStack(); + + CandidateFinder finder(*candidates, mWindow->GetExtantDoc()); + for (auto& elem : finder.OrderedCandidates()) { + reactionsStack->EnqueueUpgradeReaction(elem, aDefinition); + } + } +} + +JSObject* CustomElementRegistry::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return CustomElementRegistry_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* CustomElementRegistry::GetParentObject() const { return mWindow; } + +DocGroup* CustomElementRegistry::GetDocGroup() const { + return mWindow ? mWindow->GetDocGroup() : nullptr; +} + +int32_t CustomElementRegistry::InferNamespace( + JSContext* aCx, JS::Handle<JSObject*> constructor) { + JS::Rooted<JSObject*> XULConstructor( + aCx, XULElement_Binding::GetConstructorObject(aCx)); + + JS::Rooted<JSObject*> proto(aCx, constructor); + while (proto) { + if (proto == XULConstructor) { + return kNameSpaceID_XUL; + } + + JS_GetPrototype(aCx, proto, &proto); + } + + return kNameSpaceID_XHTML; +} + +bool CustomElementRegistry::JSObjectToAtomArray( + JSContext* aCx, JS::Handle<JSObject*> aConstructor, const nsString& aName, + nsTArray<RefPtr<nsAtom>>& aArray, ErrorResult& aRv) { + JS::Rooted<JS::Value> iterable(aCx, JS::UndefinedValue()); + if (!JS_GetUCProperty(aCx, aConstructor, aName.get(), aName.Length(), + &iterable)) { + aRv.NoteJSContextException(aCx); + return false; + } + + if (!iterable.isUndefined()) { + if (!iterable.isObject()) { + aRv.ThrowTypeError<MSG_CONVERSION_ERROR>(NS_ConvertUTF16toUTF8(aName), + "sequence"); + return false; + } + + JS::ForOfIterator iter(aCx); + if (!iter.init(iterable, JS::ForOfIterator::AllowNonIterable)) { + aRv.NoteJSContextException(aCx); + return false; + } + + if (!iter.valueIsIterable()) { + aRv.ThrowTypeError<MSG_CONVERSION_ERROR>(NS_ConvertUTF16toUTF8(aName), + "sequence"); + return false; + } + + JS::Rooted<JS::Value> attribute(aCx); + while (true) { + bool done; + if (!iter.next(&attribute, &done)) { + aRv.NoteJSContextException(aCx); + return false; + } + if (done) { + break; + } + + nsAutoString attrStr; + if (!ConvertJSValueToString(aCx, attribute, eStringify, eStringify, + attrStr)) { + aRv.NoteJSContextException(aCx); + return false; + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aArray.AppendElement(NS_Atomize(attrStr)); + } + } + + return true; +} + +// https://html.spec.whatwg.org/commit-snapshots/b48bb2238269d90ea4f455a52cdf29505aff3df0/#dom-customelementregistry-define +void CustomElementRegistry::Define( + JSContext* aCx, const nsAString& aName, + CustomElementConstructor& aFunctionConstructor, + const ElementDefinitionOptions& aOptions, ErrorResult& aRv) { + JS::Rooted<JSObject*> constructor(aCx, aFunctionConstructor.CallableOrNull()); + + // We need to do a dynamic unwrap in order to throw the right exception. We + // could probably avoid that if we just threw MSG_NOT_CONSTRUCTOR if unwrap + // fails. + // + // In any case, aCx represents the global we want to be using for the unwrap + // here. + JS::Rooted<JSObject*> constructorUnwrapped( + aCx, js::CheckedUnwrapDynamic(constructor, aCx)); + if (!constructorUnwrapped) { + // If the caller's compartment does not have permission to access the + // unwrapped constructor then throw. + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + /** + * 1. If IsConstructor(constructor) is false, then throw a TypeError and abort + * these steps. + */ + if (!JS::IsConstructor(constructorUnwrapped)) { + aRv.ThrowTypeError<MSG_NOT_CONSTRUCTOR>("Argument 2"); + return; + } + + int32_t nameSpaceID = InferNamespace(aCx, constructor); + + /** + * 2. If name is not a valid custom element name, then throw a "SyntaxError" + * DOMException and abort these steps. + */ + Document* doc = mWindow->GetExtantDoc(); + RefPtr<nsAtom> nameAtom(NS_Atomize(aName)); + if (!nsContentUtils::IsCustomElementName(nameAtom, nameSpaceID)) { + aRv.ThrowSyntaxError( + nsPrintfCString("'%s' is not a valid custom element name", + NS_ConvertUTF16toUTF8(aName).get())); + return; + } + + /** + * 3. If this CustomElementRegistry contains an entry with name name, then + * throw a "NotSupportedError" DOMException and abort these steps. + */ + if (mCustomDefinitions.GetWeak(nameAtom)) { + aRv.ThrowNotSupportedError( + nsPrintfCString("'%s' has already been defined as a custom element", + NS_ConvertUTF16toUTF8(aName).get())); + return; + } + + /** + * 4. If this CustomElementRegistry contains an entry with constructor + * constructor, then throw a "NotSupportedError" DOMException and abort these + * steps. + */ + const auto& ptr = mConstructors.lookup(constructorUnwrapped); + if (ptr) { + MOZ_ASSERT(mCustomDefinitions.GetWeak(ptr->value()), + "Definition must be found in mCustomDefinitions"); + nsAutoCString name; + ptr->value()->ToUTF8String(name); + aRv.ThrowNotSupportedError( + nsPrintfCString("'%s' and '%s' have the same constructor", + NS_ConvertUTF16toUTF8(aName).get(), name.get())); + return; + } + + /** + * 5. Let localName be name. + * 6. Let extends be the value of the extends member of options, or null if + * no such member exists. + * 7. If extends is not null, then: + * 1. If extends is a valid custom element name, then throw a + * "NotSupportedError" DOMException. + * 2. If the element interface for extends and the HTML namespace is + * HTMLUnknownElement (e.g., if extends does not indicate an element + * definition in this specification), then throw a "NotSupportedError" + * DOMException. + * 3. Set localName to extends. + * + * Special note for XUL elements: + * + * For step 7.1, we'll subject XUL to the same rules as HTML, so that a + * custom built-in element will not be extending from a dashed name. + * Step 7.2 is disregarded. But, we do check if the name is a dashed name + * (i.e. step 2) given that there is no reason for a custom built-in element + * type to take on a non-dashed name. + * This also ensures the name of the built-in custom element type can never + * be the same as the built-in element name, so we don't break the assumption + * elsewhere. + */ + nsAutoString localName(aName); + if (aOptions.mExtends.WasPassed()) { + doc->SetUseCounter(eUseCounter_custom_CustomizedBuiltin); + + RefPtr<nsAtom> extendsAtom(NS_Atomize(aOptions.mExtends.Value())); + if (nsContentUtils::IsCustomElementName(extendsAtom, kNameSpaceID_XHTML)) { + aRv.ThrowNotSupportedError( + nsPrintfCString("'%s' cannot extend a custom element", + NS_ConvertUTF16toUTF8(aName).get())); + return; + } + + if (nameSpaceID == kNameSpaceID_XHTML) { + // bgsound and multicol are unknown html element. + int32_t tag = nsHTMLTags::CaseSensitiveAtomTagToId(extendsAtom); + if (tag == eHTMLTag_userdefined || tag == eHTMLTag_bgsound || + tag == eHTMLTag_multicol) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return; + } + } else { // kNameSpaceID_XUL + // As stated above, ensure the name of the customized built-in element + // (the one that goes to the |is| attribute) is a dashed name. + if (!nsContentUtils::IsNameWithDash(nameAtom)) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return; + } + } + + localName.Assign(aOptions.mExtends.Value()); + } + + /** + * 8. If this CustomElementRegistry's element definition is running flag is + * set, then throw a "NotSupportedError" DOMException and abort these steps. + */ + if (mIsCustomDefinitionRunning) { + aRv.ThrowNotSupportedError( + "Cannot define a custom element while defining another custom element"); + return; + } + + auto callbacksHolder = MakeUnique<LifecycleCallbacks>(); + auto formAssociatedCallbacksHolder = + MakeUnique<FormAssociatedLifecycleCallbacks>(); + nsTArray<RefPtr<nsAtom>> observedAttributes; + AutoTArray<RefPtr<nsAtom>, 2> disabledFeatures; + bool formAssociated = false; + bool disableInternals = false; + bool disableShadow = false; + { // Set mIsCustomDefinitionRunning. + /** + * 9. Set this CustomElementRegistry's element definition is running flag. + */ + AutoRestore<bool> restoreRunning(mIsCustomDefinitionRunning); + mIsCustomDefinitionRunning = true; + + /** + * 14.1. Let prototype be Get(constructor, "prototype"). Rethrow any + * exceptions. + */ + // The .prototype on the constructor passed could be an "expando" of a + // wrapper. So we should get it from wrapper instead of the underlying + // object. + JS::Rooted<JS::Value> prototype(aCx); + if (!JS_GetProperty(aCx, constructor, "prototype", &prototype)) { + aRv.NoteJSContextException(aCx); + return; + } + + /** + * 14.2. If Type(prototype) is not Object, then throw a TypeError exception. + */ + if (!prototype.isObject()) { + aRv.ThrowTypeError<MSG_NOT_OBJECT>("constructor.prototype"); + return; + } + + /** + * 14.3. Let lifecycleCallbacks be a map with the four keys + * "connectedCallback", "disconnectedCallback", "adoptedCallback", and + * "attributeChangedCallback", each of which belongs to an entry whose + * value is null. The 'getCustomInterface' callback is also included + * for chrome usage. + * 14.4. For each of the four keys callbackName in lifecycleCallbacks: + * 1. Let callbackValue be Get(prototype, callbackName). Rethrow any + * exceptions. + * 2. If callbackValue is not undefined, then set the value of the + * entry in lifecycleCallbacks with key callbackName to the result + * of converting callbackValue to the Web IDL Function callback + * type. Rethrow any exceptions from the conversion. + */ + if (!callbacksHolder->Init(aCx, prototype)) { + aRv.NoteJSContextException(aCx); + return; + } + + /** + * 14.5. If the value of the entry in lifecycleCallbacks with key + * "attributeChangedCallback" is not null, then: + * 1. Let observedAttributesIterable be Get(constructor, + * "observedAttributes"). Rethrow any exceptions. + * 2. If observedAttributesIterable is not undefined, then set + * observedAttributes to the result of converting + * observedAttributesIterable to a sequence<DOMString>. Rethrow + * any exceptions from the conversion. + */ + if (callbacksHolder->mAttributeChangedCallback.WasPassed()) { + if (!JSObjectToAtomArray(aCx, constructor, u"observedAttributes"_ns, + observedAttributes, aRv)) { + return; + } + } + + /** + * 14.6. Let disabledFeatures be an empty sequence<DOMString>. + * 14.7. Let disabledFeaturesIterable be Get(constructor, + * "disabledFeatures"). Rethrow any exceptions. + * 14.8. If disabledFeaturesIterable is not undefined, then set + * disabledFeatures to the result of converting + * disabledFeaturesIterable to a sequence<DOMString>. + * Rethrow any exceptions from the conversion. + */ + if (!JSObjectToAtomArray(aCx, constructor, u"disabledFeatures"_ns, + disabledFeatures, aRv)) { + return; + } + + // 14.9. Set disableInternals to true if disabledFeaturesSequence contains + // "internals". + disableInternals = disabledFeatures.Contains( + static_cast<nsStaticAtom*>(nsGkAtoms::internals)); + + // 14.10. Set disableShadow to true if disabledFeaturesSequence contains + // "shadow". + disableShadow = disabledFeatures.Contains( + static_cast<nsStaticAtom*>(nsGkAtoms::shadow)); + + // 14.11. Let formAssociatedValue be Get(constructor, "formAssociated"). + // Rethrow any exceptions. + JS::Rooted<JS::Value> formAssociatedValue(aCx); + if (!JS_GetProperty(aCx, constructor, "formAssociated", + &formAssociatedValue)) { + aRv.NoteJSContextException(aCx); + return; + } + + // 14.12. Set formAssociated to the result of converting + // formAssociatedValue to a boolean. Rethrow any exceptions from + // the conversion. + if (!ValueToPrimitive<bool, eDefault>(aCx, formAssociatedValue, + "formAssociated", &formAssociated)) { + aRv.NoteJSContextException(aCx); + return; + } + + /** + * 14.13. If formAssociated is true, for each of "formAssociatedCallback", + * "formResetCallback", "formDisabledCallback", and + * "formStateRestoreCallback" callbackName: + * 1. Let callbackValue be ? Get(prototype, callbackName). + * 2. If callbackValue is not undefined, then set the value of the + * entry in lifecycleCallbacks with key callbackName to the result + * of converting callbackValue to the Web IDL Function callback + * type. Rethrow any exceptions from the conversion. + */ + if (formAssociated && + !formAssociatedCallbacksHolder->Init(aCx, prototype)) { + aRv.NoteJSContextException(aCx); + return; + } + } // Unset mIsCustomDefinitionRunning + + /** + * 15. Let definition be a new custom element definition with name name, + * local name localName, constructor constructor, prototype prototype, + * observed attributes observedAttributes, and lifecycle callbacks + * lifecycleCallbacks. + */ + // Associate the definition with the custom element. + RefPtr<nsAtom> localNameAtom(NS_Atomize(localName)); + + /** + * 16. Add definition to this CustomElementRegistry. + */ + if (!mConstructors.put(constructorUnwrapped, nameAtom)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<CustomElementDefinition> definition = new CustomElementDefinition( + nameAtom, localNameAtom, nameSpaceID, &aFunctionConstructor, + std::move(observedAttributes), std::move(callbacksHolder), + std::move(formAssociatedCallbacksHolder), formAssociated, + disableInternals, disableShadow); + + CustomElementDefinition* def = definition.get(); + mCustomDefinitions.InsertOrUpdate(nameAtom, std::move(definition)); + + MOZ_ASSERT(mCustomDefinitions.Count() == mConstructors.count(), + "Number of entries should be the same"); + + /** + * 17. 18. 19. Upgrade candidates + */ + UpgradeCandidates(nameAtom, def, aRv); + + /** + * 20. If this CustomElementRegistry's when-defined promise map contains an + * entry with key name: + * 1. Let promise be the value of that entry. + * 2. Resolve promise with undefined. + * 3. Delete the entry with key name from this CustomElementRegistry's + * when-defined promise map. + */ + RefPtr<Promise> promise; + mWhenDefinedPromiseMap.Remove(nameAtom, getter_AddRefs(promise)); + if (promise) { + promise->MaybeResolve(def->mConstructor); + } + + // Dispatch a "customelementdefined" event for DevTools. + { + JSString* nameJsStr = + JS_NewUCStringCopyN(aCx, aName.BeginReading(), aName.Length()); + + JS::Rooted<JS::Value> detail(aCx, JS::StringValue(nameJsStr)); + RefPtr<CustomEvent> event = NS_NewDOMCustomEvent(doc, nullptr, nullptr); + event->InitCustomEvent(aCx, u"customelementdefined"_ns, + /* CanBubble */ true, + /* Cancelable */ true, detail); + event->SetTrusted(true); + + AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(doc, event); + dispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes; + + dispatcher->PostDOMEvent(); + } + + /** + * Clean-up mElementCreationCallbacks (if it exists) + */ + mElementCreationCallbacks.Remove(nameAtom); +} + +void CustomElementRegistry::SetElementCreationCallback( + const nsAString& aName, CustomElementCreationCallback& aCallback, + ErrorResult& aRv) { + RefPtr<nsAtom> nameAtom(NS_Atomize(aName)); + if (mElementCreationCallbacks.GetWeak(nameAtom) || + mCustomDefinitions.GetWeak(nameAtom)) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return; + } + + RefPtr<CustomElementCreationCallback> callback = &aCallback; + mElementCreationCallbacks.InsertOrUpdate(nameAtom, std::move(callback)); +} + +void CustomElementRegistry::Upgrade(nsINode& aRoot) { + for (nsINode* node : ShadowIncludingTreeIterator(aRoot)) { + Element* element = Element::FromNode(node); + if (!element) { + continue; + } + + CustomElementData* ceData = element->GetCustomElementData(); + if (ceData) { + NodeInfo* nodeInfo = element->NodeInfo(); + nsAtom* typeAtom = ceData->GetCustomElementType(); + CustomElementDefinition* definition = + nsContentUtils::LookupCustomElementDefinition( + nodeInfo->GetDocument(), nodeInfo->NameAtom(), + nodeInfo->NamespaceID(), typeAtom); + if (definition) { + nsContentUtils::EnqueueUpgradeReaction(element, definition); + } + } + } +} + +void CustomElementRegistry::Get( + const nsAString& aName, + OwningCustomElementConstructorOrUndefined& aRetVal) { + RefPtr<nsAtom> nameAtom(NS_Atomize(aName)); + CustomElementDefinition* data = mCustomDefinitions.GetWeak(nameAtom); + + if (!data) { + aRetVal.SetUndefined(); + return; + } + + aRetVal.SetAsCustomElementConstructor() = data->mConstructor; +} + +already_AddRefed<Promise> CustomElementRegistry::WhenDefined( + const nsAString& aName, ErrorResult& aRv) { + // Define a function that lazily creates a Promise and perform some action on + // it when creation succeeded. It's needed in multiple cases below, but not in + // all of them. + auto createPromise = [&](auto&& action) -> already_AddRefed<Promise> { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow); + RefPtr<Promise> promise = Promise::Create(global, aRv); + + if (aRv.Failed()) { + return nullptr; + } + + action(promise); + + return promise.forget(); + }; + + RefPtr<nsAtom> nameAtom(NS_Atomize(aName)); + Document* doc = mWindow->GetExtantDoc(); + uint32_t nameSpaceID = + doc ? doc->GetDefaultNamespaceID() : kNameSpaceID_XHTML; + if (!nsContentUtils::IsCustomElementName(nameAtom, nameSpaceID)) { + return createPromise([](const RefPtr<Promise>& promise) { + promise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR); + }); + } + + if (CustomElementDefinition* definition = + mCustomDefinitions.GetWeak(nameAtom)) { + return createPromise([&](const RefPtr<Promise>& promise) { + promise->MaybeResolve(definition->mConstructor); + }); + } + + return mWhenDefinedPromiseMap.WithEntryHandle( + nameAtom, [&](auto&& entry) -> already_AddRefed<Promise> { + if (!entry) { + return createPromise([&entry](const RefPtr<Promise>& promise) { + entry.Insert(promise); + }); + } + return do_AddRef(entry.Data()); + }); +} + +namespace { + +MOZ_CAN_RUN_SCRIPT +static void DoUpgrade(Element* aElement, CustomElementDefinition* aDefinition, + CustomElementConstructor* aConstructor, + ErrorResult& aRv) { + if (aDefinition->mDisableShadow && aElement->GetShadowRoot()) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Custom element upgrade to '%s' is disabled because a shadow root " + "already exists", + NS_ConvertUTF16toUTF8(aDefinition->mType->GetUTF16String()).get())); + return; + } + + CustomElementData* data = aElement->GetCustomElementData(); + MOZ_ASSERT(data, "CustomElementData should exist"); + data->mState = CustomElementData::State::ePrecustomized; + + JS::Rooted<JS::Value> constructResult(RootingCx()); + // Rethrow the exception since it might actually throw the exception from the + // upgrade steps back out to the caller of document.createElement. + aConstructor->Construct(&constructResult, aRv, "Custom Element Upgrade", + CallbackFunction::eRethrowExceptions); + if (aRv.Failed()) { + return; + } + + Element* element; + // constructResult is an ObjectValue because construction with a callback + // always forms the return value from a JSObject. + if (NS_FAILED(UNWRAP_OBJECT(Element, &constructResult, element)) || + element != aElement) { + aRv.ThrowTypeError("Custom element constructor returned a wrong element"); + return; + } +} + +} // anonymous namespace + +// https://html.spec.whatwg.org/commit-snapshots/2793ee4a461c6c39896395f1a45c269ea820c47e/#upgrades +/* static */ +void CustomElementRegistry::Upgrade(Element* aElement, + CustomElementDefinition* aDefinition, + ErrorResult& aRv) { + CustomElementData* data = aElement->GetCustomElementData(); + MOZ_ASSERT(data, "CustomElementData should exist"); + + // Step 1. + if (data->mState != CustomElementData::State::eUndefined) { + return; + } + + // Step 2. + aElement->SetCustomElementDefinition(aDefinition); + + // Step 3. + data->mState = CustomElementData::State::eFailed; + + // Step 4. + if (!aDefinition->mObservedAttributes.IsEmpty()) { + uint32_t count = aElement->GetAttrCount(); + for (uint32_t i = 0; i < count; i++) { + mozilla::dom::BorrowedAttrInfo info = aElement->GetAttrInfoAt(i); + + const nsAttrName* name = info.mName; + nsAtom* attrName = name->LocalName(); + + if (aDefinition->IsInObservedAttributeList(attrName)) { + int32_t namespaceID = name->NamespaceID(); + nsAutoString attrValue, namespaceURI; + info.mValue->ToString(attrValue); + nsNameSpaceManager::GetInstance()->GetNameSpaceURI(namespaceID, + namespaceURI); + + LifecycleCallbackArgs args; + args.mName = nsDependentAtomString(attrName); + args.mOldValue = VoidString(); + args.mNewValue = attrValue; + args.mNamespaceURI = + (namespaceURI.IsEmpty() ? VoidString() : namespaceURI); + + nsContentUtils::EnqueueLifecycleCallback( + ElementCallbackType::eAttributeChanged, aElement, args, + aDefinition); + } + } + } + + // Step 5. + if (aElement->IsInComposedDoc()) { + nsContentUtils::EnqueueLifecycleCallback(ElementCallbackType::eConnected, + aElement, {}, aDefinition); + } + + // Step 6. + AutoConstructionStackEntry acs(aDefinition->mConstructionStack, aElement); + + // Step 7 and step 8. + DoUpgrade(aElement, aDefinition, MOZ_KnownLive(aDefinition->mConstructor), + aRv); + if (aRv.Failed()) { + MOZ_ASSERT(data->mState == CustomElementData::State::eFailed || + data->mState == CustomElementData::State::ePrecustomized); + // Spec doesn't set custom element state to failed here, but without this we + // would have inconsistent state on a custom elemet that is failed to + // upgrade, see https://github.com/whatwg/html/issues/6929, and + // https://github.com/web-platform-tests/wpt/pull/29911 for the test. + data->mState = CustomElementData::State::eFailed; + aElement->SetCustomElementDefinition(nullptr); + // Empty element's custom element reaction queue. + data->mReactionQueue.Clear(); + return; + } + + // Step 9. + if (data->IsFormAssociated()) { + ElementInternals* internals = data->GetElementInternals(); + MOZ_ASSERT(internals); + MOZ_ASSERT(aElement->IsHTMLElement()); + MOZ_ASSERT(!aDefinition->IsCustomBuiltIn()); + + internals->UpdateFormOwner(); + } + + // Step 10. + data->mState = CustomElementData::State::eCustom; + aElement->SetDefined(true); +} + +already_AddRefed<nsISupports> CustomElementRegistry::CallGetCustomInterface( + Element* aElement, const nsIID& aIID) { + MOZ_ASSERT(aElement); + + if (!nsContentUtils::IsChromeDoc(aElement->OwnerDoc())) { + return nullptr; + } + + // Try to get our GetCustomInterfaceCallback callback. + CustomElementDefinition* definition = aElement->GetCustomElementDefinition(); + if (!definition || !definition->mCallbacks || + !definition->mCallbacks->mGetCustomInterfaceCallback.WasPassed() || + (definition->mLocalName != aElement->NodeInfo()->NameAtom())) { + return nullptr; + } + LifecycleGetCustomInterfaceCallback* func = + definition->mCallbacks->mGetCustomInterfaceCallback.Value(); + + // Initialize a AutoJSAPI to enter the compartment of the callback. + AutoJSAPI jsapi; + JS::Rooted<JSObject*> funcGlobal(RootingCx(), func->CallbackGlobalOrNull()); + if (!funcGlobal || !jsapi.Init(funcGlobal)) { + return nullptr; + } + + // Grab our JSContext. + JSContext* cx = jsapi.cx(); + + // Convert our IID to a JSValue to call our callback. + JS::Rooted<JS::Value> jsiid(cx); + if (!xpc::ID2JSValue(cx, aIID, &jsiid)) { + return nullptr; + } + + JS::Rooted<JSObject*> customInterface(cx); + func->Call(aElement, jsiid, &customInterface); + if (!customInterface) { + return nullptr; + } + + // Wrap our JSObject into a nsISupports through XPConnect + nsCOMPtr<nsISupports> wrapper; + nsresult rv = nsContentUtils::XPConnect()->WrapJSAggregatedToNative( + aElement, cx, customInterface, aIID, getter_AddRefs(wrapper)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return wrapper.forget(); +} + +void CustomElementRegistry::TraceDefinitions(JSTracer* aTrc) { + for (const RefPtr<CustomElementDefinition>& definition : + mCustomDefinitions.Values()) { + if (definition && definition->mConstructor) { + mozilla::TraceScriptHolder(definition->mConstructor, aTrc); + } + } +} + +//----------------------------------------------------- +// CustomElementReactionsStack + +void CustomElementReactionsStack::CreateAndPushElementQueue() { + MOZ_ASSERT(mRecursionDepth); + MOZ_ASSERT(!mIsElementQueuePushedForCurrentRecursionDepth); + + // Push a new element queue onto the custom element reactions stack. + mReactionsStack.AppendElement(MakeUnique<ElementQueue>()); + mIsElementQueuePushedForCurrentRecursionDepth = true; +} + +void CustomElementReactionsStack::PopAndInvokeElementQueue() { + MOZ_ASSERT(mRecursionDepth); + MOZ_ASSERT(mIsElementQueuePushedForCurrentRecursionDepth); + MOZ_ASSERT(!mReactionsStack.IsEmpty(), "Reaction stack shouldn't be empty"); + + // Pop the element queue from the custom element reactions stack, + // and invoke custom element reactions in that queue. + const uint32_t lastIndex = mReactionsStack.Length() - 1; + ElementQueue* elementQueue = mReactionsStack.ElementAt(lastIndex).get(); + // Check element queue size in order to reduce function call overhead. + if (!elementQueue->IsEmpty()) { + // It is still not clear what error reporting will look like in custom + // element, see https://github.com/w3c/webcomponents/issues/635. + // We usually report the error to entry global in gecko, so just follow the + // same behavior here. + // This may be null if it's called from parser, see the case of + // attributeChangedCallback in + // https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token + // In that case, the exception of callback reactions will be automatically + // reported in CallSetup. + nsIGlobalObject* global = GetEntryGlobal(); + InvokeReactions(elementQueue, MOZ_KnownLive(global)); + } + + // InvokeReactions() might create other custom element reactions, but those + // new reactions should be already consumed and removed at this point. + MOZ_ASSERT( + lastIndex == mReactionsStack.Length() - 1, + "reactions created by InvokeReactions() should be consumed and removed"); + + mReactionsStack.RemoveLastElement(); + mIsElementQueuePushedForCurrentRecursionDepth = false; +} + +void CustomElementReactionsStack::EnqueueUpgradeReaction( + Element* aElement, CustomElementDefinition* aDefinition) { + Enqueue(aElement, new CustomElementUpgradeReaction(aDefinition)); +} + +void CustomElementReactionsStack::EnqueueCallbackReaction( + Element* aElement, + UniquePtr<CustomElementCallback> aCustomElementCallback) { + Enqueue(aElement, + new CustomElementCallbackReaction(std::move(aCustomElementCallback))); +} + +void CustomElementReactionsStack::Enqueue(Element* aElement, + CustomElementReaction* aReaction) { + CustomElementData* elementData = aElement->GetCustomElementData(); + MOZ_ASSERT(elementData, "CustomElementData should exist"); + + if (mRecursionDepth) { + // If the element queue is not created for current recursion depth, create + // and push an element queue to reactions stack first. + if (!mIsElementQueuePushedForCurrentRecursionDepth) { + CreateAndPushElementQueue(); + } + + MOZ_ASSERT(!mReactionsStack.IsEmpty()); + // Add element to the current element queue. + mReactionsStack.LastElement()->AppendElement(aElement); + elementData->mReactionQueue.AppendElement(aReaction); + return; + } + + // If the custom element reactions stack is empty, then: + // Add element to the backup element queue. + MOZ_ASSERT(mReactionsStack.IsEmpty(), + "custom element reactions stack should be empty"); + mBackupQueue.AppendElement(aElement); + elementData->mReactionQueue.AppendElement(aReaction); + + if (mIsBackupQueueProcessing) { + return; + } + + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + RefPtr<BackupQueueMicroTask> bqmt = new BackupQueueMicroTask(this); + context->DispatchToMicroTask(bqmt.forget()); +} + +void CustomElementReactionsStack::InvokeBackupQueue() { + // Check backup queue size in order to reduce function call overhead. + if (!mBackupQueue.IsEmpty()) { + // Upgrade reactions won't be scheduled in backup queue and the exception of + // callback reactions will be automatically reported in CallSetup. + // If the reactions are invoked from backup queue (in microtask check + // point), we don't need to pass global object for error reporting. + InvokeReactions(&mBackupQueue, nullptr); + } + MOZ_ASSERT( + mBackupQueue.IsEmpty(), + "There are still some reactions in BackupQueue not being consumed!?!"); +} + +void CustomElementReactionsStack::InvokeReactions(ElementQueue* aElementQueue, + nsIGlobalObject* aGlobal) { + // This is used for error reporting. + Maybe<AutoEntryScript> aes; + if (aGlobal) { + aes.emplace(aGlobal, "custom elements reaction invocation"); + } + + // Note: It's possible to re-enter this method. + for (uint32_t i = 0; i < aElementQueue->Length(); ++i) { + Element* element = aElementQueue->ElementAt(i); + // ElementQueue hold a element's strong reference, it should not be a + // nullptr. + MOZ_ASSERT(element); + + CustomElementData* elementData = element->GetCustomElementData(); + if (!elementData || !element->GetOwnerGlobal()) { + // This happens when the document is destroyed and the element is already + // unlinked, no need to fire the callbacks in this case. + continue; + } + + auto& reactions = elementData->mReactionQueue; + for (uint32_t j = 0; j < reactions.Length(); ++j) { + // Transfer the ownership of the entry due to reentrant invocation of + // this function. + auto reaction(std::move(reactions.ElementAt(j))); + if (reaction) { + if (!aGlobal && reaction->IsUpgradeReaction()) { + nsIGlobalObject* global = element->GetOwnerGlobal(); + MOZ_ASSERT(!aes); + aes.emplace(global, "custom elements reaction invocation"); + } + ErrorResult rv; + reaction->Invoke(MOZ_KnownLive(element), rv); + if (aes) { + JSContext* cx = aes->cx(); + if (rv.MaybeSetPendingException(cx)) { + aes->ReportException(); + } + MOZ_ASSERT(!JS_IsExceptionPending(cx)); + if (!aGlobal && reaction->IsUpgradeReaction()) { + aes.reset(); + } + } + MOZ_ASSERT(!rv.Failed()); + } + } + reactions.Clear(); + } + aElementQueue->Clear(); +} + +//----------------------------------------------------- +// CustomElementDefinition + +NS_IMPL_CYCLE_COLLECTION(CustomElementDefinition, mConstructor, mCallbacks, + mFormAssociatedCallbacks, mConstructionStack) + +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) + : mType(aType), + mLocalName(aLocalName), + mNamespaceID(aNamespaceID), + mConstructor(aConstructor), + mObservedAttributes(std::move(aObservedAttributes)), + mCallbacks(std::move(aCallbacks)), + mFormAssociatedCallbacks(std::move(aFormAssociatedCallbacks)), + mFormAssociated(aFormAssociated), + mDisableInternals(aDisableInternals), + mDisableShadow(aDisableShadow) {} + +} // namespace mozilla::dom |