Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/ElementInternals.h"
#include "mozAutoDocUpdate.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/CustomEvent.h"
#include "mozilla/dom/CustomStateSet.h"
#include "mozilla/dom/ElementInternalsBinding.h"
#include "mozilla/dom/FormData.h"
#include "mozilla/dom/HTMLElement.h"
#include "mozilla/dom/HTMLFieldSetElement.h"
#include "mozilla/dom/MutationEventBinding.h"
#include "mozilla/dom/MutationObservers.h"
#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/dom/ValidityState.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsGenericHTMLElement.h"
#ifdef ACCESSIBILITY
# include "nsAccessibilityService.h"
#endif
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ElementInternals)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ElementInternals)
tmp->Unlink();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTarget, mSubmissionValue, mState, mValidity,
mValidationAnchor, mCustomStateSet);
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ElementInternals)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTarget, mSubmissionValue, mState,
mValidity, mValidationAnchor,
mCustomStateSet);
for (auto& tableEntry : tmp->mAttrElementsMap) {
auto& [explicitlySetElements, cachedAttrElements] =
*tableEntry.GetModifiableData();
ImplCycleCollectionTraverse(cb, cachedAttrElements,
"cached attribute elements entry", 0);
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(ElementInternals)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ElementInternals)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ElementInternals)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsIFormControl)
NS_INTERFACE_MAP_ENTRY(nsIConstraintValidation)
NS_INTERFACE_MAP_END
ElementInternals::ElementInternals(HTMLElement* aTarget)
: nsIFormControl(FormControlType::FormAssociatedCustomElement),
mTarget(aTarget),
mForm(nullptr),
mFieldSet(nullptr),
mControlNumber(-1) {}
nsISupports* ElementInternals::GetParentObject() { return ToSupports(mTarget); }
JSObject* ElementInternals::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return ElementInternals_Binding::Wrap(aCx, this, aGivenProto);
}
ShadowRoot* ElementInternals::GetShadowRoot() const {
MOZ_ASSERT(mTarget);
ShadowRoot* shadowRoot = mTarget->GetShadowRoot();
if (shadowRoot && !shadowRoot->IsAvailableToElementInternals()) {
return nullptr;
}
return shadowRoot;
}
void ElementInternals::SetFormValue(
const Nullable<FileOrUSVStringOrFormData>& aValue,
const Optional<Nullable<FileOrUSVStringOrFormData>>& aState,
ErrorResult& aRv) {
MOZ_ASSERT(mTarget);
/**
* 1. Let element be this's target element.
* 2. If element is not a form-associated custom element, then throw a
* "NotSupportedError" DOMException.
*/
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return;
}
/**
* 3. Set target element's submission value to value if value is not a
* FormData object, or to a clone of the entry list associated with value
* otherwise.
*/
mSubmissionValue.SetNull();
if (!aValue.IsNull()) {
const FileOrUSVStringOrFormData& value = aValue.Value();
OwningFileOrUSVStringOrFormData& owningValue = mSubmissionValue.SetValue();
if (value.IsFormData()) {
owningValue.SetAsFormData() = value.GetAsFormData().Clone();
} else if (value.IsFile()) {
owningValue.SetAsFile() = &value.GetAsFile();
} else {
owningValue.SetAsUSVString() = value.GetAsUSVString();
}
}
/**
* 4. If the state argument of the function is omitted, set element's state to
* its submission value.
*/
if (!aState.WasPassed()) {
mState = mSubmissionValue;
return;
}
/**
* 5. Otherwise, if state is a FormData object, set element's state to clone
* of the entry list associated with state.
* 6. Otherwise, set element's state to state.
*/
mState.SetNull();
if (!aState.Value().IsNull()) {
const FileOrUSVStringOrFormData& state = aState.Value().Value();
OwningFileOrUSVStringOrFormData& owningState = mState.SetValue();
if (state.IsFormData()) {
owningState.SetAsFormData() = state.GetAsFormData().Clone();
} else if (state.IsFile()) {
owningState.SetAsFile() = &state.GetAsFile();
} else {
owningState.SetAsUSVString() = state.GetAsUSVString();
}
}
}
HTMLFormElement* ElementInternals::GetForm(ErrorResult& aRv) const {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return nullptr;
}
return GetForm();
}
void ElementInternals::SetValidity(
const ValidityStateFlags& aFlags, const Optional<nsAString>& aMessage,
const Optional<NonNull<nsGenericHTMLElement>>& aAnchor, ErrorResult& aRv) {
MOZ_ASSERT(mTarget);
/**
* 1. Let element be this's target element.
* 2. If element is not a form-associated custom element, then throw a
* "NotSupportedError" DOMException.
*/
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return;
}
/**
* 3. If flags contains one or more true values and message is not given or is
* the empty string, then throw a TypeError.
*/
if ((aFlags.mBadInput || aFlags.mCustomError || aFlags.mPatternMismatch ||
aFlags.mRangeOverflow || aFlags.mRangeUnderflow ||
aFlags.mStepMismatch || aFlags.mTooLong || aFlags.mTooShort ||
aFlags.mTypeMismatch || aFlags.mValueMissing) &&
(!aMessage.WasPassed() || aMessage.Value().IsEmpty())) {
aRv.ThrowTypeError("Need to provide validation message");
return;
}
/**
* 4. For each entry flag → value of flags, set element's validity flag with
* the name flag to value.
*/
SetValidityState(VALIDITY_STATE_VALUE_MISSING, aFlags.mValueMissing);
SetValidityState(VALIDITY_STATE_TYPE_MISMATCH, aFlags.mTypeMismatch);
SetValidityState(VALIDITY_STATE_PATTERN_MISMATCH, aFlags.mPatternMismatch);
SetValidityState(VALIDITY_STATE_TOO_LONG, aFlags.mTooLong);
SetValidityState(VALIDITY_STATE_TOO_SHORT, aFlags.mTooShort);
SetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW, aFlags.mRangeUnderflow);
SetValidityState(VALIDITY_STATE_RANGE_OVERFLOW, aFlags.mRangeOverflow);
SetValidityState(VALIDITY_STATE_STEP_MISMATCH, aFlags.mStepMismatch);
SetValidityState(VALIDITY_STATE_BAD_INPUT, aFlags.mBadInput);
SetValidityState(VALIDITY_STATE_CUSTOM_ERROR, aFlags.mCustomError);
mTarget->UpdateValidityElementStates(true);
/**
* 5. Set element's validation message to the empty string if message is not
* given or all of element's validity flags are false, or to message
* otherwise.
* 6. If element's customError validity flag is true, then set element's
* custom validity error message to element's validation message.
* Otherwise, set element's custom validity error message to the empty
* string.
*/
mValidationMessage =
(!aMessage.WasPassed() || IsValid()) ? EmptyString() : aMessage.Value();
/**
* 7. Set element's validation anchor to null if anchor is not given.
* Otherwise, if anchor is not a shadow-including descendant of element,
* then throw a "NotFoundError" DOMException. Otherwise, set element's
* validation anchor to anchor.
*/
nsGenericHTMLElement* anchor =
aAnchor.WasPassed() ? &aAnchor.Value() : nullptr;
// TODO: maybe create something like IsShadowIncludingDescendantOf if there
// are other places also need such check.
if (anchor && (anchor == mTarget ||
!anchor->IsShadowIncludingInclusiveDescendantOf(mTarget))) {
aRv.ThrowNotFoundError(
"Validation anchor is not a shadow-including descendant of target"
"element");
return;
}
mValidationAnchor = anchor;
}
bool ElementInternals::GetWillValidate(ErrorResult& aRv) const {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return false;
}
return WillValidate();
}
ValidityState* ElementInternals::GetValidity(ErrorResult& aRv) {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return nullptr;
}
return Validity();
}
void ElementInternals::GetValidationMessage(nsAString& aValidationMessage,
ErrorResult& aRv) const {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return;
}
aValidationMessage = mValidationMessage;
}
bool ElementInternals::CheckValidity(ErrorResult& aRv) {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return false;
}
return nsIConstraintValidation::CheckValidity(*mTarget);
}
bool ElementInternals::ReportValidity(ErrorResult& aRv) {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return false;
}
bool defaultAction = true;
if (nsIConstraintValidation::CheckValidity(*mTarget, &defaultAction)) {
return true;
}
if (!defaultAction) {
return false;
}
AutoTArray<RefPtr<Element>, 1> invalidElements;
invalidElements.AppendElement(mTarget);
AutoJSAPI jsapi;
if (!jsapi.Init(mTarget->GetOwnerGlobal())) {
return false;
}
JS::Rooted<JS::Value> detail(jsapi.cx());
if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) {
return false;
}
RefPtr<CustomEvent> event =
NS_NewDOMCustomEvent(mTarget->OwnerDoc(), nullptr, nullptr);
event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns,
/* CanBubble */ true,
/* Cancelable */ true, detail);
event->SetTrusted(true);
event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
mTarget->DispatchEvent(*event);
return false;
}
already_AddRefed<nsINodeList> ElementInternals::GetLabels(
ErrorResult& aRv) const {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return nullptr;
}
return mTarget->Labels();
}
nsGenericHTMLElement* ElementInternals::GetValidationAnchor(
ErrorResult& aRv) const {
MOZ_ASSERT(mTarget);
if (!mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return nullptr;
}
return mValidationAnchor;
}
CustomStateSet* ElementInternals::States() {
if (!mCustomStateSet) {
mCustomStateSet = new CustomStateSet(mTarget);
}
return mCustomStateSet;
}
void ElementInternals::SetForm(HTMLFormElement* aForm) { mForm = aForm; }
void ElementInternals::ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) {
if (mTarget) {
mTarget->ClearForm(aRemoveFromForm, aUnbindOrDelete);
}
}
NS_IMETHODIMP ElementInternals::Reset() {
if (mTarget) {
MOZ_ASSERT(mTarget->IsFormAssociatedElement());
nsContentUtils::EnqueueLifecycleCallback(ElementCallbackType::eFormReset,
mTarget, {});
}
return NS_OK;
}
NS_IMETHODIMP ElementInternals::SubmitNamesValues(FormData* aFormData) {
if (!mTarget) {
return NS_ERROR_UNEXPECTED;
}
MOZ_ASSERT(mTarget->IsFormAssociatedElement());
if (!mSubmissionValue.IsNull()) {
if (mSubmissionValue.Value().IsFormData()) {
aFormData->Append(mSubmissionValue.Value().GetAsFormData());
return NS_OK;
}
// Get the name
nsAutoString name;
if (!mTarget->GetAttr(nsGkAtoms::name, name) || name.IsEmpty()) {
return NS_OK;
}
if (mSubmissionValue.Value().IsUSVString()) {
return aFormData->AddNameValuePair(
name, mSubmissionValue.Value().GetAsUSVString());
}
return aFormData->AddNameBlobPair(name,
mSubmissionValue.Value().GetAsFile());
}
return NS_OK;
}
void ElementInternals::UpdateFormOwner() {
if (mTarget) {
mTarget->UpdateFormOwner();
}
}
void ElementInternals::UpdateBarredFromConstraintValidation() {
if (mTarget) {
MOZ_ASSERT(mTarget->IsFormAssociatedElement());
SetBarredFromConstraintValidation(
mTarget->IsDisabled() || mTarget->HasAttr(nsGkAtoms::readonly) ||
mTarget->HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR));
}
}
void ElementInternals::Unlink() {
if (mForm) {
// Don't notify, since we're being destroyed in any case.
ClearForm(true, true);
MOZ_DIAGNOSTIC_ASSERT(!mForm);
}
if (mFieldSet) {
mFieldSet->RemoveElement(mTarget);
mFieldSet = nullptr;
}
mAttrElementsMap.Clear();
}
void ElementInternals::GetAttr(const nsAtom* aName, nsAString& aResult) const {
MOZ_ASSERT(aResult.IsEmpty(), "Should have empty string coming in");
const nsAttrValue* val = mAttrs.GetAttr(aName);
if (val) {
val->ToString(aResult);
return;
}
SetDOMStringToNull(aResult);
}
nsresult ElementInternals::SetAttr(nsAtom* aName, const nsAString& aValue) {
Document* document = mTarget->GetComposedDoc();
mozAutoDocUpdate updateBatch(document, true);
uint8_t modType = mAttrs.HasAttr(aName) ? MutationEvent_Binding::MODIFICATION
: MutationEvent_Binding::ADDITION;
MutationObservers::NotifyARIAAttributeDefaultWillChange(mTarget, aName,
modType);
nsAttrValue attrValue(aValue);
nsresult rs = NS_OK;
if (DOMStringIsNull(aValue)) {
auto attrPos = mAttrs.IndexOfAttr(aName);
if (attrPos >= 0) {
rs = mAttrs.RemoveAttrAt(attrPos, attrValue);
}
} else {
bool attrHadValue = false;
rs = mAttrs.SetAndSwapAttr(aName, attrValue, &attrHadValue);
}
nsMutationGuard::DidMutate();
MutationObservers::NotifyARIAAttributeDefaultChanged(mTarget, aName, modType);
return rs;
}
nsresult ElementInternals::SetAttrInternal(nsAtom* aName,
const nsAString& aValue) {
bool attrHadValue;
nsAttrValue attrValue(aValue);
return mAttrs.SetAndSwapAttr(aName, attrValue, &attrHadValue);
}
nsresult ElementInternals::UnsetAttrInternal(nsAtom* aName) {
nsAttrValue attrValue;
auto attrPos = mAttrs.IndexOfAttr(aName);
if (attrPos >= 0) {
return mAttrs.RemoveAttrAt(attrPos, attrValue);
}
return NS_OK;
}
DocGroup* ElementInternals::GetDocGroup() {
return mTarget->OwnerDoc()->GetDocGroup();
}
void ElementInternals::RestoreFormValue(
Nullable<OwningFileOrUSVStringOrFormData>&& aValue,
Nullable<OwningFileOrUSVStringOrFormData>&& aState) {
mSubmissionValue = aValue;
mState = aState;
if (!mState.IsNull()) {
LifecycleCallbackArgs args;
args.mState = mState;
args.mReason = RestoreReason::Restore;
nsContentUtils::EnqueueLifecycleCallback(
ElementCallbackType::eFormStateRestore, mTarget, args);
}
}
void ElementInternals::InitializeControlNumber() {
MOZ_ASSERT(mControlNumber == -1,
"FACE control number should only be initialized once!");
mControlNumber = mTarget->OwnerDoc()->GetNextControlNumber();
}
void ElementInternals::SetAttrElement(nsAtom* aAttr, Element* aElement) {
// Accessibility requires that no other attribute changes occur between
// AttrElementWillChange and AttrElementChanged. Scripts could cause
// this, so don't let them run here. We do this even if accessibility isn't
// running so that the JS behavior is consistent regardless of accessibility.
// Otherwise, JS might be able to use this difference to determine whether
// accessibility is running, which would be a privacy concern.
nsAutoScriptBlocker scriptBlocker;
#ifdef ACCESSIBILITY
// If the target has this attribute defined then it overrides the defaults
// defined here in the Internals instance. In that case we don't need to
// notify the change to a11y since the attribute hasn't changed, just the
// underlying default. We can set accService to null and not notify.
nsAccessibilityService* accService =
!mTarget->HasAttr(aAttr) ? GetAccService() : nullptr;
if (accService) {
accService->NotifyAttrElementWillChange(mTarget, aAttr);
}
#endif
if (aElement) {
mAttrElementMap.InsertOrUpdate(aAttr, do_GetWeakReference(aElement));
SetAttrInternal(aAttr, EmptyString());
} else {
mAttrElementMap.Remove(aAttr);
UnsetAttrInternal(aAttr);
}
#ifdef ACCESSIBILITY
if (accService) {
accService->NotifyAttrElementChanged(mTarget, aAttr);
}
#endif
}
Element* ElementInternals::GetAttrElement(nsAtom* aAttr) const {
nsWeakPtr weakAttrEl = mAttrElementMap.Get(aAttr);
nsCOMPtr<Element> attrEl = do_QueryReferent(weakAttrEl);
return attrEl;
}
void ElementInternals::SetAttrElements(
nsAtom* aAttr,
const Nullable<Sequence<OwningNonNull<Element>>>& aElements) {
#ifdef ACCESSIBILITY
nsAccessibilityService* accService = GetAccService();
#endif
// Accessibility requires that no other attribute changes occur between
// AttrElementWillChange and AttrElementChanged. Scripts could cause
// this, so don't let them run here. We do this even if accessibility isn't
// running so that the JS behavior is consistent regardless of accessibility.
// Otherwise, JS might be able to use this difference to determine whether
// accessibility is running, which would be a privacy concern.
nsAutoScriptBlocker scriptBlocker;
#ifdef ACCESSIBILITY
if (accService) {
accService->NotifyAttrElementWillChange(mTarget, aAttr);
}
#endif
nsAttrValue emptyAttr;
if (aElements.IsNull()) {
mAttrElementsMap.Remove(aAttr);
UnsetAttrInternal(aAttr);
} else {
auto& [attrElements, cachedAttrElements] =
mAttrElementsMap.LookupOrInsert(aAttr);
attrElements.Clear();
for (Element* el : aElements.Value()) {
attrElements.AppendElement(do_GetWeakReference(el));
}
SetAttrInternal(aAttr, EmptyString());
}
#ifdef ACCESSIBILITY
if (accService) {
accService->NotifyAttrElementChanged(mTarget, aAttr);
}
#endif
}
void ElementInternals::GetAttrElements(
nsAtom* aAttr, bool* aUseCachedValue,
Nullable<nsTArray<RefPtr<Element>>>& aElements) {
MOZ_ASSERT(aElements.IsNull());
auto attrElementsMaybeEntry = mAttrElementsMap.Lookup(aAttr);
if (!attrElementsMaybeEntry) {
return;
}
aElements.SetValue(nsTArray<RefPtr<Element>>());
auto& [attrElements, cachedAttrElements] = attrElementsMaybeEntry.Data();
auto getAttrAssociatedElements = [&, &attrElements = attrElements]() {
CopyableTArray<RefPtr<Element>> elements;
for (const nsWeakPtr& weakEl : attrElements) {
// For each attrElement in reflectedTarget's explicitly set attr-elements:
if (nsCOMPtr<Element> attrEl = do_QueryReferent(weakEl)) {
// Append attrElement to elements.
elements.AppendElement(attrEl);
}
}
return elements;
};
// Getter steps:
// 1. Let elements be the result of running this's get the attr-associated
// elements.
auto elements = getAttrAssociatedElements();
if (elements == cachedAttrElements) {
// 2. If the contents of elements is equal to the contents of this's cached
// attr-associated elements, then return this's cached attr-associated
// elements object.
MOZ_ASSERT(!*aUseCachedValue);
*aUseCachedValue = true;
return;
}
// 3. Let elementsAsFrozenArray be elements, converted to a FrozenArray<T>?.
// (the binding code takes aElements and returns it as a FrozenArray)
// 5. Set this's cached attr-associated elements object to
// elementsAsFrozenArray.
// (the binding code stores the attr-associated elements object in a slot)
// 6. Return elementsAsFrozenArray.
aElements.SetValue(elements.Clone());
// 4. Set this's cached attr-associated elements to elements.
cachedAttrElements = std::move(elements);
}
bool ElementInternals::GetAttrElements(nsAtom* aAttr,
nsTArray<Element*>& aElements) {
aElements.Clear();
auto attrElementsMaybeEntry = mAttrElementsMap.Lookup(aAttr);
if (!attrElementsMaybeEntry) {
return false;
}
auto& [attrElements, cachedAttrElements] = attrElementsMaybeEntry.Data();
for (const nsWeakPtr& weakEl : attrElements) {
if (nsCOMPtr<Element> attrEl = do_QueryReferent(weakEl)) {
aElements.AppendElement(attrEl);
}
}
return true;
}
} // namespace mozilla::dom