gecko-dev/dom/base/CustomElementRegistry.cpp
Timothy Guan-tin Chien 9b93107cb5 Bug 1462703 - Upgrade the created element after callback runs r=smaug
nsContentUtils::NS_NewXULOrHTMLElement will call into
CustomElementRegisty::RegisterCallbackUpgradeElement, which keeps
the newly created element, allowing RunCustomElementCreationCallback
to upgrade them after the callback runs.

It is unclear if this changes the order of constructor executions,
but even so it should not affact our use case.

MozReview-Commit-ID: LWTn7B35aBv

--HG--
extra : rebase_source : 15382431f34dd887c14142ff47337e8d1eec74ef
extra : source : 47058a61951c2974514e41e316e5370cfa4f9d8b
2018-05-23 13:04:18 +08:00

1355 lines
43 KiB
C++

/* -*- 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/CycleCollectedJSContext.h"
#include "mozilla/dom/CustomElementRegistryBinding.h"
#include "mozilla/dom/HTMLElementBinding.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WebComponentsBinding.h"
#include "mozilla/dom/DocGroup.h"
#include "nsHTMLTags.h"
#include "jsapi.h"
#include "nsGlobalWindow.h"
namespace mozilla {
namespace dom {
//-----------------------------------------------------
// CustomElementUpgradeReaction
class CustomElementUpgradeReaction final : public CustomElementReaction
{
public:
explicit CustomElementUpgradeReaction(CustomElementDefinition* aDefinition)
: mDefinition(aDefinition)
{
mIsUpgradeReaction = true;
}
private:
virtual void Invoke(Element* aElement, ErrorResult& aRv) override
{
CustomElementRegistry::Upgrade(aElement, mDefinition, aRv);
}
CustomElementDefinition* mDefinition;
};
//-----------------------------------------------------
// CustomElementCallbackReaction
class CustomElementCallbackReaction final : public CustomElementReaction
{
public:
explicit CustomElementCallbackReaction(UniquePtr<CustomElementCallback> aCustomElementCallback)
: mCustomElementCallback(Move(aCustomElementCallback))
{
}
virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const override
{
mCustomElementCallback->Traverse(aCb);
}
private:
virtual void Invoke(Element* aElement, ErrorResult& aRv) override
{
mCustomElementCallback->Call();
}
UniquePtr<CustomElementCallback> mCustomElementCallback;
};
//-----------------------------------------------------
// CustomElementCallback
void
CustomElementCallback::Call()
{
switch (mType) {
case nsIDocument::eConnected:
static_cast<LifecycleConnectedCallback *>(mCallback.get())->Call(mThisObject);
break;
case nsIDocument::eDisconnected:
static_cast<LifecycleDisconnectedCallback *>(mCallback.get())->Call(mThisObject);
break;
case nsIDocument::eAdopted:
static_cast<LifecycleAdoptedCallback *>(mCallback.get())->Call(mThisObject,
mAdoptedCallbackArgs.mOldDocument, mAdoptedCallbackArgs.mNewDocument);
break;
case nsIDocument::eAttributeChanged:
static_cast<LifecycleAttributeChangedCallback *>(mCallback.get())->Call(mThisObject,
mArgs.name, mArgs.oldValue, mArgs.newValue, mArgs.namespaceURI);
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);
}
CustomElementCallback::CustomElementCallback(Element* aThisObject,
nsIDocument::ElementCallbackType aCallbackType,
mozilla::dom::CallbackFunction* aCallback)
: mThisObject(aThisObject),
mCallback(aCallback),
mType(aCallbackType)
{
}
//-----------------------------------------------------
// CustomElementConstructor
already_AddRefed<Element>
CustomElementConstructor::Construct(const char* aExecutionReason,
ErrorResult& aRv)
{
CallSetup s(this, aRv, aExecutionReason,
CallbackFunction::eRethrowExceptions);
JSContext* cx = s.GetContext();
if (!cx) {
MOZ_ASSERT(aRv.Failed());
return nullptr;
}
JS::Rooted<JSObject*> result(cx);
JS::Rooted<JS::Value> constructor(cx, JS::ObjectValue(*mCallback));
if (!JS::Construct(cx, constructor, JS::HandleValueArray::empty(), &result)) {
aRv.NoteJSContextException(cx);
return nullptr;
}
RefPtr<Element> element;
if (NS_FAILED(UNWRAP_OBJECT(Element, &result, element))) {
return nullptr;
}
return element.forget();
}
//-----------------------------------------------------
// 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)
{
MOZ_ASSERT(mState == State::eCustom);
MOZ_ASSERT(!mCustomElementDefinition);
MOZ_ASSERT(aDefinition->mType == mType);
mCustomElementDefinition = aDefinition;
}
CustomElementDefinition*
CustomElementData::GetCustomElementDefinition()
{
MOZ_ASSERT(mCustomElementDefinition ? mState == State::eCustom
: mState != State::eCustom);
return mCustomElementDefinition;
}
nsAtom*
CustomElementData::GetCustomElementType()
{
return mType;
}
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));
}
}
void
CustomElementData::Unlink()
{
mReactionQueue.Clear();
mCustomElementDefinition = nullptr;
}
//-----------------------------------------------------
// CustomElementRegistry
namespace {
class MOZ_RAII AutoConstructionStackEntry final
{
public:
AutoConstructionStackEntry(nsTArray<RefPtr<Element>>& aStack,
Element* aElement)
: mStack(aStack)
{
MOZ_ASSERT(aElement->IsHTMLElement() || aElement->IsXULElement());
mIndex = mStack.Length();
mStack.AppendElement(aElement);
}
~AutoConstructionStackEntry()
{
MOZ_ASSERT(mIndex == mStack.Length() - 1,
"Removed element should be the last element");
mStack.RemoveElementAt(mIndex);
}
private:
nsTArray<RefPtr<Element>>& mStack;
uint32_t mIndex;
};
} // namespace anonymous
// Only needed for refcounted objects.
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 (ConstructorMap::Enum iter(tmp->mConstructors); !iter.empty(); iter.popFront()) {
aCallbacks.Trace(&iter.front().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);
MOZ_ALWAYS_TRUE(mConstructors.init());
mozilla::HoldJSObjects(this);
}
CustomElementRegistry::~CustomElementRegistry()
{
mozilla::DropJSObjects(this);
}
bool
CustomElementRegistry::IsCustomElementEnabled(JSContext* aCx, JSObject* aObject)
{
if (nsContentUtils::IsCustomElementsEnabled()) {
return true;
}
return XRE_IsParentProcess() && nsContentUtils::AllowXULXBLForPrincipal(nsContentUtils::ObjectPrincipal(aObject));
}
bool
CustomElementRegistry::IsCustomElementEnabled(nsIDocument* aDoc)
{
if (nsContentUtils::IsCustomElementsEnabled()) {
return true;
}
return XRE_IsParentProcess() && aDoc->AllowXULXBL();
}
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.");
CustomElementDefinition* definition =
mRegistry->mCustomDefinitions.GetWeak(mAtom);
MOZ_ASSERT(definition, "Callback should define the definition of type.");
MOZ_ASSERT(!mRegistry->mElementCreationCallbacks.GetWeak(mAtom),
"Callback should be removed.");
nsAutoPtr<nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>> elements;
mRegistry->mElementCreationCallbacksUpgradeCandidatesMap.Remove(mAtom, &elements);
MOZ_ASSERT(elements, "There should be a list");
for (auto iter = elements->Iter(); !iter.Done(); iter.Next()) {
nsCOMPtr<Element> elem = do_QueryReferent(iter.Get()->GetKey());
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,
nsAtom* aTypeAtom)
{
CustomElementDefinition* data = mCustomDefinitions.GetWeak(aTypeAtom);
if (!data) {
RefPtr<CustomElementCreationCallback> callback;
mElementCreationCallbacks.Get(aTypeAtom, getter_AddRefs(callback));
if (callback) {
mElementCreationCallbacks.Remove(aTypeAtom);
mElementCreationCallbacksUpgradeCandidatesMap.LookupOrAdd(aTypeAtom);
RefPtr<Runnable> runnable =
new RunCustomElementCreationCallback(this, aTypeAtom, callback);
nsContentUtils::AddScriptRunner(runnable);
data = mCustomDefinitions.GetWeak(aTypeAtom);
}
}
if (data && data->mLocalName == aNameAtom) {
return data;
}
return nullptr;
}
CustomElementDefinition*
CustomElementRegistry::LookupCustomElementDefinition(JSContext* aCx,
JSObject* aConstructor) const
{
JS::Rooted<JSObject*> constructor(aCx, js::CheckedUnwrap(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;
}
nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>* unresolved =
mCandidatesMap.LookupOrAdd(typeName);
nsWeakPtr elem = do_GetWeakReference(aElement);
unresolved->PutEntry(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
nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>* candidates = nullptr;
if (mCandidatesMap.Get(aTypeName, &candidates)) {
MOZ_ASSERT(candidates);
candidates->RemoveEntry(weak);
}
}
/* static */ UniquePtr<CustomElementCallback>
CustomElementRegistry::CreateCustomElementCallback(
nsIDocument::ElementCallbackType aType, Element* aCustomElement,
LifecycleCallbackArgs* aArgs,
LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
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 nsIDocument::eConnected:
if (aDefinition->mCallbacks->mConnectedCallback.WasPassed()) {
func = aDefinition->mCallbacks->mConnectedCallback.Value();
}
break;
case nsIDocument::eDisconnected:
if (aDefinition->mCallbacks->mDisconnectedCallback.WasPassed()) {
func = aDefinition->mCallbacks->mDisconnectedCallback.Value();
}
break;
case nsIDocument::eAdopted:
if (aDefinition->mCallbacks->mAdoptedCallback.WasPassed()) {
func = aDefinition->mCallbacks->mAdoptedCallback.Value();
}
break;
case nsIDocument::eAttributeChanged:
if (aDefinition->mCallbacks->mAttributeChangedCallback.WasPassed()) {
func = aDefinition->mCallbacks->mAttributeChangedCallback.Value();
}
break;
}
// If there is no such callback, stop.
if (!func) {
return nullptr;
}
// Add CALLBACK to ELEMENT's callback queue.
auto callback =
MakeUnique<CustomElementCallback>(aCustomElement, aType, func);
if (aArgs) {
callback->SetArgs(*aArgs);
}
if (aAdoptedCallbackArgs) {
callback->SetAdoptedCallbackArgs(*aAdoptedCallbackArgs);
}
return Move(callback);
}
/* static */ void
CustomElementRegistry::EnqueueLifecycleCallback(nsIDocument::ElementCallbackType aType,
Element* aCustomElement,
LifecycleCallbackArgs* aArgs,
LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
CustomElementDefinition* aDefinition)
{
CustomElementDefinition* definition = aDefinition;
if (!definition) {
definition = aCustomElement->GetCustomElementDefinition();
if (!definition ||
definition->mLocalName != aCustomElement->NodeInfo()->NameAtom()) {
return;
}
if (!definition->mCallbacks) {
// definition has been unlinked. Don't try to mess with it.
return;
}
}
auto callback =
CreateCustomElementCallback(aType, aCustomElement, aArgs,
aAdoptedCallbackArgs, definition);
if (!callback) {
return;
}
DocGroup* docGroup = aCustomElement->OwnerDoc()->GetDocGroup();
if (!docGroup) {
return;
}
if (aType == nsIDocument::eAttributeChanged) {
RefPtr<nsAtom> attrName = NS_Atomize(aArgs->name);
if (definition->mObservedAttributes.IsEmpty() ||
!definition->mObservedAttributes.Contains(attrName)) {
return;
}
}
CustomElementReactionsStack* reactionsStack =
docGroup->CustomElementReactionsStack();
reactionsStack->EnqueueCallbackReaction(aCustomElement, Move(callback));
}
namespace {
class CandidateFinder
{
public:
CandidateFinder(nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>& aCandidates,
nsIDocument* aDoc);
nsTArray<nsCOMPtr<Element>> OrderedCandidates();
private:
bool Traverse(Element* aRoot, nsTArray<nsCOMPtr<Element>>& aOrderedElements);
nsCOMPtr<nsIDocument> mDoc;
nsInterfaceHashtable<nsPtrHashKey<Element>, Element> mCandidates;
};
CandidateFinder::CandidateFinder(nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>& aCandidates,
nsIDocument* aDoc)
: mDoc(aDoc)
, mCandidates(aCandidates.Count())
{
MOZ_ASSERT(mDoc);
for (auto iter = aCandidates.Iter(); !iter.Done(); iter.Next()) {
nsCOMPtr<Element> elem = do_QueryReferent(iter.Get()->GetKey());
if (!elem) {
continue;
}
Element* key = elem.get();
mCandidates.Put(key, elem.forget());
}
}
nsTArray<nsCOMPtr<Element>>
CandidateFinder::OrderedCandidates()
{
if (mCandidates.Count() == 1) {
// Fast path for one candidate.
for (auto iter = mCandidates.Iter(); !iter.Done(); iter.Next()) {
nsTArray<nsCOMPtr<Element>> rval({ Move(iter.Data()) });
iter.Remove();
return rval;
}
}
nsTArray<nsCOMPtr<Element>> orderedElements(mCandidates.Count());
for (Element* child = mDoc->GetFirstElementChild(); child; child = child->GetNextElementSibling()) {
if (!Traverse(child, orderedElements)) {
break;
}
}
return orderedElements;
}
bool
CandidateFinder::Traverse(Element* aRoot, nsTArray<nsCOMPtr<Element>>& aOrderedElements)
{
nsCOMPtr<Element> elem;
if (mCandidates.Remove(aRoot, getter_AddRefs(elem))) {
aOrderedElements.AppendElement(Move(elem));
if (mCandidates.Count() == 0) {
return false;
}
}
if (ShadowRoot* root = aRoot->GetShadowRoot()) {
// First iterate the children of the shadow root if aRoot is a shadow host.
for (Element* child = root->GetFirstElementChild(); child;
child = child->GetNextElementSibling()) {
if (!Traverse(child, aOrderedElements)) {
return false;
}
}
}
// Iterate the explicit children of aRoot.
for (Element* child = aRoot->GetFirstElementChild(); child;
child = child->GetNextElementSibling()) {
if (!Traverse(child, aOrderedElements)) {
return false;
}
}
return true;
}
}
void
CustomElementRegistry::UpgradeCandidates(nsAtom* aKey,
CustomElementDefinition* aDefinition,
ErrorResult& aRv)
{
DocGroup* docGroup = mWindow->GetDocGroup();
if (!docGroup) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
nsAutoPtr<nsTHashtable<nsRefPtrHashKey<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 CustomElementRegistryBinding::Wrap(aCx, this, aGivenProto);
}
nsISupports* CustomElementRegistry::GetParentObject() const
{
return mWindow;
}
DocGroup*
CustomElementRegistry::GetDocGroup() const
{
return mWindow ? mWindow->GetDocGroup() : nullptr;
}
// https://html.spec.whatwg.org/multipage/scripting.html#element-definition
void
CustomElementRegistry::Define(JSContext* aCx,
const nsAString& aName,
Function& aFunctionConstructor,
const ElementDefinitionOptions& aOptions,
ErrorResult& aRv)
{
// Note: No calls that might run JS or trigger CC before this point, or
// there's a (vanishingly small) chance of our constructor being nulled
// before we access it.
JS::Rooted<JSObject*> constructor(aCx, aFunctionConstructor.CallableOrNull());
/**
* 1. If IsConstructor(constructor) is false, then throw a TypeError and abort
* these steps.
*/
// For now, all wrappers are constructable if they are callable. So we need to
// unwrap constructor to check it is really constructable.
JS::Rooted<JSObject*> constructorUnwrapped(aCx, js::CheckedUnwrap(constructor));
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;
}
if (!JS::IsConstructor(constructorUnwrapped)) {
aRv.ThrowTypeError<MSG_NOT_CONSTRUCTOR>(NS_LITERAL_STRING("Argument 2 of CustomElementRegistry.define"));
return;
}
/**
* 2. If name is not a valid custom element name, then throw a "SyntaxError"
* DOMException and abort these steps.
*/
nsIDocument* doc = mWindow->GetExtantDoc();
uint32_t nameSpaceID = doc ? doc->GetDefaultNamespaceID() : kNameSpaceID_XHTML;
RefPtr<nsAtom> nameAtom(NS_Atomize(aName));
if (!nsContentUtils::IsCustomElementName(nameAtom, nameSpaceID)) {
aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
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.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
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");
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
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()) {
RefPtr<nsAtom> extendsAtom(NS_Atomize(aOptions.mExtends.Value()));
if (nsContentUtils::IsCustomElementName(extendsAtom, kNameSpaceID_XHTML)) {
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
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.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return;
}
auto callbacksHolder = MakeUnique<LifecycleCallbacks>();
nsTArray<RefPtr<nsAtom>> observedAttributes;
{ // Set mIsCustomDefinitionRunning.
/**
* 9. Set this CustomElementRegistry's element definition is running flag.
*/
AutoSetRunningFlag as(this);
/**
* 10.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;
}
/**
* 10.2. If Type(prototype) is not Object, then throw a TypeError exception.
*/
if (!prototype.isObject()) {
aRv.ThrowTypeError<MSG_NOT_OBJECT>(NS_LITERAL_STRING("constructor.prototype"));
return;
}
/**
* 10.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.
* 10.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;
}
/**
* 10.5. Let observedAttributes be an empty sequence<DOMString>.
* 10.6. 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()) {
JS::Rooted<JS::Value> observedAttributesIterable(aCx);
if (!JS_GetProperty(aCx, constructor, "observedAttributes",
&observedAttributesIterable)) {
aRv.NoteJSContextException(aCx);
return;
}
if (!observedAttributesIterable.isUndefined()) {
if (!observedAttributesIterable.isObject()) {
aRv.ThrowTypeError<MSG_NOT_SEQUENCE>(NS_LITERAL_STRING("observedAttributes"));
return;
}
JS::ForOfIterator iter(aCx);
if (!iter.init(observedAttributesIterable, JS::ForOfIterator::AllowNonIterable)) {
aRv.NoteJSContextException(aCx);
return;
}
if (!iter.valueIsIterable()) {
aRv.ThrowTypeError<MSG_NOT_SEQUENCE>(NS_LITERAL_STRING("observedAttributes"));
return;
}
JS::Rooted<JS::Value> attribute(aCx);
while (true) {
bool done;
if (!iter.next(&attribute, &done)) {
aRv.NoteJSContextException(aCx);
return;
}
if (done) {
break;
}
nsAutoString attrStr;
if (!ConvertJSValueToString(aCx, attribute, eStringify, eStringify, attrStr)) {
aRv.NoteJSContextException(aCx);
return;
}
if (!observedAttributes.AppendElement(NS_Atomize(attrStr))) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
}
}
}
} // Unset mIsCustomDefinitionRunning
/**
* 11. 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));
/**
* 12. Add definition to this CustomElementRegistry.
*/
if (!mConstructors.put(constructorUnwrapped, nameAtom)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
RefPtr<CustomElementDefinition> definition =
new CustomElementDefinition(nameAtom,
localNameAtom,
&aFunctionConstructor,
Move(observedAttributes),
Move(callbacksHolder));
CustomElementDefinition* def = definition.get();
mCustomDefinitions.Put(nameAtom, definition.forget());
MOZ_ASSERT(mCustomDefinitions.Count() == mConstructors.count(),
"Number of entries should be the same");
/**
* 13. 14. 15. Upgrade candidates
*/
UpgradeCandidates(nameAtom, def, aRv);
/**
* 16. 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->MaybeResolveWithUndefined();
}
/**
* 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.Put(nameAtom, callback.forget());
return;
}
void
CustomElementRegistry::Get(JSContext* aCx, const nsAString& aName,
JS::MutableHandle<JS::Value> aRetVal)
{
RefPtr<nsAtom> nameAtom(NS_Atomize(aName));
CustomElementDefinition* data = mCustomDefinitions.GetWeak(nameAtom);
if (!data) {
aRetVal.setUndefined();
return;
}
aRetVal.setObject(*data->mConstructor->Callback(aCx));
}
already_AddRefed<Promise>
CustomElementRegistry::WhenDefined(const nsAString& aName, ErrorResult& aRv)
{
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
RefPtr<Promise> promise = Promise::Create(global, aRv);
if (aRv.Failed()) {
return nullptr;
}
RefPtr<nsAtom> nameAtom(NS_Atomize(aName));
nsIDocument* doc = mWindow->GetExtantDoc();
uint32_t nameSpaceID = doc ? doc->GetDefaultNamespaceID() : kNameSpaceID_XHTML;
if (!nsContentUtils::IsCustomElementName(nameAtom, nameSpaceID)) {
promise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR);
return promise.forget();
}
if (mCustomDefinitions.GetWeak(nameAtom)) {
promise->MaybeResolve(JS::UndefinedHandleValue);
return promise.forget();
}
auto entry = mWhenDefinedPromiseMap.LookupForAdd(nameAtom);
if (entry) {
promise = entry.Data();
} else {
entry.OrInsert([&promise](){ return promise; });
}
return promise.forget();
}
namespace {
static void
DoUpgrade(Element* aElement,
CustomElementConstructor* aConstructor,
ErrorResult& aRv)
{
// Rethrow the exception since it might actually throw the exception from the
// upgrade steps back out to the caller of document.createElement.
RefPtr<Element> constructResult =
aConstructor->Construct("Custom Element Upgrade", aRv);
if (aRv.Failed()) {
return;
}
if (!constructResult || constructResult.get() != aElement) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
}
} // anonymous namespace
// https://html.spec.whatwg.org/multipage/scripting.html#upgrades
/* static */ void
CustomElementRegistry::Upgrade(Element* aElement,
CustomElementDefinition* aDefinition,
ErrorResult& aRv)
{
RefPtr<CustomElementData> data = aElement->GetCustomElementData();
MOZ_ASSERT(data, "CustomElementData should exist");
// Step 1 and step 2.
if (data->mState == CustomElementData::State::eCustom ||
data->mState == CustomElementData::State::eFailed) {
return;
}
// Step 3.
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);
nsContentUtils::NameSpaceManager()->GetNameSpaceURI(namespaceID,
namespaceURI);
LifecycleCallbackArgs args = {
nsDependentAtomString(attrName),
VoidString(),
attrValue,
(namespaceURI.IsEmpty() ? VoidString() : namespaceURI)
};
nsContentUtils::EnqueueLifecycleCallback(nsIDocument::eAttributeChanged,
aElement,
&args, nullptr, aDefinition);
}
}
}
// Step 4.
if (aElement->IsInComposedDoc()) {
nsContentUtils::EnqueueLifecycleCallback(nsIDocument::eConnected, aElement,
nullptr, nullptr, aDefinition);
}
// Step 5.
AutoConstructionStackEntry acs(aDefinition->mConstructionStack, aElement);
// Step 6 and step 7.
DoUpgrade(aElement, aDefinition->mConstructor, aRv);
if (aRv.Failed()) {
data->mState = CustomElementData::State::eFailed;
// Empty element's custom element reaction queue.
data->mReactionQueue.Clear();
return;
}
// Step 8.
data->mState = CustomElementData::State::eCustom;
// Step 9.
aElement->SetCustomElementDefinition(aDefinition);
}
//-----------------------------------------------------
// 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, 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.RemoveElementAt(lastIndex);
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(Move(aCustomElementCallback)));
}
void
CustomElementReactionsStack::Enqueue(Element* aElement,
CustomElementReaction* aReaction)
{
RefPtr<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");
MOZ_ASSERT(!aReaction->IsUpgradeReaction() ||
nsContentUtils::IsChromeDoc(aElement->OwnerDoc()),
"Upgrade reaction should not be scheduled to backup queue "
"except when Custom Element is used inside XBL (in chrome).");
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);
RefPtr<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(Move(reactions.ElementAt(j)));
if (reaction) {
if (!aGlobal && reaction->IsUpgradeReaction()) {
// This is for the special case when custom element is included
// inside XBL.
MOZ_ASSERT(nsContentUtils::IsChromeDoc(element->OwnerDoc()));
nsIGlobalObject* global = element->GetOwnerGlobal();
MOZ_ASSERT(!aes);
aes.emplace(global, "custom elements reaction invocation");
}
ErrorResult rv;
reaction->Invoke(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_CLASS(CustomElementDefinition)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CustomElementDefinition)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mConstructor)
tmp->mCallbacks = nullptr;
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CustomElementDefinition)
mozilla::dom::LifecycleCallbacks* callbacks = tmp->mCallbacks.get();
if (callbacks->mAttributeChangedCallback.WasPassed()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb,
"mCallbacks->mAttributeChangedCallback");
cb.NoteXPCOMChild(callbacks->mAttributeChangedCallback.Value());
}
if (callbacks->mConnectedCallback.WasPassed()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mCallbacks->mConnectedCallback");
cb.NoteXPCOMChild(callbacks->mConnectedCallback.Value());
}
if (callbacks->mDisconnectedCallback.WasPassed()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mCallbacks->mDisconnectedCallback");
cb.NoteXPCOMChild(callbacks->mDisconnectedCallback.Value());
}
if (callbacks->mAdoptedCallback.WasPassed()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mCallbacks->mAdoptedCallback");
cb.NoteXPCOMChild(callbacks->mAdoptedCallback.Value());
}
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mConstructor");
cb.NoteXPCOMChild(tmp->mConstructor);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CustomElementDefinition)
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CustomElementDefinition, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CustomElementDefinition, Release)
CustomElementDefinition::CustomElementDefinition(nsAtom* aType,
nsAtom* aLocalName,
Function* aConstructor,
nsTArray<RefPtr<nsAtom>>&& aObservedAttributes,
UniquePtr<LifecycleCallbacks>&& aCallbacks)
: mType(aType),
mLocalName(aLocalName),
mConstructor(new CustomElementConstructor(aConstructor)),
mObservedAttributes(Move(aObservedAttributes)),
mCallbacks(Move(aCallbacks))
{
}
} // namespace dom
} // namespace mozilla